詳細介紹Linux IO

1.IO概述

分析一下寫操作:

char *buf = malloc(MAX_BUF_SIZE);

strncpy(buf, src, , MAX_BUF_SIZE);

fwrite(buf, MAX_BUF_SIZE, 1, fp);

fclose(fp);

以下圖為例:分析數據流寫入硬盤的過程

malloc的buf對於圖層中的application buffer,即應用程序的buffer;

調用fwrite後,把數據從application buffer 拷貝到瞭 CLib buffer,即C庫標準IObuffer。

fwrite返回後,數據還在CLib buffer,如果這時候進程core掉。這些數據會丟失。沒有寫到磁盤介質上。當調用fclose的時候,fclose調用會把這些數據刷新到磁盤介質上。

除瞭fclose方法外,還有一個主動刷新操作fflush函數,不過fflush函數隻是把數據從CLib buffer 拷貝到page cache 中,並沒有刷新到磁盤上,從page cache刷新到磁盤上可以通過調用fsync函數完成。

fwrite是系統提供的最上層接口,也是最常用的接口。它在用戶進程空間開辟一個buffer,將多次小數據量相鄰寫操作先緩存起來,合並,最終調用write函數一次性寫入(或者將大塊數據分解多次write調用)。

Write函數通過調用系統調用接口,將數據從應用層copy到內核層,所以write會觸發內核態/用戶態切換。當數據到達page cache後,內核並不會立即把數據往下傳遞。而是返回用戶空間。數據什麼時候寫入硬盤,有內核IO調度決定,所以write是一個異步調用。這一點和read不同,read調用是先檢查page cache裡面是否有數據,如果有,就取出來返回用戶,如果沒有,就同步傳遞下去並等待有數據,再返回用戶,所以read是一個同步過程。當然你也可以把write的異步過程改成同步過程,就是在open文件的時候帶上O_SYNC標記。

數據到瞭page cache後,內核有pdflush線程在不停的檢測臟頁,判斷是否要寫回到磁盤中。把需要寫回的頁提交到IO隊列——即IO調度隊列。IO調度隊列調度策略調度何時寫回。

IO隊列有2個主要任務。一是合並相鄰扇區的,而是排序。合並相信很容易理解,排序就是盡量按照磁盤選擇方向和磁頭前進方向排序。因為磁頭尋道時間是和昂貴的。

這裡IO隊列和我們常用的分析工具IOStat關系密切。IOStat中rrqm/s wrqm/s表示讀寫合並個數。avgqu-sz表示平均隊列長度。

內核中有多種IO調度算法。當硬盤是SSD時候,沒有什麼磁道磁頭,人傢是隨機讀寫的,加上這些調度算法反而畫蛇添足。OK,剛好有個調度算法叫noop調度算法,就是什麼都不錯(合並是做瞭)。剛好可以用來配置SSD硬盤的系統。

從IO隊列出來後,就到瞭驅動層(當然內核中有更多的細分層,這裡忽略掉),驅動層通過DMA,將數據寫入磁盤cache。

至於磁盤cache時候寫入磁盤介質,那是磁盤控制器自己的事情。如果想要睡個安慰覺,確認要寫到磁盤介質上。就調用fsync函數吧。可以確定寫到磁盤上瞭。

2.linux IO子系統和文件系統讀寫流程

I/O子系統是個層次很深的系統,數據請求從用戶空間最終到達磁盤,經過瞭復雜的數據流動。

read系統調用的處理分為用戶空間和內核空間處理兩部分。其中,用戶空間處理隻是通過0x80中斷陷入內核,接著調用其中斷服務例程,即sys_read以進入內核處理流程。

