塊設(shè)備傳輸數(shù)據(jù)的固定長度為一個sector,因此,輸入、輸出調(diào)度器和塊設(shè)備的驅(qū)動必須管理扇區(qū)的數(shù)據(jù)。
虛擬文件系統(tǒng)->磁盤設(shè)備的中間映射層,它使用一種邏輯單元來管理磁盤數(shù)據(jù),這種邏輯單元被稱之為“Block”,一個塊相當(dāng)于一個文件系統(tǒng)里面的最小磁盤存儲單元。
塊設(shè)備驅(qū)動能夠拷貝一個segment的數(shù)據(jù):每個段是一個內(nèi)存頁或者一個內(nèi)存頁的一個部分包含了磁盤上的在物理上相鄰的block【即segment由一個或多個block組成,而內(nèi)存page由一個或多個磁盤上相鄰的segment組成】;
【注意】這里的segment只是處理數(shù)據(jù)的單位,并不是操作系統(tǒng)中段頁式管理內(nèi)存中的那個“段的概念”
磁盤緩沖工作于磁盤數(shù)據(jù)的“頁page”上,每一頁適合于一頁的框架內(nèi)。
因為Generic block層連接上層和下層的所有組件,因此在這一層里定義了sectors blocks segments 和數(shù)據(jù)頁
sector: 扇區(qū) block:塊 segment:段 page:頁
sector:硬件(磁盤)上的最小的操作單位,是操作系統(tǒng)和塊設(shè)備(硬件、磁盤)之間傳送數(shù)據(jù)的單位
block由一個或多個sector組成,是軟件(OS、文件系統(tǒng))中最小的操作單位;操作系統(tǒng)的虛擬文件系統(tǒng)從硬件設(shè)備上讀取一個block,實際為從硬件設(shè)備讀取一個或多個sector.對于文件管理來說,每個文件對應(yīng)的多個block可能是不連續(xù)的;block最終要映射到sector上,所以block的大小一般是sector的整數(shù)倍。不同的文件系統(tǒng)block可使用不同的大小,操作系統(tǒng)會在內(nèi)存中開辟內(nèi)存,存放block到所謂的block buffer中。
segment由磁盤上的在物理上相鄰的一個或多個block組成(從硬盤中將block獨到內(nèi)存中)
在linux內(nèi)核中,內(nèi)存以4KB/頁 進(jìn)行管理
block_device_operations:
與字符設(shè)備的file_operations類似,塊設(shè)備有一個block_device_operations結(jié)構(gòu)體;該block_device_operations結(jié)構(gòu)體主要綁定open、close、iocrl、compat_ioctl、media_changged、revalidate_disk、getgeo等函數(shù)。
gendisk:
在linux內(nèi)核中,使用gendisk結(jié)構(gòu)體來表示一個獨立的磁盤設(shè)備或者一個分區(qū)。
同一個磁盤的各個分區(qū)共享一個主設(shè)備號,而次設(shè)備號則不同。
在OS中,gendisk屬于通用塊設(shè)備層,格式化在該層進(jìn)行處理。
gendisk的struct disk_part_tbl part_tbl成員指向分區(qū)表。
gendisk的struct disk_part_tbl part_tbl成員指向分區(qū)表。
gendisk的struct hd_struct part0成員表示一個分區(qū)。
disk->part_tbl->part[0] = &disk->part0;
針對該結(jié)構(gòu)體的相關(guān)函數(shù)有:
block_device:
類似于字符設(shè)備的描述結(jié)構(gòu)體cdev,塊設(shè)備(或它的分區(qū))也有一個塊設(shè)備描述結(jié)構(gòu)體【類似于gendisk既可以描述磁盤設(shè)備也可以描述分區(qū)】,但是不像字符設(shè)備驅(qū)動需要注冊cdev,塊設(shè)置直接注冊gendisk即可。
struct block_device {
};
下面的圖片有一個磁盤disk,磁盤有個一分區(qū)partition; 磁盤的塊設(shè)備描述符block_device 的bd_contains成員指向自己,而
分區(qū)partition的bd_contains成員指向該分區(qū)所屬的磁盤設(shè)備即disk,以表示一種從屬的關(guān)系。
通過add_disk(struct gendisk *)就可以注冊一個設(shè)備【并不是注冊一個分區(qū)】,就一個設(shè)備與gendisk綁定;而在linux內(nèi)部分區(qū)時也用gendisk表示分區(qū),我們不用管。
圖中g(shù)endisk中的part是gendisk結(jié)構(gòu)體中的part_table成員,指向設(shè)備的分區(qū)表;在塊設(shè)備中用hd_struct結(jié)構(gòu)體表示一個分區(qū),所以圖中表示有四個分區(qū),其中partition分區(qū)的描述結(jié)構(gòu)體block_device中的bd_part指向第二分區(qū)處。
通過圖片分析知在程序中如果
request:
在linux內(nèi)核的塊設(shè)備中,使用request結(jié)構(gòu)體來表征等待進(jìn)行的IO請求。
request_queue:
表示IO請求隊列的結(jié)構(gòu)體,一個塊請求隊列是一個塊IO請求隊列。
Disk Caches,磁盤高速緩存。
在linux中,程序?qū)τ脖P設(shè)備的寫操作實際上是寫到硬盤在內(nèi)存中的高速緩存,內(nèi)黑中有pdflush守護(hù)進(jìn)程,什么時候高速緩存中的數(shù)據(jù)寫入到硬盤中由pdflush決定,可通過sync() fsync()進(jìn)行強制更新。
將磁盤上的數(shù)據(jù)緩存在內(nèi)存中,加速文件的讀寫。實際上,在一般情況下,read/write是只跟緩存打交道的。(當(dāng)然,存在特殊情況。下面會說到。)
read就直接從緩存讀數(shù)據(jù)。如果要讀的數(shù)據(jù)還不在緩存中,則觸發(fā)一次讀盤操作,然后等待磁盤上的數(shù)據(jù)被更新到磁盤高速緩存中;write也是直接寫到緩存里去,然后就不用管了。后續(xù)內(nèi)核會負(fù)責(zé)將數(shù)據(jù)寫回磁盤。
BIO :塊IO
read就直接從緩存讀數(shù)據(jù)。如果要讀的數(shù)據(jù)還不在緩存中,則觸發(fā)一次讀盤操作,然后等待磁盤上的數(shù)據(jù)被更新到磁盤高速緩存中;write也是直接寫到緩存里去,然后就不用管了。后續(xù)內(nèi)核會負(fù)責(zé)將數(shù)據(jù)寫回磁盤。
BIO :塊IO
通常一個bio對應(yīng)一個 上層傳遞給塊層的I/O請求,I/O請求算法可將連續(xù)的bio合并成一個request。request是bio經(jīng)由塊層進(jìn)行調(diào)整后的結(jié)果。
上圖中request的成員q指向自己所屬的等待隊列,request通過quelist成員組成request鏈表;
request的bio與bio_tail指向request包含的bio。
在IO調(diào)度器中,上層提交的bio被構(gòu)造成request結(jié)構(gòu),一個request結(jié)構(gòu)包含了一組順序的bio。而每個物理設(shè)備會對應(yīng)一個request_queue,里面順序存放著相關(guān)的request。
新的bio可能被合并到request_queue中已有的request結(jié)構(gòu)中(甚至合并到已有的bio中),也可能生成新的request結(jié)構(gòu)并插入到request_queue的適當(dāng)位置上。具體怎么合并、怎么插入,取決于設(shè)備驅(qū)動程序選擇的IO調(diào)度算法。大體上可以把IO調(diào)度算法就想象成“電梯算法”,盡管實際的IO調(diào)度算法有所改進(jìn)。
新的bio可能被合并到request_queue中已有的request結(jié)構(gòu)中(甚至合并到已有的bio中),也可能生成新的request結(jié)構(gòu)并插入到request_queue的適當(dāng)位置上。具體怎么合并、怎么插入,取決于設(shè)備驅(qū)動程序選擇的IO調(diào)度算法。大體上可以把IO調(diào)度算法就想象成“電梯算法”,盡管實際的IO調(diào)度算法有所改進(jìn)。

