溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Linux IO的示例分析

發(fā)布時間:2021-04-26 14:36:28 來源:億速云 閱讀:163 作者:小新 欄目:開發(fā)技術(shù)

這篇文章給大家分享的是有關(guān)Linux IO的示例分析的內(nèi)容。小編覺得挺實用的,因此分享給大家做個參考,一起跟隨小編過來看看吧。

什么是Linux系統(tǒng)

Linux是一種免費使用和自由傳播的類UNIX操作系統(tǒng),是一個基于POSIX的多用戶、多任務(wù)、支持多線程和多CPU的操作系統(tǒng),使用Linux能運行主要的Unix工具軟件、應(yīng)用程序和網(wǎng)絡(luò)協(xié)議。

1.IO概述

分析一下寫操作:

char *buf = malloc(MAX_BUF_SIZE);

strncpy(buf, src, , MAX_BUF_SIZE);

fwrite(buf, MAX_BUF_SIZE, 1, fp);

fclose(fp);

以下圖為例:分析數(shù)據(jù)流寫入硬盤的過程

Linux IO的示例分析

malloc的buf對于圖層中的application buffer,即應(yīng)用程序的buffer;

調(diào)用fwrite后,把數(shù)據(jù)從application buffer 拷貝到了 CLib buffer,即C庫標(biāo)準(zhǔn)IObuffer。

fwrite返回后,數(shù)據(jù)還在CLib buffer,如果這時候進程core掉。這些數(shù)據(jù)會丟失。沒有寫到磁盤介質(zhì)上。當(dāng)調(diào)用fclose的時候,fclose調(diào)用會把這些數(shù)據(jù)刷新到磁盤介質(zhì)上。

除了fclose方法外,還有一個主動刷新操作fflush函數(shù),不過fflush函數(shù)只是把數(shù)據(jù)從CLib buffer 拷貝到page cache 中,并沒有刷新到磁盤上,從page cache刷新到磁盤上可以通過調(diào)用fsync函數(shù)完成。

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

Write函數(shù)通過調(diào)用系統(tǒng)調(diào)用接口,將數(shù)據(jù)從應(yīng)用層copy到內(nèi)核層,所以write會觸發(fā)內(nèi)核態(tài)/用戶態(tài)切換。當(dāng)數(shù)據(jù)到達page cache后,內(nèi)核并不會立即把數(shù)據(jù)往下傳遞。而是返回用戶空間。數(shù)據(jù)什么時候?qū)懭胗脖P,有內(nèi)核IO調(diào)度決定,所以write是一個異步調(diào)用。這一點和read不同,read調(diào)用是先檢查page cache里面是否有數(shù)據(jù),如果有,就取出來返回用戶,如果沒有,就同步傳遞下去并等待有數(shù)據(jù),再返回用戶,所以read是一個同步過程。當(dāng)然你也可以把write的異步過程改成同步過程,就是在open文件的時候帶上O_SYNC標(biāo)記。

數(shù)據(jù)到了page cache后,內(nèi)核有pdflush線程在不停的檢測臟頁,判斷是否要寫回到磁盤中。把需要寫回的頁提交到IO隊列——即IO調(diào)度隊列。IO調(diào)度隊列調(diào)度策略調(diào)度何時寫回。

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

這里IO隊列和我們常用的分析工具IOStat關(guān)系密切。IOStat中rrqm/s wrqm/s表示讀寫合并個數(shù)。avgqu-sz表示平均隊列長度。

內(nèi)核中有多種IO調(diào)度算法。當(dāng)硬盤是SSD時候,沒有什么磁道磁頭,人家是隨機讀寫的,加上這些調(diào)度算法反而畫蛇添足。OK,剛好有個調(diào)度算法叫noop調(diào)度算法,就是什么都不錯(合并是做了)。剛好可以用來配置SSD硬盤的系統(tǒng)。