對於read系統調用在內核的處理,如上圖所述,經過瞭VFS、具體文件系統,如ext2、頁高速緩沖存層、通用塊層、IO調度層、設備驅動層、和設備層。其中,VFS主要是用來屏蔽下層具體文件系統操作的差異,對上提供一個統一接口,正是因為有瞭這個層次,所以可以把設備抽象成文件。具體文件系統,則定義瞭自己的塊大小、操作集合等。引入cache層的目的,是為瞭提高IO效率。它緩存瞭磁盤上的部分數據,當請求到達時,如果在cache中存在該數據且是最新的,則直接將其傳遞給用戶程序,免除瞭對底層磁盤的操作。通用塊層的主要工作是,接收上層發出的磁盤請求,並最終發出IO請求(BIO)。IO調度層則試圖根據設置好的調度算法對通用塊層的bio請求合並和排序,回調驅動層提供的請求處理函數,以處理具體的IO請求。驅動層的驅動程序對應具體的物理設備,它從上層取出IO請求,並根據該IO請求中指定的信息,通過向具體塊設備的設備控制器發送命令的方式,來操縱設備傳輸數據。設備層都是具體的物理設備。

VFS層

內核函數sys_read是read系統調用在該層的入口點。它根據文件fd指定的索引,從當前進程描述符中取出相應的file對象,並調用vfs_read執行文件讀取操作。vfs_read會調用與具體文件相關的read函數執行讀取操作,file->f_op.read。然後,VFS將控制權交給瞭ext2文件系統。(ext2在此作為示例,進行解析)

Ext2文件系統層

通過ext2_file_operations結構知道,上述函數最終會調用到do_sync_read函數,它是系統通用的讀取函數。所以說,do_sync_read才是ext2層的真實入口。該層入口函數 do_sync_read 調用函數 generic_file_aio_read ,後者判斷本次讀請求的訪問方式,如果是直接 io (filp->f_flags 被設置瞭 O_DIRECT 標志,即不經過 cache)的方式,則調用 generic_file_direct_IO 函數;如果是 page cache 的方式,則調用 do_generic_file_read 函數。它會判斷該頁是否在頁高速緩存,如果是,直接將數據拷貝到用戶空間。如果不在,則調用page_cache_sync_readahead函數執行預讀(檢查是否可以預讀),它會調用mpage_readpages。如果仍然未能命中(可能不允許預讀或者其它原因),則直接跳轉readpage,執行mpage_readpage,從磁盤讀取數據。在mpage_readpages(一次讀多個頁)中,它會將連續的磁盤塊放入同一個BIO,並延緩BIO的提交,直到出現不連續的塊,則直接提交BIO,再繼續處理,以構造另外的BIO。

page cache 結構

圖5顯示瞭一個文件的 page cache 結構。文件被分割為一個個以 page 大小為單元的數據塊,這些數據塊(頁)被組織成一個多叉樹(稱為 radix 樹)。樹中所有葉子節點為一個個頁幀結構(struct page),表示瞭用於緩存該文件的每一個頁。在葉子層最左端的第一個頁保存著該文件的前4096個字節(如果頁的大小為4096字節),接下來的頁保存著文件第二個4096個字節,依次類推。樹中的所有中間節點為組織節點,指示某一地址上的數據所在的頁。此樹的層次可以從0層到6層,所支持的文件大小從0字節到16 T 個字節。樹的根節點指針可以從和文件相關的 address_space 對象(該對象保存在和文件關聯的 inode 對象中)中取得(更多關於 page cache 的結構內容請參見參考資料)。

mpage處理機制就是page cache層要處理的問題。

通用塊層

在緩存層處理末尾,執行mpage_submit_bio之後,會調用generic_make_request函數。這是通用塊層的入口函數。它將bio傳送到IO調度層進行處理。

IO調度層

對bio進行合並、排序,以提高IO效率。然後,調用設備驅動層的回調函數,request_fn,轉到設備驅動層處理。

設備驅動層

request函數對請求隊列中每個bio進行分別處理,根據bio中的信息向磁盤控制器發送命令。處理完成後,調用完成函數end_bio以通知上層完成。

3.IO之流程與buffer概覽

一般情況下,進程在io的時候,要依賴於內核中的一個buffer模塊來和外存發生數據交換行為。另一個角度來說,數據從應用進程自己的buffer流動到外存,中間要先拷貝到內核的buffer中,然後再由內核決定什麼時候把這些載有數據的內核buffer寫出到外存。

“buffer cache”僅僅被內核用於常規文件(磁盤文件)的I/O操作。

內核中的buffer模塊-“buffer cache”(buffer,cache的功能兼備)