bio結(jié)構(gòu)中通過bi_next組成bio鏈表。bio的核心是一個稱為bi_io_vec的數(shù)組,它由bio_vec結(jié)構(gòu)體組成。
bio結(jié)構(gòu)體中的成員bv_page是一個內(nèi)存的頁指針。

bio通過IO調(diào)度算法這一層層的排序、合并等,生產(chǎn)IO請求,IO請求存放到IO請求隊列中去。
為了實現(xiàn)這樣的緩存,每個文件的inode內(nèi)嵌了一個address_space結(jié)構(gòu),通過inode->i_mapping來訪問。address_space結(jié)構(gòu)中維護(hù)了一棵radix樹,用于磁盤高速緩存的內(nèi)存頁面就掛在這棵樹上。而既然磁盤高速緩存是跟文件的inode關(guān)聯(lián)上的,則打開這個文件的每個進(jìn)程都共用同一份緩存。
radix樹的具體實現(xiàn)細(xì)節(jié)這里可以不用關(guān)心,可以把它理解成一個數(shù)組。數(shù)組中的每個元素就是一個頁面,文件的內(nèi)容就順序存放在這些頁面中。
于是,通過要讀寫的文件pos,可以換算得到要讀寫的是第幾頁(pos是以字節(jié)為單位,只需要除以每個頁的字節(jié)數(shù)即可)。
inode被載入內(nèi)存的時候,對應(yīng)的磁盤高速緩存是空的(radix樹上沒有頁面)。隨著文件的讀寫,磁盤上的數(shù)據(jù)被載入內(nèi)存,相應(yīng)的內(nèi)存頁被掛到radix樹的相應(yīng)位置上。
如果文件被寫,則僅僅是對應(yīng)inode的radix樹上的對應(yīng)頁上的內(nèi)容被更新,并不會直接寫回磁盤。這樣被寫過,但還沒有更新到磁盤的頁稱為臟頁。
內(nèi)核線程pdflush定期將每個inode上的臟頁更新到磁盤,也會適時地將radix上的頁面回收,這些內(nèi)容都不在這里深入探討了。
當(dāng)需要讀寫的文件內(nèi)容尚未載入到對應(yīng)的radix樹時,read/write的執(zhí)行過程會向底層的“通用塊層”發(fā)起讀請求,以便將數(shù)據(jù)讀入。
而如果文件打開時指定了O_DIRECT選項,則表示繞開磁盤高速緩存,直接與“通用塊層”打交道。
既然磁盤高速緩存提供了有利于提高讀寫效率的緩存機(jī)制,為什么又要使用O_DIRECT選項來繞開它呢?一般情況下,這樣做的應(yīng)用程序會自己在用戶態(tài)維護(hù)一套更利于應(yīng)用程序使用的專用的緩存機(jī)制,用以取代內(nèi)核提供的磁盤高速緩存這種通用的緩存機(jī)制。(數(shù)據(jù)庫程序通常就會這么干。)
既然使用O_DIRECT選項后,文件的緩存從內(nèi)核提供的磁盤高速緩存變成了用戶態(tài)的緩存,那么打開同一文件的不同進(jìn)程將無法共享這些緩存(除非這些進(jìn)程再創(chuàng)建一個共享內(nèi)存什么的)。而如果對于同一個文件,某些進(jìn)程使用了O_DIRECT選項,而某些又沒有呢?沒有使用O_DIRECT選項的進(jìn)程讀寫這個文件時,會在磁盤高速緩存中留下相應(yīng)的內(nèi)容;而使用了O_DIRECT選項的進(jìn)程讀寫這個文件時,需要先將磁盤高速緩存里面對應(yīng)本次讀寫的臟數(shù)據(jù)寫回磁盤,然后再對磁盤進(jìn)行直接讀寫。
關(guān)于O_DIRECT選項帶來的direct_IO的具體實現(xiàn)細(xì)節(jié),說來話長,在這里就不做介紹了。
3、Generic Block Layer,通用塊層。
linux內(nèi)核為塊設(shè)備抽象了統(tǒng)一的模型,把塊設(shè)備看作是由若干個扇區(qū)組成的數(shù)組空間。扇區(qū)是磁盤設(shè)備讀寫的最小單位,通過扇區(qū)號可以指定要訪問的磁盤扇區(qū)。
上層的讀寫請求在通用塊層被構(gòu)造成一個或多個bio結(jié)構(gòu),這個結(jié)構(gòu)里面描述了一次請求--訪問的起始扇區(qū)號?訪問多少個扇區(qū)?是讀還是寫?相應(yīng)的內(nèi)存頁有哪些、頁偏移和數(shù)據(jù)長度是多少?等等……
這里面主要有兩個問題:要訪問的扇區(qū)號從哪里來?內(nèi)存是怎么組織的?
前面說過,上層的讀寫請求通過文件pos可以定位到要訪問的是相應(yīng)的磁盤高速緩存的第幾個頁,而通過這個頁index就可以知道要訪問的是文件的第幾個扇區(qū),得到扇區(qū)的index。
但是,文件的第幾個扇區(qū)并不等同于磁盤上的第幾個扇區(qū),得到的扇區(qū)index還需要由特定文件系統(tǒng)提供的函數(shù)來轉(zhuǎn)換成磁盤的扇區(qū)號。文件系統(tǒng)會記載當(dāng)前磁盤上的扇區(qū)使用情況,并且對于每一個inode,它依次使用了哪些扇區(qū)。
于是,通過文件系統(tǒng)提供的特定函數(shù),上層請求的文件pos最終被對應(yīng)到了磁盤上的扇區(qū)號。
可見,上層的一次請求可能跨多個扇區(qū),可能形成多個非連續(xù)的扇區(qū)段。對應(yīng)于每個扇區(qū)段,一個bio結(jié)構(gòu)被構(gòu)造出來。而由于塊設(shè)備一般都支持一次性訪問若干個連續(xù)的扇區(qū),所以一個扇區(qū)段(不止一個扇區(qū))可以包含在代表一次塊設(shè)備IO請求的一個bio結(jié)構(gòu)中。
接下來談?wù)剝?nèi)存的組織。既然上層的一次讀寫請求可能跨多個扇區(qū),它也可能跨越磁盤高速緩存上的多個頁。于是,一個bio里面包含的扇區(qū)請求可能會對應(yīng)一組內(nèi)存頁。而這些頁是單獨分配的,內(nèi)存地址很可能不連續(xù)。
那么,既然bio描述的是一次塊設(shè)備請求,塊設(shè)備能夠一次性訪問一組連續(xù)的扇區(qū),但是能夠一次性對一組非連續(xù)的內(nèi)存地址進(jìn)行存取嗎?
塊設(shè)備一般是通過DMA,將塊設(shè)備上一組連續(xù)的扇區(qū)上的數(shù)據(jù)拷貝到一組連續(xù)的內(nèi)存頁面上(或?qū)⒁唤M連續(xù)的內(nèi)存頁面上的數(shù)據(jù)拷貝到塊設(shè)備上一組連續(xù)的扇區(qū)),DMA本身一般是不支持一次性訪問非連續(xù)的內(nèi)存頁面的。
但是某些體系結(jié)構(gòu)包含了io-mmu。就像通過mmu可以將一組非連續(xù)的物理頁面映射成連續(xù)的虛擬地址一樣,對io-mmu進(jìn)行編程,可以讓DMA將一組非連續(xù)的物理內(nèi)存看作連續(xù)的。所以,即使一個bio包含了非連續(xù)的多段內(nèi)存,它也是有可能可以在一次DMA中完成的。當(dāng)然,不是所有的體系結(jié)構(gòu)都支持io-mmu,所以一個bio也可能在后面的設(shè)備驅(qū)動程序中被拆分成多個設(shè)備請求。
每個被構(gòu)造的bio結(jié)構(gòu)都會分別被提交,提交到底層的IO調(diào)度器中。
4、I/O Scheduler
我們知道,磁盤是通過磁頭來讀寫數(shù)據(jù)的,磁頭在定位扇區(qū)的過程中需要做機(jī)械的移動。相比于電和磁的傳遞,機(jī)械運動是非常慢速的,這也就是磁盤為什么那么慢的主要原因。
IO調(diào)度器要做的事情就是在完成現(xiàn)有請求的前提下,讓磁頭盡可能少移動,從而提高磁盤的讀寫效率。最有名的就是“電梯算法”。
在IO調(diào)度器中,上層提交的bio被構(gòu)造成request結(jié)構(gòu),一個request結(jié)構(gòu)包含了一組順序的bio。而每個物理設(shè)備會對應(yīng)一個request_queue,里面順序存放著相關(guān)的request。
新的bio可能被合并到request_queue中已有的request結(jié)構(gòu)中(甚至合并到已有的bio中),也可能生成新的request結(jié)構(gòu)并插入到request_queue的適當(dāng)位置上。具體怎么合并、怎么插入,取決于設(shè)備驅(qū)動程序選擇的IO調(diào)度算法。大體上可以把IO調(diào)度算法就想象成“電梯算法”,盡管實際的IO調(diào)度算法有所改進(jìn)。
除了類似“電梯算法”的IO調(diào)度算法,還有“none”算法,這實際上是沒有算法,也可以說是“先來先服務(wù)算法”。因為現(xiàn)在很多塊設(shè)備已經(jīng)能夠很好地支持隨機(jī)訪問了(比如固態(tài)磁盤、flash閃存),使用“電梯算法”對于它們沒有什么意義。
IO調(diào)度器除了改變請求的順序,還可能延遲觸發(fā)對請求的處理。因為只有當(dāng)請求隊列有一定數(shù)目的請求時,“電梯算法”才能發(fā)揮其功效,否則極端情況下它將退化成“先來先服務(wù)算法”。
這是通過對request_queue的plug/unplug來實現(xiàn)的,plug相當(dāng)于停用,unplug相當(dāng)于恢復(fù)。請求少時將request_queue停用,當(dāng)請求達(dá)到一定數(shù)目,或者request_queue里最“老”的請求已經(jīng)等待很長一段時間了,這時候才將request_queue恢復(fù)。
在request_queue恢復(fù)的時候,驅(qū)動程序提供的回調(diào)函數(shù)將被調(diào)用,于是驅(qū)動程序開始處理request_queue。
一般來說,read/write系統(tǒng)調(diào)用到這里就返回了。返回之后可能等待(同步)或是繼續(xù)干其他事(異步)。而返回之前會在任務(wù)隊列里面添加一個任務(wù),而處理該任務(wù)隊列的內(nèi)核線程將來會執(zhí)行request_queue的unplug操作,以觸發(fā)驅(qū)動程序處理請求。
5、Device Driver,設(shè)備驅(qū)動程序。
到了這里,設(shè)備驅(qū)動程序要做的事情就是從request_queue里面取出請求,然后操作硬件設(shè)備,逐個去執(zhí)行這些請求。
除了處理請求,設(shè)備驅(qū)動程序還要選擇IO調(diào)度算法,因為設(shè)備驅(qū)動程序最知道設(shè)備的屬性,知道用什么樣的IO調(diào)度算法最合適。甚至于,設(shè)備驅(qū)動程序可以將IO調(diào)度器屏蔽掉,而直接對上層的bio進(jìn)行處理。(當(dāng)然,設(shè)備驅(qū)動程序也可實現(xiàn)自己的IO調(diào)度算法。)
可以說,IO調(diào)度器是內(nèi)核提供給設(shè)備驅(qū)動程序的一組方法。用與不用、使用怎樣的方法,選擇權(quán)在于設(shè)備驅(qū)動程序。
于是,對于支持隨機(jī)訪問的塊設(shè)備,驅(qū)動程序除了選擇“none”算法,還有一種更直接的做法,就是注冊自己的bio提交函數(shù)。這樣,bio生成后,并不會使用通用的提交函數(shù),被提交到IO調(diào)度器,而是直接被驅(qū)動程序處理。
但是,如果設(shè)備比較慢的話,bio的提交可能會阻塞較長時間。所以這種做法一般被基于內(nèi)存的“塊設(shè)備”驅(qū)動使用(當(dāng)然,這樣的塊設(shè)備是由驅(qū)動程序虛擬的)。
下面大致介紹一下read/write的執(zhí)行流程:
sys_read。通過fd得到對應(yīng)的file結(jié)構(gòu),然后調(diào)用vfs_read;
vfs_read。各種權(quán)限及文件鎖的檢查,然后調(diào)用file->f_op->read(若不存在則調(diào)用do_sync_read)。file->f_op是從對應(yīng)的inode->i_fop而來,而inode->i_fop是由對應(yīng)的文件系統(tǒng)類型在生成這個inode時賦予的。file->f_op->read很可能就等同于do_sync_read;
do_sync_read。f_op->read是完成一次同步讀,而f_op->aio_read完成一次異步讀。do_sync_read則是利用f_op->aio_read這個異步讀操作來完成同步讀,也就是在發(fā)起一次異步讀之后,如果返回值是-EIOCBQUEUED,則進(jìn)程睡眠,直到讀完成即可。但實際上對于磁盤文件的讀,f_op->aio_read一般不會返回-EIOCBQUEUED,除非是設(shè)置了O_DIRECT標(biāo)志aio_read,或者是對于一些特殊的文件系統(tǒng)(如nfs這樣的網(wǎng)絡(luò)文件系統(tǒng));
f_op->aio_read。這個函數(shù)通常是由generic_file_aio_read或者其封裝來實現(xiàn)的;
generic_file_aio_read。一次異步讀可能包含多個讀操作(對應(yīng)于readv系統(tǒng)調(diào)用),對于其中的每一個,調(diào)用do_generic_file_read;
do_generic_file_read。主要流程是在radix樹里面查找是否存在對應(yīng)的page,且該頁可用。是則從page里面讀出所需的數(shù)據(jù),然后返回,否則通過file->f_mapping->a_ops->readpage去讀這個頁;
file->f_mapping是從對應(yīng)inode->i_mapping而來,inode->i_mapping->a_ops是由對應(yīng)的文件系統(tǒng)類型在生成這個inode時賦予的。而各個文件系統(tǒng)類型提供的a_ops->readpage函數(shù)一般是mpage_readpage函數(shù)的封裝;
mpage_readpage。調(diào)用do_mpage_readpage構(gòu)造一個bio,再調(diào)用mpage_bio_submit將其提交;
do_mpage_readpage。根據(jù)page->index確定需要讀的磁盤扇區(qū)號,然后構(gòu)造一組bio。其中需要使用文件系統(tǒng)類型提供的get_block函數(shù)來對應(yīng)需要讀取的磁盤扇區(qū)號;
mpage_bio_submit。設(shè)置bio的結(jié)束回調(diào)bio->bi_end_io為mpage_end_io_read,然后調(diào)用submit_bio提交這組bio;
submit_bio。調(diào)用generic_make_request將bio提交到磁盤驅(qū)動維護(hù)的請求隊列中;
generic_make_request。一個包裝函數(shù),對于每一個bio,調(diào)用__generic_make_request;
__generic_make_request。獲取bio對應(yīng)的塊設(shè)備文件對應(yīng)的磁盤對象的請求隊列bio->bi_bdev->bd_disk->queue,調(diào)用q->make_request_fn將bio添加到隊列;
q->make_request_fn。設(shè)備驅(qū)動程序在其初始化時會初始化這個request_queue結(jié)構(gòu),并且設(shè)置q->make_request_fn和q->request_fn(這個下面就會用到)。前者用于將一個bio組裝成request添加到request_queue,后者用于處理request_queue中的請求。一般情況下,設(shè)備驅(qū)動通過調(diào)用blk_init_queue來初始化request_queue,q->request_fn需要給定,而q->make_request_fn使用了默認(rèn)的__make_request;
__make_request。會根據(jù)不同的調(diào)度算法來決定如何添加bio,生成對應(yīng)的request結(jié)構(gòu)加入request_queue結(jié)構(gòu)中,并且決定是否調(diào)用q->request_fn,或是在kblockd_workqueue任務(wù)隊列里面添加一個任務(wù),等kblockd內(nèi)核線程來調(diào)用q->request_fn;
q->request_fn。由驅(qū)動程序定義的函數(shù),負(fù)責(zé)從request_queue里面取出request進(jìn)行處理。從添加bio到request被取出,若干的請求已經(jīng)被IO調(diào)度算法整理過了。驅(qū)動程序負(fù)責(zé)根據(jù)request結(jié)構(gòu)里面的描述,將實際物理設(shè)備里面的數(shù)據(jù)讀到內(nèi)存中。當(dāng)驅(qū)動程序完成一個request時,會調(diào)用end_request(或類似)函數(shù),以結(jié)束這個request;
end_request。完成request的收尾工作,并且會調(diào)用對應(yīng)的bio的的結(jié)束方法bio->bi_end_io,即前面設(shè)置的mpage_end_io_read;
mpage_end_io_read。如果page已更新則設(shè)置其up-to-date標(biāo)記,并為page解鎖,喚醒等待page解鎖的進(jìn)程。最后釋放bio對象;
sys_write。跟sys_read一樣,對應(yīng)的vfs_write、do_sync_write、f_op->aio_write、generic_file_aio_write被順序調(diào)用;
generic_file_aio_write。調(diào)用__generic_file_aio_write_nolock來進(jìn)行寫的處理,將數(shù)據(jù)寫到磁盤高速緩存中。寫完成之后,判斷如果文件打開時使用了O_SYNC標(biāo)記,則再調(diào)用sync_page_range將寫入到磁盤高速緩存中的數(shù)據(jù)同步到磁盤(只同步文件頭信息);
__generic_file_aio_write_nolock。進(jìn)行一些檢查之后,調(diào)用generic_file_buffered_write;
generic_file_buffered_write。調(diào)用generic_perform_write執(zhí)行寫,寫完成之后,判斷如果文件打開時使用了O_SYNC標(biāo)記,則再調(diào)用generic_osync_inode將寫入到磁盤高速緩存中的數(shù)據(jù)同步到磁盤(同步文件頭信息和文件內(nèi)容);
generic_perform_write。一次異步寫可能包含多個寫操作(對應(yīng)于writev系統(tǒng)調(diào)用),對于其中牽涉的每一個page,調(diào)用file->f_mapping->a_ops->write_begin準(zhǔn)備好需要寫的磁盤高速緩存頁面,然后將需要寫的數(shù)據(jù)拷入其中,最后調(diào)用file->f_mapping->a_ops->write_end完成寫;
file->f_mapping是從對應(yīng)inode->i_mapping而來,inode->i_mapping->a_ops是由對應(yīng)的文件系統(tǒng)類型在生成這個inode時賦予的。而各個文件系統(tǒng)類型提供的file->f_mapping->a_ops->write_begin函數(shù)一般是block_write_begin函數(shù)的封裝、file->f_mapping->a_ops->write_end函數(shù)一般是generic_write_end函數(shù)的封裝;
block_write_begin。調(diào)用grab_cache_page_write_begin在radix樹里面查找要被寫的page,如果不存在則創(chuàng)建一個。調(diào)用__block_prepare_write為這個page準(zhǔn)備一組buffer_head結(jié)構(gòu),用于描述組成這個page的數(shù)據(jù)塊(利用其中的信息,可以生成對應(yīng)的bio結(jié)構(gòu));
generic_write_end。調(diào)用block_write_end提交寫請求,然后設(shè)置page的dirty標(biāo)記;
block_write_end。調(diào)用__block_commit_write為page中的每一個buffer_head結(jié)構(gòu)設(shè)置dirty標(biāo)記;
至此,write調(diào)用就要返回了。如果文件打開時使用了O_SYNC標(biāo)記,sync_page_range或generic_osync_inode將被調(diào)用。否則write就結(jié)束了,等待pdflush內(nèi)核線程發(fā)現(xiàn)radix樹上的臟頁,并最終調(diào)用到do_writepages寫回這些臟頁;
sync_page_range也是調(diào)用generic_osync_inode來實現(xiàn)的,而generic_osync_inode最終也會調(diào)用到do_writepages;
do_writepages。調(diào)用inode->i_mapping->a_ops->writepages,而后者一般是mpage_writepages函數(shù)的包裝;
mpage_writepages。檢查radix樹中需要寫回的page,對每一個page調(diào)用__mpage_writepage;
__mpage_writepage。這里也是構(gòu)造bio,然后調(diào)用mpage_bio_submit來進(jìn)行提交;
后面的流程跟read幾乎就一樣了……