從IO隊列出來后,就到了驅(qū)動層(當(dāng)然內(nèi)核中有更多的細分層,這里忽略掉),驅(qū)動層通過DMA,將數(shù)據(jù)寫入磁盤cache。

至于磁盤cache時候?qū)懭氪疟P介質(zhì),那是磁盤控制器自己的事情。如果想要睡個安慰覺,確認(rèn)要寫到磁盤介質(zhì)上。就調(diào)用fsync函數(shù)吧??梢源_定寫到磁盤上了。

2.linux IO子系統(tǒng)和文件系統(tǒng)讀寫流程

Linux IO的示例分析

I/O子系統(tǒng)是個層次很深的系統(tǒng),數(shù)據(jù)請求從用戶空間最終到達磁盤,經(jīng)過了復(fù)雜的數(shù)據(jù)流動。

read系統(tǒng)調(diào)用的處理分為用戶空間和內(nèi)核空間處理兩部分。其中,用戶空間處理只是通過0x80中斷陷入內(nèi)核,接著調(diào)用其中斷服務(wù)例程,即sys_read以進入內(nèi)核處理流程。

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

VFS層

內(nèi)核函數(shù)sys_read是read系統(tǒng)調(diào)用在該層的入口點。它根據(jù)文件fd指定的索引,從當(dāng)前進程描述符中取出相應(yīng)的file對象,并調(diào)用vfs_read執(zhí)行文件讀取操作。vfs_read會調(diào)用與具體文件相關(guān)的read函數(shù)執(zhí)行讀取操作,file->f_op.read。然后,VFS將控制權(quán)交給了ext2文件系統(tǒng)。(ext2在此作為示例,進行解析)

Ext2文件系統(tǒng)層

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

page cache 結(jié)構(gòu)

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

Linux IO的示例分析

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

通用塊層

在緩存層處理末尾,執(zhí)行mpage_submit_bio之后,會調(diào)用generic_make_request函數(shù)。這是通用塊層的入口函數(shù)。它將bio傳送到IO調(diào)度層進行處理。

IO調(diào)度層

對bio進行合并、排序,以提高IO效率。然后,調(diào)用設(shè)備驅(qū)動層的回調(diào)函數(shù),request_fn,轉(zhuǎn)到設(shè)備驅(qū)動層處理。

設(shè)備驅(qū)動層

request函數(shù)對請求隊列中每個bio進行分別處理,根據(jù)bio中的信息向磁盤控制器發(fā)送命令。處理完成后,調(diào)用完成函數(shù)end_bio以通知上層完成。

3.IO之流程與buffer概覽

Linux IO的示例分析

一般情況下,進程在io的時候,要依賴于內(nèi)核中的一個buffer模塊來和外存發(fā)生數(shù)據(jù)交換行為。另一個角度來說,數(shù)據(jù)從應(yīng)用進程自己的buffer流動到外存,中間要先拷貝到內(nèi)核的buffer中,然后再由內(nèi)核決定什么時候把這些載有數(shù)據(jù)的內(nèi)核buffer寫出到外存。

“buffer cache”僅僅被內(nèi)核用于常規(guī)文件(磁盤文件)的I/O操作。

內(nèi)核中的buffer模塊-“buffer cache”(buffer,cache的功能兼?zhèn)?

一般情況下,read,write系統(tǒng)調(diào)用并不直接訪問磁盤。這兩個系統(tǒng)調(diào)用僅僅是在用戶空間和內(nèi)核空間的buffer之間傳遞目標(biāo)數(shù)據(jù)。舉個例子,下面的write系統(tǒng)調(diào)用僅僅是把3個字節(jié)從用戶空間拷貝到內(nèi)核空間的buffer之后就直接返回了

write(fd,”abc”,3);

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