一般情況下,read,write系統調用並不直接訪問磁盤。這兩個系統調用僅僅是在用戶空間和內核空間的buffer之間傳遞目標數據。舉個例子,下面的write系統調用僅僅是把3個字節從用戶空間拷貝到內核空間的buffer之後就直接返回瞭

write(fd,”abc”,3);

在以後的某個時間點上,內核把裝著“abc”三個字節的buffer寫入(flush)磁盤。如果另外的進程在這個過程中想要讀剛才被打開寫的那個文件怎麼辦?答案是:內核會從剛才的buffer提供要讀取的數據,而不是從磁盤讀。

當前系統上第一次讀一個文件時,read系統調用觸發內核以blocrk為單位從磁盤讀取文件數據,並把數據blocks存入內核buffer,然後read不斷地從這個buffer取需要的數據,直到buffer中的數據全部被讀完,接下來,內核從磁盤按順序把當前文件後面的blocks再讀入內核buffer,然後read重復之前的動作…

一般的文件訪問,都是這種不斷的順序讀取的行為,為瞭加速應用程序讀磁盤,unix的設計者們為這種普遍的順序讀取行為,設計瞭這樣的機制—-預讀,來保證進程在想讀後續數據的時候,這些後續數據已經的由內核預先從磁盤讀好並且放在buffer裡瞭。這麼做的原因是磁盤的io訪問比內存的io訪問要慢很多,指數級的差別。

read,write從語義和概念上來說,本來是必須要直接和磁盤交互的,調用時間非常長,應用每次在使用這兩個系統的時候,從表象上來說都是被卡住。而有瞭這些buffer,這些系統調用就直接和buffer交互就可以瞭,大幅的加速瞭應用執行。

Linux內核並沒有規定”buffer cache”的尺寸上線,原則上來說,除瞭系統正常運行所必需和用戶進程自身所必需的之外的內存都可以被”buffer cache”使用。而系統和用戶進程需要申請更多的內存的時候,”buffer cache”的內存釋放行為會被觸發,一些長久未被讀取,以及被寫過的臟頁就會被釋放和寫入磁盤,騰出內存,以便被需要的行為方使用。

”buffer cache”有五個flush的觸發點:

1.pdflush(內核線程)定期flush;

2.系統和其他進程需要內存的時候觸發它flush;

3.用戶手工sync,外部命令觸發它flush;

4.proc內核接口觸發flush,”echo 3 >/proc/sys/vm/drop_caches;

5.應用程序內部控制flush。

這個”buffer cache”從概念上的理解就是這些瞭,實際上,更準確的說,linux從2.4開始就不再維護獨立的”buffer cache”模塊瞭,而是把它的功能並入瞭”page cache”這個內存管理的子系統瞭,”buffer cache”現在已經是一個unix系統族的普遍的歷史概念瞭

高性能寫文件

寫100MB的數據

場景1,1次寫1個字節,總共write 100M次;

場景2,1次寫1K個字節,總共write 100K次;

場景3,1次寫4K個字節,總共write 25K次;

場景4,1次寫16k個字節,總共write大約不到7K次。

以上4種寫入方式,內核寫磁盤的次數基本相同,因為寫磁盤的單位是block,而不是字節。現在的系統默認的block都是4k。

第1種性能非常差,user time和system time執行時間都很長,既然寫盤次數都差不多,那他慢在哪兒呢?答案是系統調用的次數太多

第2種,user time和system time都顯著降低,不過system time降低幅度更大

第2種以後,性能差別就不是很高瞭,第3種和第4種性能幾乎一樣

有興趣的朋友可以試一試,如果你的服務器很好,可以適當放大測試樣本。

總而言之,得出的結論是以block的尺寸為write(fd, sizeof(buf),buf)的調用單位就可以瞭,再大對性能也沒什麼太大的提高。

題外話:一個衡量涉及IO程序的好壞的粗略標準是“程序運行應該盡量集中在user time,避免大量的system time”以及“IO的時候肯定是需要一些應用層buf的,比如上述4個場景,匹配就可以瞭(比如場景3,場景1和場景2會導致系統調用次數太多,場景4使用的buf尺寸過於浪費)”

