跳至內容

Linux的文件系統(File System)架構簡析

更新時間
快连VPN:速度和安全性最佳的VPN服务
快连VPN:速度和安全性最佳的VPN服务

本文主要討論虛擬文件系統。Linux文件系統的架構包括具體文件系統(如Ext2、Ext3和XFS等)與應用程序之間的抽象層,即虛擬文件系統(Virtual File System,簡稱VFS)。VFS允許應用程序與不同類型的文件系統進行通信而無需瞭解底層文件系統的細節。通過VFS,文件系統的實現可以被隔離並與應用程序解耦,從而提高了系統的靈活性和可維護性。VFS還允許Linux內核支持多種文件系統類型,並提供了一個統一的接口供應用程序訪問文件系統。在VFS的框架下,不同的文件系統可以通過實現標準的文件系統操作接口來與內核進行交

圖片

上圖顯示,這一架構的中心是虛擬文件系統VFS。VFS提供了一個文件系統框架,本地文件系統可以基於VFS實現。它主要完成以下幾項工作:

1) VFS作爲抽象層爲應用層提供了統一的接口(read、write和chmod等)。

2) 在VFS中實現了一些公共的功能,如inode緩存和頁緩存等。

3) 規範了具體文件系統應該實現的接口。

根據以上設定,其他具體的文件系統只需遵循VFS的約定,實現相應的接口和內部邏輯,並註冊到系統中。用戶在格式化並掛載文件系統後,便可以利用硬盤資源進行基於該文件系統的操作。

在Linux操作系統中,在格式化磁盤後需要通過mount命令將其掛載到系統目錄樹的某個目錄下面,這個目錄稱爲掛載點(mount point)。完成掛載後,我們就可以使用基於該文件系統格式化的硬盤空間了。在Linux操作系統中,掛載點幾乎可以是任意目錄,但爲了規範化,掛載點通常是mnt目錄下的子目錄。    

下面展示的是一個相對複雜的目錄結構。在這個目錄結構中,根目錄位於硬盤sda上,並且在mnt目錄下有三個子目錄分別是ext4、xfs和nfs,它們分別掛載了Ext4文件系統(構建在sdb硬盤上)、XFS文件系統(構建在sdc硬盤上)和NFS文件系統(通過網絡掛載)。

圖片

目錄樹中多個文件系統的關係是內核中的一些數據結構表示的。在進行文件系統掛載的時候會建立文件系統間的關係,並且註冊具體文件系統的API。當用戶態調用打開文件的API時,會找到對應的文件系統API,並關聯到文件相關的結構體(例如file和inode等)。

上面的描述比較概要,大家可能還是有點雲裏霧裏的感覺。不過大家不要着急,我們接下來會結合代碼更加詳細的介紹VFS及如何實現對多種文件系統的支持。

1.從文件系統API到VFS,再到具體文件系統

Linux的VFS並不是一開始就有的,最早發佈的Linux版本並沒有VFS。而且,VFS並非是在Linux發明的,它最早於1985年由Sun公司在其SunOS2.0中開發。開發VFS的主要目的是爲了適配其本地文件系統和NFS文件系統。

VFS通過一套公共的API和數據結構實現了對具體文件系統的抽象。當用戶調用操作系統提供的文件系統API時會通過軟中斷的方式調用內核VFS實現的函數。如下表所示是部分文件API與內核VFS函數的對應關係。    

用戶態API

內核函數

說明

open

do_sys_open

打開文件

close

ksys_close

關閉文件

read

ksys_read/vfs_read

讀取數據

write

ksys_write/vfs_write

寫入數據

mount

do_mount

掛載文件系統

由上表可以看出每個用戶態的API都有一個內核態的函數與之對應。當應用程序調用文件系統的API時會觸發內核態的對應函數。這裏列舉的只是文件系統API中的一個比較小的子集,目的是爲了說明API與VFS的關係。如果大家想了解其他API請自行閱讀內核源代碼,本文不再贅述。

爲了讓大家能夠對VFS與具體文件系統的關係有個感性的認識,本節以Ext2的寫API爲例來展示一下從API到VFS函數,再到Ext2文件系統函數的調用關係。如下圖所示,API函數write通過軟中斷觸發內核的ksys_write函數,該函數經過若干處理後最終會通過函數指針(file->f_op->wirte_iter)的方式調用Ext2文件系統的ext2_file_write_iter函數。    

圖片

在上圖中內核流程的入口是ksys_write函數,通過實現代碼可以看出,這裏主要是獲取一個fd,然後以fd中的成員file作爲參數調用vfs_write。其中fd是一個結構體,其格式如下圖所示,file成員是比較核心的數據結構。從上圖可以看出,正是通過這個成員中的內容才調到了Ext2文件系統的函數。    

