轉自http://hi.baidu.com/_kouu/item/4e9db87580328244ef1e53d0html
在《linux內核虛擬文件系統淺析》這篇文章中,咱們看到文件是如何被打開、文件的讀寫是如何被觸發的。
對一個已打開的文件fd進行read/write系統調用時,內核中該文件所對應的file結構的f_op->read/f_op->write被調用。
本文將順着這條路走下去,大體看看普通磁盤文件的讀寫是怎樣實現的。
linux內核響應一個塊設備文件讀寫的層次結構如圖(摘自ULK3):
一、VFS,虛擬文件系統。
以前咱們已經看到f_op->read/f_op->write如何被調用,這就是VFS乾的事(參見:《linux內核虛擬文件系統淺析》);
二、Disk Caches,磁盤高速緩存。
將磁盤上的數據緩存在內存中,加速文件的讀寫。實際上,在通常狀況下,read/write是隻跟緩存打交道的。(固然,存在特殊狀況。下面會說到。)
read就直接從緩存讀數據。若是要讀的數據還不在緩存中,則觸發一次讀盤操做,而後等待磁盤上的數據被更新到磁盤高速緩存中;write也是直接寫到緩存裏去,而後就不用管了。後續內核會負責將數據寫回磁盤。
爲了實現這樣的緩存,每一個文件的inode內嵌了一個address_space結構,經過inode->i_mapping來訪問。address_space結構中維護了一棵radix樹,用於磁盤高速緩存的內存頁面就掛在這棵樹上。而既然磁盤高速緩存是跟文件的inode關聯上的,則打開這個文件的每一個進程都共用同一份緩存。
radix樹的具體實現細節這裏能夠不用關心,能夠把它理解成一個數組。數組中的每一個元素就是一個頁面,文件的內容就順序存放在這些頁面中。
因而,經過要讀寫的文件pos,能夠換算獲得要讀寫的是第幾頁(pos是以字節爲單位,只須要除以每一個頁的字節數便可)。
inode被載入內存的時候,對應的磁盤高速緩存是空的(radix樹上沒有頁面)。隨着文件的讀寫,磁盤上的數據被載入內存,相應的內存頁被掛到radix樹的相應位置上。
若是文件被寫,則僅僅是對應inode的radix樹上的對應頁上的內容被更新,並不會直接寫回磁盤。這樣被寫過,但尚未更新到磁盤的頁稱爲髒頁。
內核線程pdflush按期將每一個inode上的髒頁更新到磁盤,也會適時地將radix上的頁面回收,這些內容都不在這裏深刻探討了(能夠參考《linux頁面回收淺析》)。
當須要讀寫的文件內容還沒有載入到對應的radix樹時,read/write的執行過程會向底層的「通用塊層」發起讀請求,以便將數據讀入。
而若是文件打開時指定了O_DIRECT選項,則表示繞開磁盤高速緩存,直接與「通用塊層」打交道。
既然磁盤高速緩存提供了有利於提升讀寫效率的緩存機制,爲何又要使用O_DIRECT選項來繞開它呢?通常狀況下,這樣作的應用程序會本身在用戶態維護一套更利於應用程序使用的專用的緩存機制,用以取代內核提供的磁盤高速緩存這種通用的緩存機制。(數據庫程序一般就會這麼幹。)
既然使用O_DIRECT選項後,文件的緩存從內核提供的磁盤高速緩存變成了用戶態的緩存,那麼打開同一文件的不一樣進程將沒法共享這些緩存(除非這些進程再建立一個共享內存什麼的)。而若是對於同一個文件,某些進程使用了O_DIRECT選項,而某些又沒有呢?沒有使用O_DIRECT選項的進程讀寫這個文件時,會在磁盤高速緩存中留下相應的內容;而使用了O_DIRECT選項的進程讀寫這個文件時,須要先將磁盤高速緩存裏面對應本次讀寫的髒數據寫回磁盤,而後再對磁盤進行直接讀寫。
關於O_DIRECT選項帶來的direct_IO的具體實現細節,說來話長,在這裏就不作介紹了。能夠參考《linux異步IO淺析》。
三、Generic Block Layer,通用塊層。
linux內核爲塊設備抽象了統一的模型,把塊設備看做是由若干個扇區組成的數組空間。扇區是磁盤設備讀寫的最小單位,經過扇區號能夠指定要訪問的磁盤扇區。
上層的讀寫請求在通用塊層被構形成一個或多個bio結構,這個結構裏面描述了一次請求--訪問的起始扇區號?訪問多少個扇區?是讀仍是寫?相應的內存頁有哪些、頁偏移和數據長度是多少?等等……
這裏面主要有兩個問題:要訪問的扇區號從哪裏來?內存是怎麼組織的?
前面說過,上層的讀寫請求經過文件pos能夠定位到要訪問的是相應的磁盤高速緩存的第幾個頁,而經過這個頁index就能夠知道要訪問的是文件的第幾個扇區,獲得扇區的index。
可是,文件的第幾個扇區並不等同於磁盤上的第幾個扇區,獲得的扇區index還須要由特定文件系統提供的函數來轉換成磁盤的扇區號。文件系統會記載當前磁盤上的扇區使用狀況,而且對於每個inode,它依次使用了哪些扇區。(參見《linux文件系統實現淺析》)
因而,經過文件系統提供的特定函數,上層請求的文件pos最終被對應到了磁盤上的扇區號。
可見,上層的一次請求可能跨多個扇區,可能造成多個非連續的扇區段。對應於每一個扇區段,一個bio結構被構造出來。而因爲塊設備通常都支持一次性訪問若干個連續的扇區,因此一個扇區段(不止一個扇區)能夠包含在表明一次塊設備IO請求的一個bio結構中。
接下來談談內存的組織。既然上層的一次讀寫請求可能跨多個扇區,它也可能跨越磁盤高速緩存上的多個頁。因而,一個bio裏面包含的扇區請求可能會對應一組內存頁。而這些頁是單獨分配的,內存地址極可能不連續。
那麼,既然bio描述的是一次塊設備請求,塊設備可以一次性訪問一組連續的扇區,可是可以一次性對一組非連續的內存地址進行存取嗎?
塊設備通常是經過DMA,將塊設備上一組連續的扇區上的數據拷貝到一組連續的內存頁面上(或將一組連續的內存頁面上的數據拷貝到塊設備上一組連續的扇區),DMA自己通常是不支持一次性訪問非連續的內存頁面的。
可是某些體系結構包含了io-mmu。就像經過mmu能夠將一組非連續的物理頁面映射成連續的虛擬地址同樣,對io-mmu進行編程,可讓DMA將一組非連續的物理內存看做連續的。因此,即便一個bio包含了非連續的多段內存,它也是有可能能夠在一次DMA中完成的。固然,不是全部的體系結構都支持io-mmu,因此一個bio也可能在後面的設備驅動程序中被拆分紅多個設備請求。
每一個被構造的bio結構都會分別被提交,提交到底層的IO調度器中。
四、I/O SchedulerLayer,IO調度器。
咱們知道,磁盤是經過磁頭來讀寫數據的,磁頭在定位扇區的過程當中須要作機械的移動。相比於電和磁的傳遞,機械運動是很是慢速的,這也就是磁盤爲何那麼慢的主要緣由。
IO調度器要作的事情就是在完成現有請求的前提下,讓磁頭儘量少移動,從而提升磁盤的讀寫效率。最有名的就是「電梯算法」。
在IO調度器中,上層提交的bio被構形成request結構,一個request結構包含了一組順序的bio。而每一個物理設備會對應一個request_queue,裏面順序存放着相關的request。
新的bio可能被合併到request_queue中已有的request結構中(甚至合併到已有的bio中),也可能生成新的request結構並插入到request_queue的適當位置上。具體怎麼合併、怎麼插入,取決於設備驅動程序選擇的IO調度算法。大致上能夠把IO調度算法就想象成「電梯算法」,儘管實際的IO調度算法有所改進。
除了相似「電梯算法」的IO調度算法,還有「none」算法,這其實是沒有算法,也能夠說是「先來先服務算法」。由於如今不少塊設備已經可以很好地支持隨機訪問了(好比固態磁盤、flash閃存),使用「電梯算法」對於它們沒有什麼意義。
IO調度器除了改變請求的順序,還可能延遲觸發對請求的處理。由於只有當請求隊列有必定數目的請求時,「電梯算法」才能發揮其功效,不然極端狀況下它將退化成「先來先服務算法」。
這是經過對request_queue的plug/unplug來實現的,plug至關於停用,unplug至關於恢復。請求少時將request_queue停用,當請求達到必定數目,或者request_queue裏最「老」的請求已經等待很長一段時間了,這時候纔將request_queue恢復。
在request_queue恢復的時候,驅動程序提供的回調函數將被調用,因而驅動程序開始處理request_queue。
通常來講,read/write系統調用到這裏就返回了。返回以後可能等待(同步)或是繼續幹其餘事(異步)。而返回以前會在任務隊列裏面添加一個任務,而處理該任務隊列的內核線程未來會執行request_queue的unplug操做,以觸發驅動程序處理請求。
五、Device Driver,設備驅動程序。
到了這裏,設備驅動程序要作的事情就是從request_queue裏面取出請求,而後操做硬件設備,逐個去執行這些請求。
除了處理請求,設備驅動程序還要選擇IO調度算法,由於設備驅動程序最知道設備的屬性,知道用什麼樣的IO調度算法最合適。甚至於,設備驅動程序能夠將IO調度器屏蔽掉,而直接對上層的bio進行處理。(固然,設備驅動程序也可實現本身的IO調度算法。)
能夠說,IO調度器是內核提供給設備驅動程序的一組方法。用與不用、使用怎樣的方法,選擇權在於設備驅動程序。
因而,對於支持隨機訪問的塊設備,驅動程序除了選擇「none」算法,還有一種更直接的作法,就是註冊本身的bio提交函數。這樣,bio生成後,並不會使用通用的提交函數,被提交到IO調度器,而是直接被驅動程序處理。
可是,若是設備比較慢的話,bio的提交可能會阻塞較長時間。因此這種作法通常被基於內存的「塊設備」驅動使用(固然,這樣的塊設備是由驅動程序虛擬的)。
下面大體介紹一下read/write的執行流程:
sys_read。經過fd獲得對應的file結構,而後調用vfs_read;
vfs_read。各類權限及文件鎖的檢查,而後調用file->f_op->read(若不存在則調用do_sync_read)。file->f_op是從對應的inode->i_fop而來,而inode->i_fop是由對應的文件系統類型在生成這個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這個異步讀操做來完成同步讀,也就是在發起一次異步讀以後,若是返回值是-EIOCBQUEUED,則進程睡眠,直到讀完成便可。但實際上對於磁盤文件的讀,f_op->aio_read通常不會返回-EIOCBQUEUED,除非是設置了O_DIRECT標誌aio_read,或者是對於一些特殊的文件系統(如nfs這樣的網絡文件系統);
f_op->aio_read。這個函數一般是由generic_file_aio_read或者其封裝來實現的;
generic_file_aio_read。一次異步讀可能包含多個讀操做(對應於readv系統調用),對於其中的每個,調用do_generic_file_read;
do_generic_file_read。主要流程是在radix樹裏面查找是否存在對應的page,且該頁可用。是則從page裏面讀出所需的數據,而後返回,不然經過file->f_mapping->a_ops->readpage去讀這個頁。(file->f_mapping->a_ops->readpage返回後,說明讀請求已經提交了。可是磁盤上的數據還不必定就已經讀上來了,須要等待數據讀完。等待的方法就是lock_page:在調用file->f_mapping->a_ops->readpage以前會給page置PG_locked標記。而數據讀完後,會將該標記清除,這個後面會看到。而這裏的lock_page就是要等待PG_locked標記被清除。);
file->f_mapping是從對應inode->i_mapping而來,inode->i_mapping->a_ops是由對應的文件系統類型在生成這個inode時賦予的。而各個文件系統類型提供的a_ops->readpage函數通常是mpage_readpage函數的封裝;
mpage_readpage。調用do_mpage_readpage構造一個bio,再調用mpage_bio_submit將其提交;
do_mpage_readpage。根據page->index肯定須要讀的磁盤扇區號,而後構造一組bio。其中須要使用文件系統類型提供的get_block函數來對應須要讀取的磁盤扇區號;
mpage_bio_submit。設置bio的結束回調bio->bi_end_io爲mpage_end_io_read,而後調用submit_bio提交這組bio;
submit_bio。調用generic_make_request將bio提交到磁盤驅動維護的請求隊列中;
generic_make_request。一個包裝函數,對於每個bio,調用__generic_make_request;
__generic_make_request。獲取bio對應的塊設備文件對應的磁盤對象的請求隊列bio->bi_bdev->bd_disk->queue,調用q->make_request_fn將bio添加到隊列;
q->make_request_fn。設備驅動程序在其初始化時會初始化這個request_queue結構,而且設置q->make_request_fn和q->request_fn(這個下面就會用到)。前者用於將一個bio組裝成request添加到request_queue,後者用於處理request_queue中的請求。通常狀況下,設備驅動經過調用blk_init_queue來初始化request_queue,q->request_fn須要給定,而q->make_request_fn使用了默認的__make_request;
__make_request。會根據不一樣的調度算法來決定如何添加bio,生成對應的request結構加入request_queue結構中,而且決定是否調用q->request_fn,或是在kblockd_workqueue任務隊列裏面添加一個任務,等kblockd內核線程來調用q->request_fn;
q->request_fn。由驅動程序定義的函數,負責從request_queue裏面取出request進行處理。從添加bio到request被取出,若干的請求已經被IO調度算法整理過了。驅動程序負責根據request結構裏面的描述,將實際物理設備裏面的數據讀到內存中。當驅動程序完成一個request時,會調用end_request(或相似)函數,以結束這個request;
end_request。完成request的收尾工做,而且會調用對應的bio的的結束方法bio->bi_end_io,即前面設置的mpage_end_io_read;
mpage_end_io_read。若是page已更新則設置其up-to-date標記,併爲page解鎖,喚醒等待page解鎖的進程。最後釋放bio對象;
sys_write。跟sys_read同樣,對應的vfs_write、do_sync_write、f_op->aio_write、generic_file_aio_write被順序調用;
generic_file_aio_write。調用__generic_file_aio_write_nolock來進行寫的處理,將數據寫到磁盤高速緩存中。寫完成以後,判斷若是文件打開時使用了O_SYNC標記,則再調用sync_page_range將寫入到磁盤高速緩存中的數據同步到磁盤(只同步文件頭信息);
__generic_file_aio_write_nolock。進行一些檢查以後,調用generic_file_buffered_write;
generic_file_buffered_write。調用generic_perform_write執行寫,寫完成以後,判斷若是文件打開時使用了O_SYNC標記,則再調用generic_osync_inode將寫入到磁盤高速緩存中的數據同步到磁盤(同步文件頭信息和文件內容);
generic_perform_write。一次異步寫可能包含多個寫操做(對應於writev系統調用),對於其中牽涉的每個page,調用file->f_mapping->a_ops->write_begin準備好須要寫的磁盤高速緩存頁面,而後將須要寫的數據拷入其中,最後調用file->f_mapping->a_ops->write_end完成寫;
file->f_mapping是從對應inode->i_mapping而來,inode->i_mapping->a_ops是由對應的文件系統類型在生成這個inode時賦予的。而各個文件系統類型提供的file->f_mapping->a_ops->write_begin函數通常是block_write_begin函數的封裝、file->f_mapping->a_ops->write_end函數通常是generic_write_end函數的封裝;
block_write_begin。調用grab_cache_page_write_begin在radix樹裏面查找要被寫的page,若是不存在則建立一個。調用__block_prepare_write爲這個page準備一組buffer_head結構,用於描述組成這個page的數據塊(利用其中的信息,能夠生成對應的bio結構);
generic_write_end。調用block_write_end提交寫請求,而後設置page的dirty標記;
block_write_end。調用__block_commit_write爲page中的每個buffer_head結構設置dirty標記;
至此,write調用就要返回了。若是文件打開時使用了O_SYNC標記,sync_page_range或generic_osync_inode將被調用。不然write就結束了,等待pdflush內核線程發現radix樹上的髒頁,並最終調用到do_writepages寫回這些髒頁;
sync_page_range也是調用generic_osync_inode來實現的,而generic_osync_inode最終也會調用到do_writepages;
do_writepages。調用inode->i_mapping->a_ops->writepages,然後者通常是mpage_writepages函數的包裝;
mpage_writepages。檢查radix樹中須要寫回的page,對每個page調用__mpage_writepage;
__mpage_writepage。這裏也是構造bio,而後調用mpage_bio_submit來進行提交;
後面的流程跟read幾乎就同樣了……node
#linux內核linux