每個系統調用在返回的時候,會有一個從內核態向用戶態切換的間隙,每次在這個間隙裡面,系統要幹兩個事情—-遞送信號和進程調度,其中進程調度會重新計算全部RUN狀態進程的優先級。

系統調用太多的話,遞送信號和進程調度引起的計算量是不容忽視的。

精確地flush “buffer cache”

在很多業務場景下,我們僅僅調用write()把需要寫盤的數據推送至內核的”buffer cache”中,這是很不負責任的。或許我們應該不斷地頻繁地把”buffer cache”中的數據強制flush到磁盤,盡最大可能保證我們的業務數據盡量不因斷電而丟失。

天下沒有免費的午餐,既想要效率(寫入內核buffer),又想要安全性(數據必須flush到外存介質中才安全),這似乎是很矛盾的。SUSv3(Single UNIX Specification Version 3)給瞭這種需求一個折中的解決方案,讓OS盡量滿足我們的苛刻的要求。介紹這個折中方案之前,有兩個SUSv3提案的規范很重要,說明如下:

1.數據完整性同步(synchronized I/O data integrity)

一個常規文件所包含的信息有兩種:文件元數據和文件內容數據。

文件元數據包括:文件所屬用戶、組、訪問權限,文件尺寸,文件硬連接數目,最後訪問時間戳,最後修改時間戳,最後文件元數據修改時間戳,文件數據塊指針。

對於文件內容數據,大傢應該都很清楚是什麼東西。

對於寫操作,這個規范規定瞭,寫文件時保證文件內容數據和必要的文件元數據保持完整性即可。粗糙地舉個例子來解釋這個規范,某次flush內核中的數據到磁盤的時候,僅僅把文件內容數據寫入磁盤即可,但是如果這次寫文件導致瞭文件尺寸的變化,那麼這個文件尺寸作為文件的元數據也需要被寫入磁盤,必要信息保持同步。而其他的文件元數據,例如修改時間,訪問時間一概略去,不需要同步。

2.文件完整性同步(synchronized I/O file integrity)

相對於數據完整性同步而言,這個規范規定瞭,所有內容數據以及元數據都要同步。

下面來介紹linux提供的幾種flush內核緩沖數據的幾種方案,相信看完之後,大傢應該知道上述提及的折中方案是怎樣的:)

1.int fsync(int fd);

文件完整性同步;

2.int fdatasync(int fd);

數據完整性同步。

fdatasync相對於fsync的意義在於,fdatasync大致僅需要一次磁盤操作,而fsync需要兩次磁盤操作。舉例說明一下,假如文件內容改變瞭,但是文件尺寸並沒有發生變化,那調用fdatasync僅僅是把文件內容數據flush到磁盤,而fsync不僅僅把文件內容flush刷入磁盤,還要把文件的last modified time也同步到磁盤文件系統。last modified time屬於文件的元數據,一般情況下文件的元數據和文件內容數據在磁盤上不是連續存放的,寫完內容數據再寫元數據,必然涉及到磁盤的seek,而seek又是機械硬盤速度慢的根源。。。

在某些業務場景下,fdatasync和fsync的這點微小差別會導致應用程序性能的大幅差異。

3.sync_file_range()

這個接口是linux從2.6.17之後實現的,是linux獨有的非標準接口。這個接口提供瞭比fdatasync更為精準的flush數據的能力。詳細請參照man。

4.void sync(void);

強制”buffer cache”中的數據全部flush到磁盤,並且要遵循文件完整性同步。

上面4種方式介紹完畢,open()系統調用的打開文件的標志位,比如O_DSYNC諸如此類的標志,對flush數據的影響和上面幾個接口作用類似。

預讀

上面介紹瞭寫buffer以及如何控制buffer的flush,下面來講一講如何控制讀cache的行為。

讀cache這一塊,基本上,我們可以控制的就是文件的預讀。

我們從POSIX規定的一個接口來論述一下如何控制文件的預讀以及控制它的意義。接口原型如下:

int posix_fadvise(int fd, off_t offset, off_t len, int advice);

fd:打開文件的描述符其實;

offset和len:指明文件區域;

advice:預讀的方式。預讀方式及其意義如下:

