從內核文件系統看文件讀寫過程

從內核文件系統看文件讀寫過程

系統調用

操作系統的主要功能是爲管理硬件資源和爲應用程序開發人員提供良好的環境,但是計算機系統的各種硬件資源是有限的,因此爲了保證每一個進程都能安全的執行。處理器設有兩種模式:“用戶模式”與“內核模式”。一些容易發生安全問題的操作都被限制在只有內核模式下纔可以執行,例如I/O操作,修改基址寄存器內容等。而連接用戶模式和內核模式的接口稱之爲系統調用。

應用程序代碼運行在用戶模式下,當應用程序需要實現內核模式下的指令時,先向操作系統發送調用請求。操作系統收到請求後,執行系統調用接口,使處理器進入內核模式。當處理器處理完系統調用操作後,操作系統會讓處理器返回用戶模式,繼續執行用戶代碼。

進程的虛擬地址空間可分爲兩部分,內核空間和用戶空間。內核空間中存放的是內核代碼和數據,而進程的用戶空間中存放的是用戶程序的代碼和數據。不管是內核空間還是用戶空間,它們都處於虛擬空間中,都是對物理地址的映射。

應用程序中實現對文件的操作過程就是典型的系統調用過程。

 

虛擬文件系統

一個操作系統可以支持多種底層不同的文件系統(比如NTFS, FAT, ext3, ext4),爲了給內核和用戶進程提供統一的文件系統視圖,Linux在用戶進程和底層文件系統之間加入了一個抽象層,即虛擬文件系統(Virtual File System, VFS),進程所有的文件操作都通過VFS,由VFS來適配各種底層不同的文件系統,完成實際的文件操作。

通俗的說,VFS就是定義了一個通用文件系統的接口層和適配層,一方面爲用戶進程提供了一組統一的訪問文件,目錄和其他對象的統一方法,另一方面又要和不同的底層文件系統進行適配。如圖所示:

          

 

虛擬文件系統主要模塊

1、超級塊(super_block),用於保存一個文件系統的所有元數據,相當於這個文件系統的信息庫,爲其他的模塊提供信息。因此一個超級塊可代表一個文件系統。文件系統的任意元數據修改都要修改超級塊。超級塊對象是常駐內存並被緩存的。

2、目錄項模塊,管理路徑的目錄項。比如一個路徑 /home/foo/hello.txt,那麼目錄項有home, foo, hello.txt。目錄項的塊,存儲的是這個目錄下的所有的文件的inode號和文件名等信息。其內部是樹形結構,操作系統檢索一個文件,都是從根目錄開始,按層次解析路徑中的所有目錄,直到定位到文件。

3、inode模塊,管理一個具體的文件,是文件的唯一標識,一個文件對應一個inode。通過inode可以方便的找到文件在磁盤扇區的位置。同時inode模塊可鏈接到address_space模塊,方便查找自身文件數據是否已經緩存。

4、打開文件列表模塊,包含所有內核已經打開的文件。已經打開的文件對象由open系統調用在內核中創建,也叫文件句柄。打開文件列表模塊中包含一個列表,每個列表表項是一個結構體struct file,結構體中的信息用來表示打開的一個文件的各種狀態參數。

5、file_operations模塊。這個模塊中維護一個數據結構,是一系列函數指針的集合,其中包含所有可以使用的系統調用函數,例如open、read、write、mmap等。每個打開文件(打開文件列表模塊的一個表項)都可以連接到file_operations模塊,從而對任何已打開的文件,通過系統調用函數,實現各種操作。

6、address_space模塊,它表示一個文件在頁緩存中已經緩存了的物理頁。它是頁緩存和外部設備中文件系統的橋樑。如果將文件系統可以理解成數據源,那麼address_space可以說關聯了內存系統和文件系統。我們會在文章後面繼續討論。

模塊間的相互作用和邏輯關係如下圖所示:

           

 

由圖可以看出:

1、每個模塊都維護了一個X_op指針指向它所對應的操作對象X_operations。

2、超級塊維護了一個s_files指針指向了“已打開文件列表模塊”,即內核所有的打開文件的鏈表,這個鏈表信息是所有進程共享的。

3、目錄操作模塊和inode模塊都維護了一個X_sb指針指向超級塊,從而可以獲得整個文件系統的元數據信息。

4、 目錄項對象和inode對象各自維護了指向對方的指針,可以找到對方的數據。