圖片

看上去很簡單,VFS只要調用具體文件系統註冊的函數指針即可。但是這裏有個問題沒有解決,VFS中的函數指針是什麼時候被註冊的呢?

Ext2的函數指針是在打開文件的時候被初始化的(具體細節請參考《文件系統技術內幕》3.1.2.2節)。大家都知道,用戶態的程序在打開一個文件的時候返回的是一個文件描述符,但在內核中表示文件的結構體file與之對應。這個結構體裏面比較重要的幾個成員包括f_inode、f_ops和f_mapping等,具體如下圖所示。

圖片

在上圖中,f_inode是該文件對應的inode節點。f_ops是具體文件系統(例如Ext2)文件操作的函數指針集合,它是在打開文件的時候被初始化的。VFS正是通過該函數指針集合來實現對具體文件系統訪問的。

上面又涉及到VFS的另外一個概念inode。在Linux中,inode是index node的縮寫,他表示了文件系統中的一個具體的對象(比如文件或者目錄)。在VFS中有一個名稱爲inode的數據結構,他是對具體文件系統inode的抽象。比如在Ext2文件系統中具體定義爲ext2_inode_info,在XFS中則是通過數據結構xfs_inode表示的。而且具體文件系統的inode數據結構與VFS的inode有個內在的關聯,大家可以自行閱讀代碼。

2.inode緩存與dentry緩存

在架構圖中我們看到在VFS中有若干個緩存實現,包括頁緩存、inode緩存和dentry緩存等。其中inode緩存和dentry緩存實現方式相同,也比較簡單。所以,本文先介紹一下這兩個緩存。

其實這兩個緩存是通過哈希表實現的,哈希表的概念大家都比較清楚,本文不再贅述。以inode緩存爲例,如下圖是其初始化的過程,通過參數ihash_entries可以看出其大小是動態的(其大小跟系統內存相關,系統內存閱讀,inode緩存就越大)。

圖片

由於訪問文件時會經常訪問inode和dentry,所以將兩者緩存起來能夠避免從硬盤讀取數據導致的性能損失。

3.頁緩存(Page Cache)

VFS頁緩存(Cache)的作用主要用來提升文件系統的性能。緩存技術是指在內存中存儲文件系統的部分數據和元數據而提升文件系統性能的技術。由於內存的訪問延時是機械硬盤訪問延時的十萬分之一(如下圖所示,以寄存器爲基準單位1s),因此採用緩存技術可以大幅提升文件系統的性能。

圖片

緩存通過三方面的IO優化來提升文件系統的性能,分別是熱點數據、預讀和IO合併。很多應用都會有熱點數據,比如作者在編輯文檔的時候,當前這個數據塊及附近的數據塊就是熱點數據。或者當出現一個爆款文章時,這篇文章的內容就是熱點數據。底層存儲設備對於大塊讀寫的性能往往較好,預讀就是提前從底層設備讀取大塊數據緩存起來,這樣可以通過緩存來響應應用的請求。IO合併則是針對寫請求,寫請求不馬上持久化到後端設備,而是緩存一下,拼成大塊IO再寫入。

由於內存的容量要比硬盤的容量小的多,因此頁緩存自然不能緩存所有硬盤的數據。這樣緩存中只能存儲文件系統數據的一個子集。當用戶持續寫入數據的時候就會面臨緩存滿的情況,此時就涉及如何將緩存數據刷寫磁盤,然後存儲新數據的問題。

這裏將緩存刷寫到磁盤,並且存儲新數據的過程稱爲緩存替換。緩存替換有很多種算法,每種算法用於解決不同的問題。接下來我們介紹幾種常見的緩存替換算法。

LRU算法,LRU的全稱是Least Recently Used,也就是最近最少使用。該算法依據的是時間局部性原理,也就是如果一個數據最近被使用過,那麼接下來有很大的概率還會被使用。因此該算法會將最近沒有使用過的緩存釋放掉。

LRU算法通常使用一個鏈表來實現,剛被使用過的緩存會被插到表頭的位置,而經常沒有被使用過的數據則慢慢被擠到鏈表的尾部。爲了更加清晰的理解LRU的原理,我們結合下圖進行說明。    

圖片

在該例中,我們以全命中爲例進行介紹。假設緩存中有6個數據塊,如圖第一行所示,方塊中的數字代表該數據塊的編號。假設第一次訪問(可以是讀或者寫)的是3號數據塊,由於其被訪問過,因此將其移動到鏈表頭。

第二次訪問時訪問的是第4號數據塊,按照相同的原則,該數據塊也被移動到鏈表頭。具體如上圖第2行所示。

以此類推,當經過4輪訪問後,被訪問過的數據都被前移了,而沒有被訪問過的數據塊(例如1和2)則被慢慢擠到了鏈表的後面。這在一定程度上預示着這兩個數據塊在後面被訪問的可能性也比較小。

如果是全命中的話也就不存在緩存被替換的情況了。實際情況是緩存會經常不夠用,而需要將其中的數據釋放(視情況確定是否需要刷新到磁盤)來存儲新的數據。此時,LRU算法就派上用場了,該算法將尾部的數據塊拿來存儲新數據,然後放到鏈表頭,具體下圖如所示。如果這個數據塊裏面是髒數據則需要刷寫到磁盤,否則直接釋放掉就可以。    

圖片

LRU算法原理和實現都比較簡單,用途卻非常廣泛。但是LRU算法有個缺點,就是當突然有大量連續數據寫入時會替換掉所有的緩存塊,從而導致之前統計的緩存使用情況全部失效,這種現象稱爲緩存污染。爲了解決緩存污染問題,有很多改進的LRU算法,其中比較常見的有LRU-K、2Q和LIRS等。

LFU算法,LFU的全稱是Least Frequently Used,也就是最近最不經常使用。該算法是根據數據被訪問的頻度來決策釋放哪一個緩存塊的。訪問頻度最低的緩存塊會被最先釋放掉。

如下圖所示是LFU算法的示意圖。其中第1行是原始狀態,方塊中的數字表示該緩存塊被訪問的次數。新數據的加入和緩存塊的淘汰都是從尾部進行。假設某一塊(虛線框)數據被訪問了4次,則其訪問次數從12變成了16,因此需要移動到新的位置,也就是圖中第2行的樣子。

圖片

本書以鏈表爲例說明LFU的原理是爲了便於理解,但是在工程實現的時候是絕對不會用鏈表來實現的。因爲當數據塊的訪問次數變化時需要找新的位置,鏈表查找操作是非常耗時的。爲了能夠實現快速查找,一般採用搜索樹來實現。    

LFU也有其缺點,如果某個數據塊在很久之前的某個時間段高頻訪問,而以後不再訪問,那麼該數據會一直停留在緩存中。但是由於該數據不會被訪問了,所以導致緩存的有效容量減少了。也就是說LFU算法沒有考慮最近的情況。

本文主要介紹了LRU和LFU等2種非常基礎的替換算法。除了上述算法外,還有還很多替換算法,大多以LRU和LFU的理論爲基礎,比如2Q,MQ,LRFU,TinyLFU和ARC等等。限於篇幅,本書不再贅述,大家可以自行閱讀相關的論文。

數據預讀也是有一定的算法的,預讀算法通過識別IO模式方式來提前將數據從磁盤讀到緩存中。這樣,應用讀取數據時就可以直接從緩存讀取數據,從而極大的提高讀數據的性能。

預讀算法裏面最爲重要的是觸發條件,也就是在什麼情況下出發預讀操作。通常有兩種情況會觸發預讀:一個是有多個地址連續的讀請求時會觸發預讀操作;另外一個是應用訪問到有預讀標記的緩存時。這裏,預讀標記的緩存是在預讀操作完成時在緩存頁做的標記,當應用讀到有該標記的緩存時會觸發下一次的預讀,從而省略對IO模式的識別。

圖片

爲了更加清晰的解釋預讀的邏輯,我們通過上圖來介紹一下整個流程。當文件系統識別IO模式需要預讀的時候,會多讀出一部分內容(稱爲同步預讀),如時間1(第一行)所示。同時,對於同步預讀的數據,文件系統會在其中某個塊上打上標記。這個標記的目的是爲了在緩存結束前能夠儘早的觸發下一次的預讀。    

第2個時間點,當應用繼續讀取數據時,由於讀到了有標記的緩存塊,因此會同時觸發下一次的預讀。此時數據會被從磁盤一步讀取,可以從圖中看出緩存增加。

接下來時間點3,4,應用可以直接從緩存讀取數據。由於沒有讀到有標記的緩存塊,因此也不會觸發下一次的預讀。在時間點5,由於有預讀標記,因此又會觸發預讀的流程。

通過上述分析可以看出,由於預讀特性將數據提前讀到了緩存當中。應用可以直接從緩存讀取數據,而不用再訪問磁盤,因此整個訪問性能將得到大幅的提升。

以上就是Linux的文件系統(File System)架構簡析的詳細內容,更多請關注本站其它相關文章!

更新時間

發表留言

請注意,留言須先通過審核才能發佈。