POSIX_FADV_NORMAL:內核默認的預讀方式;

POSIX_FADV_RANDOM:內核禁用預讀。適合隨機讀文件的業務,每次按業務要求的量讀取數據,不多讀;

POSIX_FADV_SEQUENTIALP:內核把默認的預讀量(POSIX_FADV_NORMAL)擴大一倍;

POSIX_FADV_WILLNEED:讀取出來的內容會被應用程序多次訪問(就是應用程序會不斷的調用read()對這些內容不斷的讀);

POSIX_FADV_NOREUSE:讀取出來的內容隻會被應用程序訪問一次,訪問一次之後就清理掉並且釋放內存。cache服務器,比如memcache或者redis啟動時,把文件內容加載到應用層cache,就是這個參數存在的典型場景;

POSIX_FADV_DONTNEED:應用程序後續不打算訪問指定范圍中的文件內容,內核從”page cache(buffer cache)”中刪除指定范圍的文件內容,釋放內存。

對於POSIX_FADV_WILLNEED這種方式,linux自己有一個特定接口,原型如下:

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

linux的”buffer cache”默認預讀128k。

實際上,OS全局控制”buffer cache”的操作接口不僅僅是上面提及的幾種,/proc/sys/vm/目錄下還有幾個參數可以從其他一些方面來控制”buffer cache”的行為,這部分內容在之後我整理筆記之後會介紹。

IO之標準C庫buffer

在論述這個主題之前,先介紹一下標準C庫和linux系統調用以及windows API之間的關系。

拿寫文件來舉個例子

linux下寫文件用write()

windows下寫文件用WriteFile()

這說明不同操作系統實現同樣的系統功能的接口應該是不一樣的。造成這種現狀是操作系統發展的歷史原因造成的,無法在操作系統的層面統一系統函數接口。同樣功能的程序在linux上寫一套,windows上又得寫另外一套,毫無移植性可言。如果要開發一個既能在linux跑,又能在windows上跑的程序,開發成本飆升!

為瞭解決這個移植性的問題,標準C庫利用瞭封裝技術,扮演瞭一個重要的角色,統一瞭部分基本功能接口。

標準C規定的寫文件的函數是fwrite(),就是不管在linux還是在windows上,各自都有一個標準C庫,庫函數封裝的下層細節不一樣,但是接口完全一樣,提供的功能完全一樣。

這是怎麼做到的?猜一猜大致實現就知道瞭

在linux上,標準C接口fwrite()的實現偽代碼

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream){ … … return write(stream->fd,buffer,count);}

在windows上,標準C接口fwrite()的實現偽代碼

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream){#define OUT BOOL ret = false; OUT int optnum; … … ret = WriteFile(stream->filehandle, buffer, count, &;optnum,…); if( ret == true) return optnum; else return -1;}

內部實現不一致,沒關系,接口一樣就可以,不管在linux還是windows上,寫文件都用fwrite(),分別在各自平臺上編譯就可以瞭。

標準C就是這樣一個處於系統層面之上的應用層標準函數庫,為瞭統一各個操作系統上的函數接口而生。

回到我們的主題—-IO之應用層buffer

什麼是應用層buffer?

回想一下我之前介紹的《IO之內核buffer”buffer cache”》,既然write()能把需要寫文件的數據推送到一個內核buffer來偷工減料欺騙應用層(為瞭加速I/O),說“我已經寫完文件並返回瞭”。那應用層的標準C庫的fwrite()按道理也可以為瞭加速,在真正調用write()之前,把數據放到(FILE*)stream->buffer中,等到多次調用fwrite(),直至(FILE*)stream->buffer中積攢的數據量達到(FILE*)stream->bufferlen這麼多的時候,一次性的把這些數據全部送入write()接口,寫入內核,這是多麼美妙啊。。。

實際上,標準C庫就是這麼做的!

把fwrite()的linux實現再細致一下

過程其實仍然很粗糙,為瞭突出buffer的重點,計算stream->buffer是否滿,拷貝多少,填充多少這樣的細節和主題無關的東西我略去瞭

size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream){
…

if( stream->buffer滿 ){
write(stream->fd,stream->buffer,stream->bufferlen);

} else{
拷貝buffer內容至stream->buffer

}

…

return count;

//過程很粗糙,為瞭突出buffer的重點,計算stream->buffer是否滿,拷貝多少,填充多少這樣的細節和主題無關的東西我略去瞭

}