當(dāng)前系統(tǒng)上第一次讀一個文件時,read系統(tǒng)調(diào)用觸發(fā)內(nèi)核以blocrk為單位從磁盤讀取文件數(shù)據(jù),并把數(shù)據(jù)blocks存入內(nèi)核buffer,然后read不斷地從這個buffer取需要的數(shù)據(jù),直到buffer中的數(shù)據(jù)全部被讀完,接下來,內(nèi)核從磁盤按順序把當(dāng)前文件后面的blocks再讀入內(nèi)核buffer,然后read重復(fù)之前的動作…

一般的文件訪問,都是這種不斷的順序讀取的行為,為了加速應(yīng)用程序讀磁盤,unix的設(shè)計者們?yōu)檫@種普遍的順序讀取行為,設(shè)計了這樣的機制—-預(yù)讀,來保證進程在想讀后續(xù)數(shù)據(jù)的時候,這些后續(xù)數(shù)據(jù)已經(jīng)的由內(nèi)核預(yù)先從磁盤讀好并且放在buffer里了。這么做的原因是磁盤的io訪問比內(nèi)存的io訪問要慢很多,指數(shù)級的差別。

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

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

”buffer cache”有五個flush的觸發(fā)點:

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

2.系統(tǒng)和其他進程需要內(nèi)存的時候觸發(fā)它flush;

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

4.proc內(nèi)核接口觸發(fā)flush,”echo 3 >/proc/sys/vm/drop_caches;

5.應(yīng)用程序內(nèi)部控制flush。

這個”buffer cache”從概念上的理解就是這些了,實際上,更準(zhǔn)確的說,linux從2.4開始就不再維護獨立的”buffer cache”模塊了,而是把它的功能并入了”page cache”這個內(nèi)存管理的子系統(tǒng)了,”buffer cache”現(xiàn)在已經(jīng)是一個unix系統(tǒng)族的普遍的歷史概念了

高性能寫文件

寫100MB的數(shù)據(jù)

場景1,1次寫1個字節(jié),總共write 100M次;

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

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

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

以上4種寫入方式,內(nèi)核寫磁盤的次數(shù)基本相同,因為寫磁盤的單位是block,而不是字節(jié)?,F(xiàn)在的系統(tǒng)默認(rèn)的block都是4k。

第1種性能非常差,user time和system time執(zhí)行時間都很長,既然寫盤次數(shù)都差不多,那他慢在哪兒呢?答案是系統(tǒng)調(diào)用的次數(shù)太多

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

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

有興趣的朋友可以試一試,如果你的服務(wù)器很好,可以適當(dāng)放大測試樣本。

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

題外話:一個衡量涉及IO程序的好壞的粗略標(biāo)準(zhǔn)是“程序運行應(yīng)該盡量集中在user time,避免大量的system time”以及“IO的時候肯定是需要一些應(yīng)用層buf的,比如上述4個場景,匹配就可以了(比如場景3,場景1和場景2會導(dǎo)致系統(tǒng)調(diào)用次數(shù)太多,場景4使用的buf尺寸過于浪費)”

每個系統(tǒng)調(diào)用在返回的時候,會有一個從內(nèi)核態(tài)向用戶態(tài)切換的間隙,每次在這個間隙里面,系統(tǒng)要干兩個事情—-遞送信號和進程調(diào)度,其中進程調(diào)度會重新計算全部RUN狀態(tài)進程的優(yōu)先級。

系統(tǒng)調(diào)用太多的話,遞送信號和進程調(diào)度引起的計算量是不容忽視的。

精確地flush “buffer cache”

在很多業(yè)務(wù)場景下,我們僅僅調(diào)用write()把需要寫盤的數(shù)據(jù)推送至內(nèi)核的”buffer cache”中,這是很不負責(zé)任的?;蛟S我們應(yīng)該不斷地頻繁地把”buffer cache”中的數(shù)據(jù)強制flush到磁盤,盡最大可能保證我們的業(yè)務(wù)數(shù)據(jù)盡量不因斷電而丟失。

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

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

一個常規(guī)文件所包含的信息有兩種:文件元數(shù)據(jù)和文件內(nèi)容數(shù)據(jù)。

文件元數(shù)據(jù)包括:文件所屬用戶、組、訪問權(quán)限,文件尺寸,文件硬連接數(shù)目,最后訪問時間戳,最后修改時間戳,最后文件元數(shù)據(jù)修改時間戳,文件數(shù)據(jù)塊指針。

對于文件內(nèi)容數(shù)據(jù),大家應(yīng)該都很清楚是什么東西。

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

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

相對于數(shù)據(jù)完整性同步而言,這個規(guī)范規(guī)定了,所有內(nèi)容數(shù)據(jù)以及元數(shù)據(jù)都要同步。

下面來介紹linux提供的幾種flush內(nèi)核緩沖數(shù)據(jù)的幾種方案,相信看完之后,大家應(yīng)該知道上述提及的折中方案是怎樣的:)

1.int fsync(int fd);

文件完整性同步;

2.int fdatasync(int fd);

數(shù)據(jù)完整性同步。

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

在某些業(yè)務(wù)場景下,fdatasync和fsync的這點微小差別會導(dǎo)致應(yīng)用程序性能的大幅差異。

3.sync_file_range()

這個接口是linux從2.6.17之后實現(xiàn)的,是linux獨有的非標(biāo)準(zhǔn)接口。這個接口提供了比fdatasync更為精準(zhǔn)的flush數(shù)據(jù)的能力。詳細請參照man。

4.void sync(void);

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

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

預(yù)讀

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

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

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

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

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

offset和len:指明文件區(qū)域;

advice:預(yù)讀的方式。預(yù)讀方式及其意義如下:

POSIX_FADV_NORMAL:內(nèi)核默認(rèn)的預(yù)讀方式;

POSIX_FADV_RANDOM:內(nèi)核禁用預(yù)讀。適合隨機讀文件的業(yè)務(wù),每次按業(yè)務(wù)要求的量讀取數(shù)據(jù),不多讀;

POSIX_FADV_SEQUENTIALP:內(nèi)核把默認(rèn)的預(yù)讀量(POSIX_FADV_NORMAL)擴大一倍;

POSIX_FADV_WILLNEED:讀取出來的內(nèi)容會被應(yīng)用程序多次訪問(就是應(yīng)用程序會不斷的調(diào)用read()對這些內(nèi)容不斷的讀);

POSIX_FADV_NOREUSE:讀取出來的內(nèi)容只會被應(yīng)用程序訪問一次,訪問一次之后就清理掉并且釋放內(nèi)存。cache服務(wù)器,比如memcache或者redis啟動時,把文件內(nèi)容加載到應(yīng)用層cache,就是這個參數(shù)存在的典型場景;

POSIX_FADV_DONTNEED:應(yīng)用程序后續(xù)不打算訪問指定范圍中的文件內(nèi)容,內(nèi)核從”page cache(buffer cache)”中刪除指定范圍的文件內(nèi)容,釋放內(nèi)存。

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

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

linux的”buffer cache”默認(rèn)預(yù)讀128k。

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

IO之標(biāo)準(zhǔn)C庫buffer

在論述這個主題之前,先介紹一下標(biāo)準(zhǔn)C庫和linux系統(tǒng)調(diào)用以及windows API之間的關(guān)系。

拿寫文件來舉個例子

linux下寫文件用write()

windows下寫文件用WriteFile()

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

為了解決這個移植性的問題,標(biāo)準(zhǔn)C庫利用了封裝技術(shù),扮演了一個重要的角色,統(tǒng)一了部分基本功能接口。

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

這是怎么做到的?猜一猜大致實現(xiàn)就知道了

在linux上,標(biāo)準(zhǔn)C接口fwrite()的實現(xiàn)偽代碼

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

在windows上,標(biāo)準(zhǔn)C接口fwrite()的實現(xiàn)偽代碼

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;}

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

標(biāo)準(zhǔn)C就是這樣一個處于系統(tǒng)層面之上的應(yīng)用層標(biāo)準(zhǔn)函數(shù)庫,為了統(tǒng)一各個操作系統(tǒng)上的函數(shù)接口而生。