5、已打開文件列表上每一個file結構體實例維護了一個f_dentry指針,指向了它對應的目錄項,從而可以根據目錄項找到它對應的inode信息。

6、已打開文件列表上每一個file結構體實例維護了一個f_op指針,指向可以對這個文件進行操作的所有函數集合file_operations。

7、inode中不僅有和其他模塊關聯的指針,重要的是它可以指向address_space模塊,從而獲得自身文件在內存中的緩存信息。

8、address_space內部維護了一個樹結構來指向所有的物理頁結構page,同時維護了一個host指針指向inode來獲得文件的元數據。

進程和虛擬文件系統交互

1、內核使用task_struct來表示單個進程的描述符,其中包含維護一個進程的所有信息。task_struct結構體中維護了一個 files的指針(和“已打開文件列表”上的表項是不同的指針)來指向結構體files_struct,files_struct中包含文件描述符表和打開的文件對象信息。

2、file_struct中的文件描述符表實際是一個file類型的指針列表(和“已打開文件列表”上的表項是相同的指針),可以支持動態擴展,每一個指針指向虛擬文件系統中文件列表模塊的某一個已打開的文件。

          

 

3、file結構一方面可從f_dentry鏈接到目錄項模塊以及inode模塊,獲取所有和文件相關的信息,另一方面鏈接file_operations子模塊,其中包含所有可以使用的系統調用函數,從而最終完成對文件的操作。這樣,從進程到進程的文件描述符表,再關聯到已打開文件列表上對應的文件結構,從而調用其可執行的系統調用函數,實現對文件的各種操作。

進程 vs 文件列表 vs Inode

1、多個進程可以同時指向一個打開文件對象(文件列表表項),例如父進程和子進程間共享文件對象;

2、一個進程可以多次打開一個文件,生成不同的文件描述符,每個文件描述符指向不同的文件列表表項。但是由於是同一個文件,inode唯一,所以這些文件列表表項都指向同一個inode。通過這樣的方法實現文件共享(共享同一個磁盤文件);

 

I/O 緩衝區

概念

如高速緩存(cache)產生的原理類似,在I/O過程中,讀取磁盤的速度相對內存讀取速度要慢的多。因此爲了能夠加快處理數據的速度,需要將讀取過的數據緩存在內存裏。而這些緩存在內存裏的數據就是高速緩衝區(buffer cache),下面簡稱爲“buffer”。

具體來說,buffer(緩衝區)是一個用於存儲速度不同步的設備或優先級不同的設備之間傳輸數據的區域。一方面,通過緩衝區,可以使進程之間的相互等待變少,從而使從速度慢的設備讀入數據時,速度快的設備的操作進程不發生間斷。另一方面,可以保護硬盤或減少網絡傳輸的次數。

Buffer和Cache

buffer和cache是兩個不同的概念:cache是高速緩存,用於CPU和內存之間的緩衝;buffer是I/O緩存,用於內存和硬盤的緩衝;簡單的說,cache是加速“讀”,而buffer是緩衝“寫”,前者解決讀的問題,保存從磁盤上讀出的數據,後者是解決寫的問題,保存即將要寫入到磁盤上的數據。

Buffer Cache和 Page Cache

buffer cache和page cache都是爲了處理設備和內存交互時高速訪問的問題。buffer cache可稱爲塊緩衝器,page cache可稱爲頁緩衝器。在linux不支持虛擬內存機制之前,還沒有頁的概念,因此緩衝區以塊爲單位對設備進行。在linux採用虛擬內存的機制來管理內存後,頁是虛擬內存管理的最小單位,開始採用頁緩衝的機制來緩衝內存。Linux2.6之後內核將這兩個緩存整合,頁和塊可以相互映射,同時,頁緩存page cache面向的是虛擬內存,塊I/O緩存Buffer cache是面向塊設備。需要強調的是,頁緩存和塊緩存對進程來說就是一個存儲系統,進程不需要關注底層的設備的讀寫。

buffer cache和page cache兩者最大的區別是緩存的粒度。buffer cache面向的是文件系統的塊。而內核的內存管理組件採用了比文件系統的塊更高級別的抽象:頁page,其處理的性能更高。因此和內存管理交互的緩存組件,都使用頁緩存。

 

Page Cache