fwrite()在windows平臺的實現也基本上是這樣的,也有buffer。

值得一說的是,fread()也有一個讀cache來完成預讀。

setvbuf()和setbuf()都是控制這個標準C庫的buffer的。

還有fflush()是C庫用於flush數據的函數。

以上三個函數,如果大傢有興趣,可以去看看linux上對應的man文檔。

重點是要知道不僅系統的內核有buffer,應用層的C庫同樣也有buffer。這些buffer的唯一作用就是為瞭加速應用,不讓應用老是卡在和磁盤交互上。

說個題外話,實際上對於磁盤、RAID卡、盤陣這樣的外存介質而言,他們各自在硬件上也都有一層前端的buffer,有時也叫cache,用來緩沖讀寫加速。cache越多,價格越貴,性能越好。大型存儲設備一般擁有多層cache,用的是昂貴的SSD。 
需要分享的一點經驗是,不管是標準C庫的buffer也好,內核的”buffer cache”也罷,我們終究對它們的控制力度是有限的。我們在做服務器程序的時候,如果業務上涉及太大的I/O量,需要做服務整體加速的時候,我們一般自己在業務層做一層自己的”buffer”,把業務數據buffer住,攢成以文件系統或者磁盤的block塊單位的大塊數據,然後集中寫,然後集中寫又有集中寫的策略。。。 
再引申一點內容,做高性能大流量的大站的架構,其中最重要幾個架構角色之一就是cache。前端CDN、後端memcache、redis、mysql內部cache等等,都是cache的應用場景,可以說”buffer cache”在服務器領域從軟件實現到硬件加速再到架構,真的是無處不在。

4.IO隊列和IO調度

IO調度和IO隊列

1.向塊設備寫入數據塊或是從塊設備讀出數據塊時,IO請求要先進入IO隊列,等待調度。

2.這個IO隊列和調度的目標是針對某個塊設備而言的,換句話說就是每個塊設備都有一個獨立的IO隊列。 

3.本篇所涉及的所謂的塊設備就是iostat命令裡面列出的形如sda,sdb這樣的塊設備,並不是指物理磁盤。假如一個盤被分成5個分區,那麼在這個主題下,5個分區代表5個塊設備,每個塊設備都有自己獨立的IO隊列。 

4.I/O 調度程序維護這些隊列,以便更有效地利用外存設備。簡單來說,IO調度程序將無序的IO操作變為大致有序的IO請求。比如調度的時候調整幾個IO請求的順序,合並那些寫盤區域相鄰的請求,或者按照寫磁盤的位置排序這些請求,以降低磁頭在磁盤上來回seek的操作,繼而加速IO。 

5.每個隊列的每一次調度都會把整個隊列過一遍,類似於進程調度的時候每次調度都要計算RUN隊列的全部進程的優先級。

 IO隊列深度

這個參數是iostat裡面呈現的,字面意思顯而易見,就是IO隊列的深度,這個參數有何意義呢? 
針對每個機械物理盤,如果這個盤對應的IO隊列深度超過3,那麼基本上表示這個盤處理IO硬件請求有點吃緊,這個盤對應的IO隊列深度怎麼算呢? 
還拿上面一個盤被切成5個分區說事兒,5個分區對應5個塊設備,5個塊設備對應5個IO隊列,這5個IO隊列的深度總和就是這個機械物理盤的IO隊列深度瞭。 
如何解決這個盤的IO請求吃緊呢,最簡單的辦法硬件加速,把這個盤換成SSD盤:) 
說到這兒,我想提一提RAID卡。咱們使用RAID卡把幾個硬盤放在一起,讓系統隻能看見一個塊設備。這個時候,假如有4個盤被放在RAID後面。那麼這個RAID卡對應的塊設備的IO隊列深度允許超過12(4個磁盤,每個盤承受深度為3)。 
SSD盤可承受的IO隊列深度值很大,這個多少深度合適,我沒有註意具體觀察過。

 iostat另一個參數—-“%util”

