Linux的IO系統常用系統調用及分析

    Linux的IO從廣義上來說包括很多類,從狹義上來說只是講磁盤的IO。在本文中我也就只是主要介紹磁盤的IO。在這裏我對Linux的磁盤IO的常用系統調用進行深入一些的分析,希望在大家在磁盤IO產生瓶頸的時候,能夠幫助做優化,同時我也是對之前的一篇博文作總結。轉載此文請標明出處:http://blog.csdn.net/jiang1st2010/article/details/8373063


一、讀磁盤:

ssize_t read(int fd,void * buf ,size_t count);

        讀磁盤時,最常用的系統調用就是read(或者fread)。大家都很熟悉它了,首先fopen打開一個文件,同時malloc一段內存,最後調用read函數將fp指向的文件讀到這段內存當中。執行完畢後,文件讀寫位置會隨讀取到的字節移動。

        雖然很簡單,也最通用,但是read函數的執行過程有些同學可能不大瞭解。這個過程可以總結爲下面這個圖:

     

        圖中從上到下的三個位置依次表示:

  • 文件在磁盤中的存儲地址;
  • 內核維護的文件的cache(也叫做page cache,4k爲一頁,每一頁是一個基本的cache單位);
  • 用戶態的buffer(read函數中分配的那段內存)。

         發起一次讀請求後,內核會先看一下,要讀的文件是否已經緩存在內核的頁面裏面了。如果是,則直接從內核的buffer中複製到用戶態的buffer裏面。如果不是,內核會發起一次對文件的IO,讀到內核的cache中,然後纔會拷貝到buffer中。

         這個行爲有三個特點:

  • read的行爲是一種阻塞的系統調用(堵在這,直到拿到數據爲止);
  • 以內核爲緩衝,從內核到用戶內存進行了一次內存拷貝(而內存拷貝是很佔用CPU的)
  • 沒有顯示地通知使用者從文件的哪個位置開始去讀。使用者需要利用文件指針,通過lseek之類的系統調用來指定位置。

         這三個特點其實都是有很多缺點的(相信在我的描述下大家也體會到了)。對於第二個特點,可以採用direct IO消除這個內核buffer的過程(方法是fopen的時候在標誌位上加一個direct標誌),不過帶來的問題則是無法利用cache,這顯然不是一種很好的解決辦法。所以在很多場景下,直接用read不是很高效的。接下來,我就要依次爲大家介紹幾種更高效的系統調用。


ssize_t pread(intfd, void *buf, size_tcount, off_toffset);

        pread與read在功能上完全一樣,只是多一個參數:要讀的文件的起始地址。在多線程的情況下,多個線程要同時讀同一個文件的不同地址時,要對文件指針加鎖,影響了性能,而用pread後就不需要加鎖了,使程序更加高效。解決了第三個問題。


ssize_t readahead(int fd, off64_t offset, size_t count);

       readahead是一種非阻塞的系統調用,它只要求內核把這段數據預讀到內核的buffer中,並不等待它執行完就返回了。執行readahead後再執行read的話,由於之前已經並行地讓內核讀取了數據了,這時更多地是直接從內核的buffer中直接copy到用戶的buffer裏,效率就有所提升了。這樣就解決了read的第一個問題。我們可以看下面這個例子:

//original function
while(time < Ncnts)
{
   read(fd[i], buf[i], lens);
   process(buf[i]);   //相對較長的處理過程
}

//modified program
while(time < Ncnts)
{
   readahead(fd[i], buf[i], lens);
}

while(time < Ncnts)
{
   read(fd[i], buf[i], lens);
   process(buf[i]);   //相對較長的處理過程
}

        修改後加了readahead之後,readahead很快地發送了讀取消息後就返回了,而process的過程中,readahead使內核並行地讀取磁盤,下一次process的時候數據已經讀取到內核中了,減少了等待read的過程。

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

        剛纔提到的兩個函數中,從內核的buffer到用戶的buffer這個拷貝過程依然沒有處理。剛纔說了,這個內存拷貝的過程是很佔cpu的。爲了解決這個問題,一種方法就是使用mmap(在之前的這篇博文裏我已經介紹瞭如何去使用)、它直接把內核態的地址map到用戶態上,用戶態通過這個指針可以直接訪問(相當於從磁盤跳過cache直接到用戶態)。但是mmap同時存在着兩個重要的問題:

  • cache命中率不如read;
  • 在線程模型下互斥十分嚴重

        對於第一個問題的解釋,和內核的實現機制比較相關。在實際命中cache的時候,調用mmap是沒有進行從用戶態到內核態的切換的,這樣採用LRU更新策略的內核頁面沒法讓這個被訪問的頁面的熱度加1,也就是儘管可能這個頁面通過mmap訪問了許多次,但是都沒有被內核記錄下來,就好像一次也沒有訪問到一樣,這樣LRU很容易地就把它更新掉了。而read一定陷入到內核態了。爲了解決這個問題,可以用一個readahead發起這次內核態的切換(同時提前發起IO)。

        對於第二個問題產生的原因,則是在不命中內核cache的情況下內核從disk讀數據是要加一把mm級別的鎖(內核幫你加的)。加着這個級別的鎖進行IO操作,肯定不是很高效的。readahead可以一定程度解決這個問題,但是更好的辦法是利用一種和readahead一樣但是是阻塞型的系統調用(具體我不知道Linux是否提供了這樣一種函數)。阻塞的方式讓內核預讀磁盤,這樣能夠保證mmap的時候要讀的數據肯定在內核中,儘可能地避免了這把鎖。


二、寫磁盤

        對比read的系統調用,write更多是一種非阻塞的調用方式。即一般是write到內存中就可以返回了。具體的過程如下所示:

        其中有兩個過程會觸發寫磁盤:1)dirty ration(默認40%)超過閾值:此時write會被阻塞,開始同步寫髒頁,直到比例降下去以後才繼續write。2)dirty background ration(默認10%)超過閾值:此時write不被阻塞,會被返回,不過返回之前會喚醒後臺進程刷髒頁。它的行爲是有髒頁就開始刷(不一定是正在write的髒頁)。對於低於10%什麼時候刷髒頁呢?內核的後臺有一個線程,它會週期性地刷髒頁。這個週期在內核中默認是5秒鐘(即每5秒鐘喚醒一次)。它會掃描所有的髒頁,然後找到最老的髒頁變髒的時間超過dirty_expire_centisecs(默認爲30秒),達到這個時間的就刷下去(這個過程與剛纔的那個後臺進程是有些不一樣的)。

        寫磁盤遇到的問題一般是,在內核寫磁盤的過程中,如果這個比例不合適,可能會突然地寫磁盤佔用的IO過大,這樣導致讀磁盤的性能下降。爲了解決這類問題,可以讓寫的更加平滑一些,也就是把這幾個參數都調小一下。

         這樣,我就簡單地把這兩個過程介紹了一下。關於實際使用中的例子,歡迎猛戳這裏。

轉載此文請標明出處:http://blog.csdn.net/jiang1st2010/article/details/8373063







發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章