頁緩存是面向文件,面向內存的。通俗來說,它位於內存和文件之間緩衝區,文件IO操作實際上只和page cache交互,不直接和內存交互。page cache可以用在所有以文件爲單元的場景下,比如網絡文件系統等等。page cache通過一系列的數據結構,比如inode, address_space, struct page,實現將一個文件映射到頁的級別:

1、struct page結構標誌一個物理內存頁,通過page + offset就可以將此頁幀定位到一個文件中的具體位置。同時struct page還有以下重要參數:

(1)標誌位flags來記錄該頁是否是髒頁,是否正在被寫回等等;

(2)mapping指向了地址空間address_space,表示這個頁是一個頁緩存中頁,和一個文件的地址空間對應;

(3)index記錄這個頁在文件中的頁偏移量;

2、文件系統的inode實際維護了這個文件所有的塊block的塊號,通過對文件偏移量offset取模可以很快定位到這個偏移量所在的文件系統的塊號,磁盤的扇區號。同樣,通過對文件偏移量offset進行取模可以計算出偏移量所在的頁的偏移量。

3、page cache緩存組件抽象了地址空間address_space這個概念來作爲文件系統和頁緩存的中間橋樑。地址空間address_space通過指針可以方便的獲取文件inode和struct page的信息,所以可以很方便地定位到一個文件的offset在各個組件中的位置,即通過:文件字節偏移量 --> 頁偏移量 --> 文件系統塊號 block  -->  磁盤扇區號

4、頁緩存實際上就是採用了一個基數樹結構將一個文件的內容組織起來存放在物理內存struct page中。一個文件inode對應一個地址空間address_space。而一個address_space對應一個頁緩存基數樹。它們之間的關係如下:

          

 

Address Space

下面我們總結已經討論過的address_space所有功能。address_space是Linux內核中的一個關鍵抽象,它被作爲文件系統和頁緩存的中間適配器,用來指示一個文件在頁緩存中已經緩存了的物理頁。因此,它是頁緩存和外部設備中文件系統的橋樑。如果將文件系統可以理解成數據源,那麼address_space可以說關聯了內存系統和文件系統。

由圖中可以看到,地址空間address_space鏈接到頁緩存基數樹和inode,因此address_space通過指針可以方便的獲取文件inode和page的信息。那麼頁緩存是如何通過address_space實現緩衝區功能的?我們再來看完整的文件讀寫流程。

 

文件讀寫基本流程

讀文件

1、進程調用庫函數向內核發起讀文件請求;

2、內核通過檢查進程的文件描述符定位到虛擬文件系統的已打開文件列表表項;

3、調用該文件可用的系統調用函數read()

3、read()函數通過文件表項鍊接到目錄項模塊,根據傳入的文件路徑,在目錄項模塊中檢索,找到該文件的inode;

4、在inode中,通過文件內容偏移量計算出要讀取的頁;

5、通過inode找到文件對應的address_space;

6、在address_space中訪問該文件的頁緩存樹,查找對應的頁緩存結點:

(1)如果頁緩存命中,那麼直接返回文件內容;

(2)如果頁緩存缺失,那麼產生一個頁缺失異常,創建一個頁緩存頁,同時通過inode找到文件該頁的磁盤地址,讀取相應的頁填充該緩存頁;重新進行第6步查找頁緩存;

7、文件內容讀取成功。

 

寫文件

前5步和讀文件一致,在address_space中查詢對應頁的頁緩存是否存在:

6、如果頁緩存命中,直接把文件內容修改更新在頁緩存的頁中。寫文件就結束了。這時候文件修改位於頁緩存,並沒有寫回到磁盤文件中去。

7、如果頁緩存缺失,那麼產生一個頁缺失異常,創建一個頁緩存頁,同時通過inode找到文件該頁的磁盤地址,讀取相應的頁填充該緩存頁。此時緩存頁命中,進行第6步。

8、一個頁緩存中的頁如果被修改,那麼會被標記成髒頁。髒頁需要寫回到磁盤中的文件塊。有兩種方式可以把髒頁寫回磁盤:

(1)手動調用sync()或者fsync()系統調用把髒頁寫回

(2)pdflush進程會定時把髒頁寫回到磁盤

同時注意,髒頁不能被置換出內存,如果髒頁正在被寫回,那麼會被設置寫回標記,這時候該頁就被上鎖,其他寫請求被阻塞直到鎖釋放。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章