實際生產系統上,我觀察IO設備是否吃緊,其實是看這個util的。這個值長期高於60,咱們就得考慮物理磁盤IO吃不消瞭。 
如果是使用機械硬盤的服務器上這個值達到90以上,最簡單的解決方案仍然是換SSD盤,換完之後這個值會下降到20左右,非常有效。

 IO調度算法

IO調度算法存在的意義有兩個:一是提高IO吞吐量,二是降低IO響應時間。然而IO吞吐量和IO響應時間往往是矛盾的,為瞭盡量平衡這兩者,IO調度器提供瞭多種調度算法來適應不同的IO請求場景。 
以下幾個算法介紹是網上抄來的,說的很詳細,作者水平很高:) 

1、NOOP 

該算法實現瞭最簡單的FIFO隊列,所有IO請求大致按照先來後到的順序進行操作。之所以說”大致”,原因是NOOP在FIFO的基礎上還做瞭相鄰IO請求的合並,並不是完完全全按照先進先出的規則滿足IO請求。 
假設有如下的io請求序列: 
100,500,101,10,56,1000 
NOOP將會按照如下順序滿足: 
100(101),500,10,56,1000

2、CFQ 

CFQ算法的全寫為Completely Fair Queuing。該算法的特點是按照IO請求的地址進行排序,而不是按照先來後到的順序來進行響應。 
假設有如下的io請求序列: 
100,500,101,10,56,1000 
CFQ將會按照如下順序滿足: 
100,101,500,1000,10,56 
在傳統的SAS盤上,磁盤尋道花去瞭絕大多數的IO響應時間。CFQ的出發點是對IO地址進行排序,以盡量少的磁盤旋轉次數來滿足盡可能多的IO請求。在CFQ算法下,SAS盤的吞吐量大大提高瞭。但是相比於NOOP的缺點是,先來的IO請求並不一定能被滿足,可能會出現餓死的情況。

3、DEADLINE 

DEADLINE在CFQ的基礎上,解決瞭IO請求餓死的極端情況。除瞭CFQ本身具有的IO排序隊列之外,DEADLINE額外分別為讀IO和寫IO提供瞭FIFO隊列。讀FIFO隊列的最大等待時間為500ms,寫FIFO隊列的最大等待時間為5s。FIFO隊列內的IO請求優先級要比CFQ隊列中的高,而讀FIFO隊列的優先級又比寫FIFO隊列的優先級高。優先級可以表示如下: 
FIFO(Read) > FIFO(Write) > CFQ 
這個算法特別適合數據庫這種隨機讀寫的場景。

4、ANTICIPATORY 

CFQ和DEADLINE考慮的焦點在於滿足離散IO請求上。對於連續的IO請求,比如順序讀,並沒有做優化。為瞭滿足隨機IO和順序IO混合的場景,Linux還支持ANTICIPATORY調度算法。ANTICIPATORY的在DEADLINE的基礎上,為每個讀IO都設置瞭6ms的等待時間窗口。如果在這6ms內OS收到瞭相鄰位置的讀IO請求,就可以立即滿足。

IO調度器算法的選擇,既取決於硬件特征,也取決於應用場景。 
在傳統的SAS盤上,CFQ、DEADLINE、ANTICIPATORY都是不錯的選擇;對於專屬的數據庫服務器,DEADLINE的吞吐量和響應時間都表現良好。然而在新興的固態硬盤比如SSD、Fusion IO上,最簡單的NOOP反而可能是最好的算法,因為其他三個算法的優化是基於縮短尋道時間的,而固態硬盤沒有所謂的尋道時間且IO響應時間非常短。

 IO調度算法的查看和設置

查看和修改IO調度器的算法非常簡單。假設我們要對sda進行操作,如下所示: 
cat /sys/block/sda/queue/scheduler 
echo ‘cfq’ >/sys/block/sda/queue/scheduler 
還有持久化設置,不一一列舉瞭。

以上就是Linux IO介紹的詳細內容,更多關於Linux IO的資料請關註WalkonNet其它相關文章!

推薦閱讀: