如果您記性好的話,應(yīng)該記得我在linux設(shè)備驅(qū)動(dòng)實(shí)例帖中說的最多的就是字符設(shè)備驅(qū)動(dòng)程序,那么今天的塊I/O層是一個(gè)和字符設(shè)備驅(qū)動(dòng)相對(duì)應(yīng)的設(shè)備。兩 者最根本的區(qū)別就是看它們能否被隨機(jī)訪問,換句話說就是看它們能否在訪問設(shè)備時(shí)從一個(gè)位置隨意地調(diào)到另外一個(gè)位置,如果可以就是塊設(shè)備,否則就字符設(shè)備。 塊設(shè)備中最小的可尋址單元是扇區(qū)。扇區(qū)的大小一般是2的整數(shù)倍,最常見的大小是512個(gè)字節(jié)。扇區(qū)的大小是設(shè)備的物理屬性,扇區(qū)是所有塊設(shè)備的基本單元, 塊設(shè)備無法對(duì)比它還小的單元進(jìn)行尋址和操作,不過許多塊設(shè)備能夠一次就傳輸多個(gè)扇區(qū)。從軟件角度來講,最小的邏輯可尋址單元卻是塊,塊是文件系統(tǒng)的一種抽 象-----只能基于塊來訪問文件系統(tǒng)。雖然物理磁盤尋址是按照扇區(qū)級(jí)進(jìn)行的,但是內(nèi)核執(zhí)行的所有磁盤操作都是按照塊進(jìn)行的。前邊已經(jīng)說過,扇區(qū)是設(shè)備的 最小可尋址單元,所以塊不能比扇區(qū)還小,只能數(shù)倍于扇區(qū)大小。另外內(nèi)核還要求塊大小是2的整數(shù)倍,=而且不能超過一個(gè)頁的長(zhǎng)度,所以大小的最終要求是,必 須是扇區(qū)大小的2的整數(shù)倍,并且要小于頁面大小。所以通常塊大小是512字節(jié),1k或4k。 當(dāng) 一個(gè)塊被調(diào)入內(nèi)存時(shí),它要存儲(chǔ)在一個(gè)緩沖區(qū)中,每個(gè)緩沖區(qū)與一個(gè)塊對(duì)應(yīng),它相當(dāng)于是磁盤塊在內(nèi)存中的表示。另外,由于內(nèi)核在處理數(shù)據(jù)時(shí)需要一些相關(guān)的控制 信息,所以每個(gè)緩沖區(qū)都有一個(gè)叫做buffer_head的描述符來表示,被稱為緩沖區(qū)頭,在linux/buffer_head.h中定義,它包含了內(nèi) 核操作緩沖區(qū)所需要的全部信息,如下:
其中的b_state域表示緩沖區(qū)的狀態(tài),下表給出一種標(biāo)志或多種標(biāo)志的組合,在linux/buffer_head.h中定義了所有合法標(biāo)志的bh_state_bite列表,如下所示: bh_state_bits列表包含了一個(gè)特殊標(biāo)志----BH_PrivateStart,該標(biāo)志不是可用狀態(tài)標(biāo)志,使用它是為了指明可能其它代碼使用 的起始位。塊I/O層不會(huì)使用BH_PrivateStart或更高的位,那么某個(gè)驅(qū)動(dòng)程序希望通過b_state域存儲(chǔ)信息時(shí)就可以安全地使用這些位。 驅(qū)動(dòng)程序可以在這些位中定義自己的狀態(tài)標(biāo)志,只要保證自定義的狀態(tài)標(biāo)志不會(huì)與塊IO層的專用位發(fā)生沖突就可以了。b_count域表示緩沖區(qū)的使用計(jì)數(shù), 可通過兩個(gè)定義在文件linux/buffer_head.h中的內(nèi)聯(lián)函數(shù)對(duì)此域進(jìn)行增減:
在操作緩沖區(qū)頭之前,應(yīng)該先使用get_bh()函數(shù)增加緩沖區(qū)頭的引用計(jì)數(shù),確保緩沖區(qū)頭不會(huì)再被分配出去,當(dāng)完成對(duì)緩沖區(qū)頭的操作之后,還必須使用 put_bh()函數(shù)減少引用計(jì)數(shù)。與緩沖區(qū)對(duì)應(yīng)的磁盤物理塊由b_blocknr域索引,該值是b_bdev域指明的塊設(shè)備中的邏輯塊號(hào)。與緩沖區(qū)對(duì)應(yīng) 的內(nèi)存物理頁由b_page域表示,另外,b_data域直接指向相應(yīng)的塊(它位于b_page域所指明的頁面的某個(gè)位置上),塊的大小由b_size域 表示,所以塊在內(nèi)存中的起始位置在b_data處,結(jié)束位置在(b_data+b_size)處。緩沖區(qū)頭的目的在于描述磁盤塊和物理內(nèi)存緩沖區(qū)(在特定 頁面上的字節(jié)序列)之間的映射關(guān)系。這個(gè)結(jié)構(gòu)體在內(nèi)核中扮演一個(gè)描述符的角色,說明從緩沖區(qū)到塊的映射關(guān)系。使用緩沖區(qū)頭作為I/O操作有它的弊端,這里 不細(xì)說,你明白就好。我們只需知道現(xiàn)在的內(nèi)核采用了一種新型,靈活而且輕量級(jí)的容器---bio結(jié)構(gòu)體。 bio結(jié)構(gòu)體定義在linux/bio.h中,該結(jié)構(gòu)體代表了正在現(xiàn)場(chǎng)的(活動(dòng))以片斷(segment)鏈表形式組織的塊I/O操作。一個(gè)片斷是一小塊 連續(xù)的內(nèi)存緩沖區(qū)。這樣的話,就不需要保證單個(gè)緩沖區(qū)一定要連續(xù),所有通過片斷來描述緩沖區(qū),即使一個(gè)緩沖區(qū)分散在內(nèi)存的多個(gè)位置上,bio結(jié)構(gòu)體也能保 證I/O操作的執(zhí)行。下面給出bio結(jié)構(gòu)體和各個(gè)域的描述,如下:
使用bio結(jié)構(gòu)體的目的主要是代表正在現(xiàn)場(chǎng)執(zhí)行的I/O操作,所有該結(jié)構(gòu)體中的主要域都是用來管理相關(guān)信息的。其中最重要的幾個(gè)域是bi_io_vecs,bi_vcnt和bi_idx.它們之間的關(guān)系如下圖所示: 我在前邊已經(jīng)給出了struct bio的結(jié)構(gòu)體,下面給出struct bio_vec的描述:
下面來分析以上上面的那個(gè)圖,我們說:每一個(gè)塊I/O請(qǐng)求都通過一個(gè)bio結(jié)構(gòu)體表示。每個(gè)請(qǐng)求包含一個(gè)或多個(gè)塊,這些塊存儲(chǔ)在bio_vec結(jié)構(gòu)體數(shù)組 中,這些結(jié)構(gòu)體描述了每個(gè)片斷在物理頁中的實(shí)際位置,并且像向量一樣地組織在一起,IO操作的第一個(gè)片斷由b_io_vec結(jié)構(gòu)體所指向,其他的片斷在其 后依次放置,共有bi_vcnt個(gè)片斷。當(dāng)塊IO開始執(zhí)行請(qǐng)求,需要使用各個(gè)片段時(shí),bi_idx域會(huì)不斷更新,從而總指向當(dāng)前片斷。bi_idx域指向 數(shù)組中的當(dāng)前bio_vec片段,塊IO層通過它跟蹤塊IO操作的完成進(jìn)度。但該域更重要的作用是分割bio結(jié)構(gòu)體。bi_cnt域記錄bio結(jié)構(gòu)體的使 用計(jì)數(shù),如果為0,則應(yīng)該銷毀該bio結(jié)構(gòu)體,并釋放它占用的內(nèi)存。通過下面兩個(gè)函數(shù)管理使用計(jì)數(shù):
最后一個(gè)域是bi_private域,這是一個(gè)屬于擁有者的私有域,誰創(chuàng)建了bio結(jié)構(gòu),誰就可以讀寫該域。 塊設(shè)備將它們掛起的塊IO請(qǐng)求保存在請(qǐng)求隊(duì)列中,該隊(duì)列有request_queue結(jié)構(gòu)體體表示,定義在文件linux/blkdev.h中,包含一個(gè) 雙向請(qǐng)求鏈表以及相關(guān)控制信息。通過內(nèi)核中想文件系統(tǒng)這樣高層的代碼將請(qǐng)求加入到隊(duì)列中。請(qǐng)求隊(duì)列只要不為空,隊(duì)列對(duì)應(yīng)的塊設(shè)備驅(qū)動(dòng)程序就會(huì)從隊(duì)列頭獲取 請(qǐng)求,然后將其送入對(duì)應(yīng)的塊設(shè)備上去請(qǐng)求隊(duì)列表中的每一項(xiàng)都是一個(gè)單獨(dú)的請(qǐng)求,有reques結(jié)構(gòu)體體表示。隊(duì)列中的請(qǐng)求由結(jié)構(gòu)體request表示,定 義在文件linux/blkdev.h表示。因?yàn)橐粋€(gè)請(qǐng)求可能要操作多個(gè)連續(xù)的磁盤塊,所有每個(gè)請(qǐng)求可有由多個(gè)bio結(jié)構(gòu)體組成,注意,雖然磁盤上的塊必 須連續(xù),但是在內(nèi)存中的這些塊并不一定要連續(xù)----每個(gè)bio結(jié)構(gòu)都可以描述多個(gè)片段,而每個(gè)請(qǐng)求也可以包含多個(gè)bio結(jié)構(gòu)體。 好了,我們明白了塊IO請(qǐng)求,下面的就是IO調(diào)度了。每次尋址的操作就是定位磁盤磁頭到特定塊上的某個(gè)位置,為了優(yōu)化尋址操作,內(nèi)核既不會(huì)簡(jiǎn)單地按請(qǐng)求接 收次序,也不會(huì)立即將其提交給磁盤,相反,它會(huì)在提交前,先執(zhí)行名為合并與排序的預(yù)操作,這種預(yù)操作可以極大地提高系統(tǒng)的整體性能。 IO調(diào)度程序通過兩種方法減少磁盤尋址時(shí)間:合并與排序。合并指將兩個(gè)或多個(gè)請(qǐng)求結(jié)合成一個(gè)一個(gè)新請(qǐng)求。關(guān)于排序的,最有名的當(dāng)然就是大名鼎鼎的電梯調(diào) 度。排序就是整個(gè)請(qǐng)求隊(duì)列將按扇區(qū)增長(zhǎng)方向有序排列,使所有請(qǐng)求按磁盤上扇區(qū)的排列順序有序排列的目的不僅是為了縮短單獨(dú)一次請(qǐng)求的尋址時(shí)間,更重要的優(yōu) 化在于,通過保持磁盤頭以直線方向移動(dòng),縮短了所有請(qǐng)求的磁盤尋址的時(shí)間。關(guān)于linux中的電梯調(diào)度程序,很多操作系統(tǒng)的書上都已經(jīng)說的很明白,我這里 給出一個(gè)大致流程:
我前邊提到過電梯調(diào)度,但是有一個(gè)問題一直沒提,那就是電梯調(diào)度程序的缺點(diǎn):饑餓。出于減少磁盤尋址時(shí)間的考慮,對(duì)某個(gè)磁盤區(qū)域上的繁重操作,無疑會(huì)使得 磁盤其他位置上的操作得不到運(yùn)行機(jī)會(huì),實(shí)際上,一個(gè)對(duì)磁盤同一位置操作的請(qǐng)求流可以造成較遠(yuǎn)位置的其他請(qǐng)求永遠(yuǎn)得不到運(yùn)行機(jī)會(huì),這是一種很不公平的饑餓現(xiàn) 象。更糟糕的是,普通的請(qǐng)求饑餓還會(huì)帶來寫--饑餓--讀這種特殊問題。我們知道寫操作通常發(fā)生在內(nèi)核有空時(shí),而讀操作卻必須阻塞知道讀請(qǐng)求被滿足,這對(duì) 系統(tǒng)性能影響是非常大的。而且我們知道讀請(qǐng)求往往相互依靠,比如要讀大量的文件,每次都是針對(duì)一塊很小的緩沖區(qū)進(jìn)行讀操作,而應(yīng)用程序只有將上一個(gè)數(shù)據(jù)區(qū) 域從磁盤中讀取并返回之后,才能繼續(xù)讀取下一個(gè)數(shù)據(jù)區(qū),所以如果每一次請(qǐng)求都發(fā)生饑餓現(xiàn)象,那么對(duì)讀取文件的應(yīng)用程序來說,全部延遲加起來會(huì)造成過長(zhǎng)的等 待時(shí)間。減少饑餓請(qǐng)求必須以降低全局吞吐量為代價(jià)。為了避免這種問題,提出了最后期限IO調(diào)度程序,既要盡量提高全局吞吐量,又要使請(qǐng)求得到公平處理。在 最后期限IO調(diào)度程序中,每個(gè)請(qǐng)求都有一個(gè)超時(shí)時(shí)間。默認(rèn)情況下,讀請(qǐng)求的超時(shí)時(shí)間是500ms,寫請(qǐng)求的超時(shí)時(shí)間是5s。最后期限IO調(diào)度請(qǐng)求類似與 linux電梯,也以磁盤物理位置為次序維護(hù)請(qǐng)求隊(duì)列,這個(gè)隊(duì)列被稱為排序隊(duì)列。當(dāng)一個(gè)新請(qǐng)求遞交給排序隊(duì)列時(shí),最后期限IO調(diào)度程序類似于linux電 梯,合并和插入請(qǐng)求,但是最后期限IO調(diào)度程序同時(shí)也會(huì)以請(qǐng)求類型為依據(jù)將它們插入到額外隊(duì)列中。讀請(qǐng)求按次序被插入到特定的讀FIFO隊(duì)列中,寫請(qǐng)求被 插入到特定的寫FIFO隊(duì)列中。雖然普通隊(duì)列以磁盤扇區(qū)為序進(jìn)行排序,但是這些隊(duì)列是以FIFO形式組織的,結(jié)果新隊(duì)列總是被加入到隊(duì)列尾部。對(duì)于普通操 作來說,最后期限IO調(diào)度將請(qǐng)求從排序隊(duì)列的頭部去下,再推入到派發(fā)隊(duì)列中,派發(fā)隊(duì)列然后將請(qǐng)求提交給磁盤驅(qū)動(dòng),從而保證了最小化的請(qǐng)求尋址。如果在寫 FIFO隊(duì)列頭,或是在讀FIFO隊(duì)列頭的請(qǐng)求超時(shí),那么最后期限IO調(diào)度程序便從FIFO隊(duì)列中提取請(qǐng)求進(jìn)行服務(wù)。依靠這種方法,最后期限IO調(diào)度程序 試圖保證不會(huì)發(fā)生有請(qǐng)求在明顯超期的情況下仍不能得到服務(wù)的現(xiàn)象,如下圖所示: 最后期限IO調(diào)度程序的實(shí)現(xiàn)在文件driver/block/deadline-iosched.c中。 雖然最后期限IO調(diào)度程序?yàn)榻档妥x操作響應(yīng)時(shí)間做了許多工作,但同時(shí)也降低了系統(tǒng)吞吐量。考慮這樣的情況,假設(shè)一個(gè)系統(tǒng)正處于很繁重的寫操作期間,每次提 交新請(qǐng)求,IO調(diào)度程序都會(huì)迅速處理讀請(qǐng)求,這樣磁盤會(huì)首先為讀操作尋址,執(zhí)行讀操作,然后返回再尋址進(jìn)行寫操作,并且對(duì)每個(gè)讀操作都重復(fù)這個(gè)過程。這種 做法明顯損害了系統(tǒng)全局吞吐量。這事就有了預(yù)測(cè)IO調(diào)度程序。它的基礎(chǔ)就是最后期限IO調(diào)度程序。最主要的改進(jìn)是它增加了預(yù)測(cè)啟發(fā)能力。它的不同之處在于 讀操作提交后并不直接返回處理其他請(qǐng)求,而是會(huì)有意空閑片刻。這空閑的幾秒鐘,對(duì)應(yīng)用程序來說是個(gè)提交其他讀請(qǐng)求的好機(jī)會(huì)-----任何對(duì)相鄰磁盤位置操 作的請(qǐng)求都會(huì)立刻得到處理。在等待時(shí)間結(jié)束后,預(yù)測(cè)IO調(diào)度程序重新返回原來的位置,繼續(xù)執(zhí)行以前剩下的請(qǐng)求。要注意,如果等待可以減少讀請(qǐng)求所帶來的向 后再向前(back-and-forth)尋址操作,那么完全值得花一些時(shí)間來等待更多的請(qǐng)求(這里的時(shí)間花在對(duì)更多請(qǐng)求的預(yù)測(cè)上),如果一個(gè)相鄰的IO 請(qǐng)求在等待期帶來,那么IO調(diào)度程序可以節(jié)省兩次尋址操作。如果存在愈來愈多的訪問同樣區(qū)域的讀請(qǐng)求到來,那么片刻等待無疑會(huì)避免大量的尋址操作。當(dāng)然, 不得不說,如果沒有IO請(qǐng)求在等待期到來,那么預(yù)測(cè)IO調(diào)度程序會(huì)給系統(tǒng)性能帶來輕微的損失,浪費(fèi)掉幾毫秒。預(yù)測(cè)調(diào)度程序所帶來的優(yōu)勢(shì)在于能否正確預(yù)測(cè)應(yīng) 用程序和文件系統(tǒng)的行為。這種預(yù)測(cè)依靠一系列的啟發(fā)和統(tǒng)計(jì)工作。預(yù)測(cè)IO調(diào)度程序的實(shí)現(xiàn)在文件driver/block/as-iosched.c中。塊 設(shè)備使用哪個(gè)IO調(diào)度程序是可以選擇的。默認(rèn)的IO調(diào)度程序就是預(yù)測(cè)IO調(diào)度程序。 |
|