回到我們的主題—-IO之應(yīng)用層buffer

什么是應(yīng)用層buffer?

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

實際上,標(biāo)準(zhǔn)C庫就是這么做的!

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

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

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內(nèi)容至stream->buffer

}

…

return count;

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

}

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

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

setvbuf()和setbuf()都是控制這個標(biāo)準(zhǔn)C庫的buffer的。

還有fflush()是C庫用于flush數(shù)據(jù)的函數(shù)。

以上三個函數(shù),如果大家有興趣,可以去看看linux上對應(yīng)的man文檔。

重點是要知道不僅系統(tǒng)的內(nèi)核有buffer,應(yīng)用層的C庫同樣也有buffer。這些buffer的唯一作用就是為了加速應(yīng)用,不讓應(yīng)用老是卡在和磁盤交互上。

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

4.IO隊列和IO調(diào)度

Linux IO的示例分析

IO調(diào)度和IO隊列

1.向塊設(shè)備寫入數(shù)據(jù)塊或是從塊設(shè)備讀出數(shù)據(jù)塊時,IO請求要先進入IO隊列,等待調(diào)度。

2.這個IO隊列和調(diào)度的目標(biāo)是針對某個塊設(shè)備而言的,換句話說就是每個塊設(shè)備都有一個獨立的IO隊列。 

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

4.I/O 調(diào)度程序維護這些隊列,以便更有效地利用外存設(shè)備。簡單來說,IO調(diào)度程序?qū)o序的IO操作變?yōu)榇笾掠行虻腎O請求。比如調(diào)度的時候調(diào)整幾個IO請求的順序,合并那些寫盤區(qū)域相鄰的請求,或者按照寫磁盤的位置排序這些請求,以降低磁頭在磁盤上來回seek的操作,繼而加速IO。 

5.每個隊列的每一次調(diào)度都會把整個隊列過一遍,類似于進程調(diào)度的時候每次調(diào)度都要計算RUN隊列的全部進程的優(yōu)先級。

 IO隊列深度

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

 iostat另一個參數(shù)—-“%util”

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

 IO調(diào)度算法

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

1、NOOP 

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

2、CFQ 

CFQ算法的全寫為Completely Fair Queuing。該算法的特點是按照IO請求的地址進行排序,而不是按照先來后到的順序來進行響應(yīng)。 
假設(shè)有如下的io請求序列: 
100,500,101,10,56,1000 
CFQ將會按照如下順序滿足: 
100,101,500,1000,10,56 
在傳統(tǒng)的SAS盤上,磁盤尋道花去了絕大多數(shù)的IO響應(yīng)時間。CFQ的出發(fā)點是對IO地址進行排序,以盡量少的磁盤旋轉(zhuǎn)次數(shù)來滿足盡可能多的IO請求。在CFQ算法下,SAS盤的吞吐量大大提高了。但是相比于NOOP的缺點是,先來的IO請求并不一定能被滿足,可能會出現(xiàn)餓死的情況。

3、DEADLINE 

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

4、ANTICIPATORY 

CFQ和DEADLINE考慮的焦點在于滿足離散IO請求上。對于連續(xù)的IO請求,比如順序讀,并沒有做優(yōu)化。為了滿足隨機IO和順序IO混合的場景,Linux還支持ANTICIPATORY調(diào)度算法。ANTICIPATORY的在DEADLINE的基礎(chǔ)上,為每個讀IO都設(shè)置了6ms的等待時間窗口。如果在這6ms內(nèi)OS收到了相鄰位置的讀IO請求,就可以立即滿足。

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

 IO調(diào)度算法的查看和設(shè)置

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

感謝各位的閱讀!關(guān)于“Linux IO的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,讓大家可以學(xué)到更多知識,如果覺得文章不錯,可以把它分享出去讓更多的人看到吧!

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI