Linux的檔案系統(File System)架構簡析

WBOY
發布: 2024-03-11 15:31:21
轉載
470 人瀏覽過

Linux的文件系统(File System)架构简析

本文主要討論虛擬檔案系統。 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快取與de​​ntry快取

在架構圖中我們看到在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)架構簡析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

來源:mryunwei.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板