Linux 檔案系統剖析

來源:互聯網
上載者:User

什麼是檔案系統?

首先回答最常見的問題,“什麼是檔案系統”。檔案系統是對一個存放裝置上的資料和中繼資料進行組織的機制。由於定義如此寬
泛,支援它的代碼會很有意思。正如前面提到的,有許多種檔案系統和媒體。由於存在這麼多類型,可以預料到 Linux
檔案系統介面實現為分層的體繫結構,從而將使用者介面層、檔案系統實現和操作存放裝置的驅動程式分隔開。

掛裝

在 Linux 中將一個檔案系統與一個存放裝置關聯起來的過程稱為掛裝(mount)。使用 mount 命令將一個檔案系統附著到當前檔案系統階層中(根)。在執行掛裝時,要提供檔案系統類型、檔案系統和一個掛裝點。

為了說明 Linux 檔案系統層的功能(以及掛裝的方法),我們在當前檔案系統的一個檔案中建立一個檔案系統。實現的方法是,首先用 dd 命令建立一個指定大小的檔案(使用 /dev/zero 作為源進行檔案複製)—— 換句話說,一個用零進行初始化的檔案,見清單 1。

清單 1. 建立一個經過初始化的檔案

                
$ dd if=/dev/zero of=file.img bs=1k count=10000
10000+0 records in
10000+0 records out
$

現在有了一個 10MB 的 file.img 檔案。使用 losetup 命令將一個迴圈裝置與這個檔案關聯起來,讓它看起來像一個塊裝置,而不是檔案系統中的常規檔案:

$ losetup /dev/loop0 file.img
$

這個檔案現在作為一個塊裝置出現(由 /dev/loop0 表示)。然後用 mke2fs 在這個裝置上建立一個檔案系統。這個命令建立一個指定大小的新的 ext2 檔案系統,見清單 2。

清單 2. 用迴圈裝置建立 ext2 檔案系統

                
$ mke2fs -c /dev/loop0 10000
mke2fs 1.35 (28-Feb-2004)
max_blocks 1024000, rsv_groups = 1250, rsv_gdb = 39
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
2512 inodes, 10000 blocks
500 blocks (5.00%) reserved for the super user
...
$

使用 mount 命令將迴圈裝置(/dev/loop0)所表示的 file.img 檔案掛裝到掛裝點 /mnt/point1。注意,檔案系統類型指定為 ext2。掛裝之後,就可以將這個掛裝點當作一個新的檔案系統,比如使用 ls 命令,見清單 3。

清單 3. 建立掛裝點並通過迴圈裝置掛裝檔案系統

                
$ mkdir /mnt/point1
$ mount -t ext2 /dev/loop0 /mnt/point1
$ ls /mnt/point1
lost+found
$

如清單 4 所示,還可以繼續這個過程:在剛才掛裝的檔案系統中建立一個新檔案,將它與一個迴圈裝置關聯起來,再在上面建立另一個檔案系統。

清單 4. 在迴圈檔案系統中建立一個新的迴圈檔案系統

                
$ dd if=/dev/zero of=/mnt/point1/file.img bs=1k count=1000
1000+0 records in
1000+0 records out
$ losetup /dev/loop1 /mnt/point1/file.img
$ mke2fs -c /dev/loop1 1000
mke2fs 1.35 (28-Feb-2004)
max_blocks 1024000, rsv_groups = 125, rsv_gdb = 3
Filesystem label=
...
$ mkdir /mnt/point2
$ mount -t ext2 /dev/loop1 /mnt/point2
$ ls /mnt/point2
lost+found
$ ls /mnt/point1
file.img lost+found
$

通過這個簡單的示範很容易體會到 Linux 檔案系統(和迴圈裝置)是多麼強大。可以按照相同的方法在檔案上用迴圈裝置建立加密的檔案系統。可以在需要時使用迴圈裝置臨時掛裝檔案,這有助於保護資料。

回頁首

檔案系統體繫結構

既然已經看到了檔案系統的構造方法,現在就看看 Linux 檔案系統層的體繫結構。本文從兩個角度考察 Linux 檔案系統。首先採用高層體繫結構的角度。然後進行深層次討論,介紹實現檔案系統層的主要結構。

回頁首

高層體繫結構

儘管大多數檔案系統代碼在核心中(後面討論的使用者空間檔案系統除外),但是圖 1 所示的體繫結構顯示了使用者空間和核心中與檔案系統相關的主要組件之間的關係。

圖 1. Linux 檔案系統組件的體繫結構

使用者空間包含一些應用程式(例如,檔案系統的使用者)和 GNU C 庫(glibc),它們為檔案系統調用(開啟、讀取、寫和關閉)提供使用者介面。系統調用介面的作用就像是交換器,它將系統調用從使用者空間發送到核心空間中的適當端點。

VFS 是底層檔案系統的主要介面。這個組件匯出一組介面,然後將它們抽象到各個檔案系統,各個檔案系統的行為可能差異很大。有兩個針對檔案系統對象的緩衝(inode 和 dentry)。它們緩衝最近使用過的檔案系統對象。

每個檔案系統實現(比如 ext2、JFS 等等)匯出一組通用介面,供 VFS
使用。緩衝區快取會快取檔案系統和相關塊裝置之間的請求。例如,對底層裝置驅動程式的讀寫請求會通過緩衝區快取來傳遞。這就允許在其中緩衝請求,減少訪問
物理裝置的次數,加快訪問速度。以最近使用(LRU)列表的形式管理緩衝區快取。注意,可以使用 sync 命令將緩衝區快取中的請求發送到儲存媒體(迫使所有未寫的資料發送到裝置驅動程式,進而發送到存放裝置)。

對象關係

我們已經查看了 VFS 層中的各種重要對象,現在我們通過一個圖表展示它們之間的關係。到目前為止,我都是以一種自下而上的方式探索對象,現在我們採用自上而下方式,從使用者透視圖中考察對象(見 圖 7)。

在頂層是開啟的 file 對象,它由進程的檔案描述符列表引用。file 對象引用 dentry 對象,後者引用 inodeinodedentry 對象都引用底層的 super_block 對象。可能有多個檔案對象引用同一個 dentry(當兩個使用者共用同一個檔案時)。注意,在圖 7 中一個 dentry 對象還引用另一個 dentry 對象。在這裡,目錄引用檔案,而檔案反過來引用特定檔案的 inode。

圖 7. VFS 中的主要對象之間的關係

回頁首

VFS 架構

VFS 的內部架構由一個調度層(提供檔案系統抽象)和許多緩衝(用於改善檔案系統操作的效能)組成。這個小節探索內部架構和主要對象之間的互動(見圖 8)。

圖 8. VFS 層的進階視圖

在 VFS 中動態管理的兩個主要對象是 dentryinode
對象。緩衝這兩個對象,以改善訪問底層檔案系統的效能。當開啟一個檔案時,dentry
緩衝將被表示目錄層級(目錄層級表示路徑)的條目填充。此外,還為該對象建立一個表示檔案的 inode。使用散列表建立 dentry
緩衝,並且根據對象名分配緩衝。dentry 緩衝的條目從 dentry_cache slab
分配器分配,並且在緩衝存在壓力時使用最近不使用(least-recently-used,LRU)演算法刪除條目。您可以在
./linux/fs/dcache.c 和 ./linux/include/linux/dcache.h 中找到與 dentry
緩衝相關的函數。

為了實現更快的尋找速度,inode 緩衝被實現為兩個列表和一個散列表。第一個列表定義當前使用的 inode;第二個列表定義未使用的 inode。正在使用的 inode 還儲存在散列表中。從 inode_cache slab 分配器分配單個 inode 緩衝對象。您可以在 ./linux/fs/inode.c 和 ./linux/include/fs.h 中找到與 inode 緩衝相關的函數。在現在的實現中,dentry 緩衝支配著 inode 緩衝。如果存在一個 dentry 對象,那麼 inode 緩衝中也將存在一個 inode 對象。尋找是在 dentry 緩衝中執行的,這將導致 inode 緩衝中出現一個對象。

虛擬檔案系統(Virtual File System, 簡稱 VFS),
是 Linux 核心中的一個軟體層,用於給使用者空間的程式提供檔案系統介面;同時,它也提供了核心中的一個
抽象功能,允許不同的檔案系統共存。系統中所有的檔案系統不但依賴 VFS 共存,而且也依靠 VFS 協同工作。

為了能夠支援各種實際檔案系統,VFS 定義了所有檔案系統都支援的基本的、概念上的介面和資料
結構;同時實際檔案系統也提供 VFS 所期望的抽象介面和資料結構,將自身的諸如檔案、目錄等概念在形式
上與VFS的定義保持一致。換句話說,一個實際的檔案系統想要被 Linux 支援,就必須提供一個符合VFS標準
的介面,才能與 VFS 協同工作。實際檔案系統在統一的介面和資料結構下隱藏了具體的實現細節,所以在VFS
層和核心的其他部分看來,所有檔案系統都是相同的。圖3顯示了VFS在核心中與實際的檔案系統的協同關係。

圖3. VFS在核心中與其他的核心模組的協同關係

我們已經知道,正是由於在核心中引入了VFS,跨檔案系統的檔案操作才能實現,“一切皆是檔案”
的口號才能承諾。而為什麼引入了VFS,就能實現這兩個特性呢?在接下來,我們將以這樣的一個思路來切入
文章的正題:我們將先簡要介紹下用以描述VFS模型的一些資料結構,總結出這些資料結構相互間的關係;然後
選擇兩個具有代表性的檔案I/O操作sys_open()和sys_read()來詳細說明核心是如何藉助VFS和具體的檔案系統打
交道以實現跨檔案系統的檔案操作和承諾“一切皆是檔案”的口號。

圖4. 磁碟與檔案系統

VFS資料結構

超級塊對象

儲存一個已安裝的檔案系統的控制資訊,代表一個已安裝的檔案系統;每次一個實際的檔案系統被安裝時,
核心會從磁碟的特定位置讀取一些控制資訊來填充記憶體中的超級塊對象。一個安裝執行個體和一個超級塊對象一一對應。
超級塊通過其結構中的一個域s_type記錄它所屬的檔案系統類型。

索引節點對象

索引節點Object Storage Service了檔案的相關資訊,代表了存放裝置上的一個實際的物理檔案。當一個
檔案首次被訪問時,核心會在記憶體中組裝相應的索引節點對象,以便向核心提供對一個檔案進行操
作時所必需的全部資訊;這些資訊一部分儲存在磁碟特定位置,另外一部分是在載入時動態填充的。

目錄項對象

引入目錄項的概念主要是出於方便尋找檔案的目的。一個路徑的各個組成部分,不管是目錄還是
普通的檔案,都是一個目錄項對象。如,在路徑/home/source/test.c中,目錄 /, home, source和檔案
test.c都對應一個目錄項對象。不同於前面的兩個對象,目錄項對象沒有對應的磁碟資料結構,VFS在遍
曆路徑名的過程中現場將它們逐個地解析成目錄項對象。

檔案對象

檔案對象是已開啟的檔案在記憶體中的表示,主要用於建立進程和磁碟上的檔案的對應關係。它由sys_open()
現場建立,由sys_close()銷毀。檔案對象和物理檔案的關係有點像進程和程式的關係一樣。當我們站在使用者空間來看
待VFS,我們像是只需與檔案對象打交道,而無須關心超級塊,索引節點或目錄項。因為多個進程可以同時開啟和操作
同一個檔案,所以同一個檔案也可能存在多個對應的檔案對象。檔案對象僅僅在進程觀點上代表已經開啟的檔案,它
反過來指向目錄項對象(反過來指向索引節點)。一個檔案對應的檔案對象可能不是惟一的,但是其對應的索引節點和
目錄項對象無疑是惟一的。

和檔案系統相關

根據檔案系統所在的物理介質和資料在物理介質上的組織方式來區分不同的檔案系統類型的。
file_system_type結構用於描述具體的檔案系統的類型資訊。被Linux支援的檔案系統,都有且僅有一
個file_system_type結構而不管它有零個或多個執行個體被安裝到系統中。

而與此對應的是每當一個檔案系統被實際安裝,就有一個vfsmount結構體被建立,這個結構體對應一個安裝點。

對象間的聯絡

如上的資料結構並不是孤立存在的。正是通過它們的有機聯絡,VFS才能正常工作。如下的幾張圖是對它們之間的聯絡的描述。

5所示,被Linux支援的檔案系統,都有且僅有一個file_system_type結構而不管它有零個或多個執行個體被安裝到系統
中。每安裝一個檔案系統,就對應有一個超級塊和安裝點。超級塊通過它的一個域s_type指向其對應的具體的檔案系統類型。具體的
檔案系統通過file_system_type中的一個域fs_supers連結具有同一種檔案類型的超級塊。同一種檔案系統類型的超級塊通過域s_instances鏈
接。

圖5. 超級塊、安裝點和具體的檔案系統的關係

從圖6可知:進程通過task_struct中的一個域files_struct files來瞭解它當前所開啟的檔案對象;而我們通常所說的檔案
描述符其實是進程開啟的檔案對象數組的索引值。檔案對象通過域f_dentry找到它對應的dentry對象,再由dentry對象的域d_inode找
到它對應的索引結點,這樣就建立了檔案對象與實際的物理檔案的關聯。最後,還有一點很重要的是, 檔案對象所對應的檔案操作函數
列表是通過索引結點的域i_fop得到的。圖6對第三部分源碼的理解起到很大的作用。

圖6. 進程與超級塊、檔案、索引結點、目錄項的關係

回頁首

基於VFS的檔案I/O

到目前為止,文章主要都是從理論上來講述VFS的運行機制;接下來我們將深入原始碼層中,通過闡述兩個具有代表性的系統
調用sys_open()和sys_read()來更好地理解VFS向具體檔案系統提供的介面機制。由於本文更關注的是檔案操作的整個流程體制,所以我
們在追蹤原始碼時,對一些細節性的處理不予關心。又由於篇幅所限,只列出相關代碼。本文中的原始碼來自於linux-2.6.17核心版本。

在深入sys_open()和sys_read()之前,我們先概覽下調用sys_read()的上下文。圖7描述了從使用者空間的read()調用到資料從
磁碟讀出的整個流程。當在使用者應用程式調用檔案I/O read()操作時,系統調用sys_read()被激發,sys_read()找到檔案所在的具體檔案
系統,把控制權傳給該檔案系統,最後由具體檔案系統與物理介質互動,從介質中讀出資料。

圖7. 從物理介質讀資料的過程

3.1 sys_open()

sys_open()系統調用開啟或建立一個檔案,成功返回該檔案的檔案描述符。圖8是sys_open()實現代碼中主要的函數呼叫歷程圖。

圖8. sys_open函數呼叫歷程圖

由於sys_open()的代碼量大,函數調用關係複雜,以下主要是對該函數做整體的解析;而對其中的一些關鍵點,則列出其關鍵代碼。

a. 從sys_open()的函數呼叫歷程圖可以看到,sys_open()在做了一些簡單的參數檢驗後,就把接力棒傳給do_sys_open():

1)、首先,get_unused_fd()得到一個可用的檔案描述符;通過該函數,可知檔案描述符實質是進程開啟檔案清單中對應某個檔案對象的索引值;

2)、接著,do_filp_open()開啟檔案,返回一個file對象,代表由該進程開啟的一個檔案;進程通過這樣的一個資料結構對物理檔案進行讀寫操作。

3)、最後,fd_install()建立檔案描述符與file對象的聯絡,以後進程對檔案的讀寫都是通過操縱該檔案描述符而進行。

b. do_filp_open()用於開啟檔案,返回一個file對象;而開啟之前需要先找到該檔案:

1)、open_namei()用於根據檔案路徑名尋找檔案,藉助一個持有路徑資訊的資料結構nameidata而進行;

2)、尋找結束後將填充有路徑資訊的nameidata返回給接下來的函數nameidata_to_filp()從而得到最終的file對象;當達到目的後,nameidata這個資料結構將會馬上被釋放。

c.open_namei()用於尋找一個檔案:

1)、path_lookup_open()實現檔案的尋找功能;要開啟的檔案若不存在,還需要有一個建立的過程,則調用
path_lookup_create(),後者和前者封裝的是同一個實際的路徑尋找函數,只是參數不一樣,使它們在處理細節上有所偏差;

2)、當是以建立檔案的方式開啟檔案時,即設定了O_CREAT標識時需要建立一個新的索引節點,代表建立一個檔案。在vfs_create()裡的一句
核心語句dir->i_op->create(dir, dentry, mode,
nd)可知它調用了具體的檔案系統所提供的建立索引節點的方法。注意:這邊的索引節點的概念,還只是位於記憶體之中,它和磁碟上的物理的索引節點的關係就像
位於記憶體中和位於磁碟中的檔案一樣。此時建立的索引節點還不能完全標誌一個物理檔案的成功建立,只有當把索引節點回寫到磁碟上才是一個物理檔案的真正創
建。想想我們以建立的方式開啟一個檔案,對其讀寫但最終沒有儲存而關閉,則位於記憶體中的索引節點會經曆從建立到消失的過程,而磁碟卻始終不知道有人曾經想
過建立一個檔案,這是因為索引節點沒有回寫的緣故。

3)、path_to_nameidata()填充nameidata資料結構;

4)、may_open()檢查是否可以開啟該檔案;一些檔案如連結檔案和只有寫入權限的目錄是不能被開啟的,先檢查
nd->dentry->inode所指的檔案是否是這一類檔案,是的話則錯誤返回。還有一些檔案是不能以TRUNC的方式開啟的,若
nd->dentry->inode所指的檔案屬於這一類,則顯式地關閉TRUNC標誌位。接著如果有以TRUNC方式開啟檔案的,則更新
nd->dentry->inode的資訊

3.1.1__path_lookup_intent_open()

不管是path_lookup_open()還是path_lookup_create()最終都是調用
__path_lookup_intent_open()來實現尋找檔案的功能。
尋找時,在遍曆路徑的過程中,會逐層地將各個路徑組成部分解析成目錄項對象,如果此目錄項對象在目錄項緩衝中,則直接從緩衝中獲得;如果該目錄項在緩衝中
不存在,則進行一次實際的讀盤操作,從磁碟中讀取該目錄項所對應的索引節點。得到索引節點後,則建立索引節點與該目錄項的聯絡。如此迴圈,直到最終找到目
標檔案對應的目錄項,也就找到了索引節點,而由索引節點找到對應的超級塊對象就可知道該檔案所在的檔案系統的類型。
從磁碟中讀取該目錄項所對應的索引節點;這將引發VFS和實際的檔案系統的一次互動。從前面的VFS理論介紹可知,讀索引節點方法是由超級塊來提供的。而
當安裝一個實際的檔案系統時,在記憶體中建立的超級塊的資訊是由一個實際檔案系統的相關資訊來填充的,這裡的相關資訊就包括了實際檔案系統所定義的超級塊的
操作函數列表,當然也就包括了讀索引節點的具體執行方式。
當繼續追蹤一個實際檔案系統ext3的ext3_read_inode()時,可發現這個函數很重要的一個工作就是為不同的檔案類型設定不同的索引節點操
作函數表和檔案操作函數表。

清單8. ext3_read_inode

                
void ext3_read_inode(struct inode * inode)
{
……
//是普通檔案         
   if (S_ISREG(inode->i_mode)) {
inode->i_op = &ext3_file_inode_operations;
inode->i_fop = &ext3_file_operations;
ext3_set_aops(inode);
} else if (S_ISDIR(inode->i_mode)) {
//是目錄檔案
      inode->i_op = &ext3_dir_inode_operations;
      inode->i_fop = &ext3_dir_operations;
     } else if (S_ISLNK(inode->i_mode)) {
   // 是串連檔案 
      ……
     } else { 
          // 如果以上三種情況都排除了,則是裝置驅動
//這裡的裝置還包括套結字、FIFO等偽裝置 
……
}

3.1.2 nameidata_to_filp子函數:__dentry_open

這是VFS與實際的檔案系統聯絡的一個關鍵點。從3.1.1小節分析中可知,調用實際檔案系統讀取索引節點的方法讀取索引節點時,實際檔案系統會根據檔案
的不同類型賦予索引節點不同的檔案操作函數集,如普通檔案有普通檔案對應的一套操作函數,裝置檔案有裝置檔案對應的一套操作函數。這樣當把對應的索引節點
的檔案操作函數集賦予檔案對象,以後對該檔案進行操作時,比如讀操作,VFS雖然對各種不同檔案都是執行同一個read()操作介面,但是真正讀時,核心
卻知道怎麼區分對待不同的檔案類型。

清單9. __dentry_open

                
static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,
int flags, struct file *f,
int (*open)(struct inode *, struct file *))
{
struct inode *inode;
……
//整個函數的工作在於填充一個file對象
……
f->f_mapping = inode->i_mapping;
f->f_dentry = dentry;
f->f_vfsmnt = mnt;
f->f_pos = 0;
//將對應的索引節點的檔案操作函數集賦予檔案對象的巨集指令清單
f->f_op = fops_get(inode->i_fop);
……
//若檔案自己定義了open操作,則執行這個特定的open操作。
if (!open && f->f_op)
open = f->f_op->open;
if (open) {
error = open(inode, f);
if (error)
goto cleanup_all;
……
return f;
}

3.2 sys_read()

sys_read()系統調用用於從已開啟的檔案讀取資料。如read成功,則返回讀到的位元組數。如已到達檔案的尾端,則返回0。圖9是sys_read()實現代碼中的函數呼叫歷程圖。

圖9. sys_read函數呼叫歷程圖

對檔案進行讀操作時,需要先開啟它。從3.1小結可知,開啟一個檔案時,會在記憶體組裝一個檔案對象,希望對該檔案執行的操作方法已在檔案對象設定好。所以
對檔案進行讀操作時,VFS在做了一些簡單的轉換後(由檔案描述符得到其對應的檔案對象;其核心思想是返回
current->files->fd[fd]所指向的檔案對象),就可以通過語句
file->f_op->read(file, buf, count, pos)輕鬆調用實際檔案系統的相應方法對檔案進行讀操作了。

跨檔案系統的檔案操作的基本原理

到此,我們也就能夠解釋在Linux中為什麼能夠跨檔案系統地操作檔案了。舉個例子,將vfat格式的磁碟上的一個檔案a.txt拷貝到ext3格式的磁
盤上,命名為b.txt。這包含兩個過程,對a.txt進行讀操作,對b.txt進行寫操作。讀寫操作前,需要先開啟檔案。由前面的分析可知,開啟檔案
時,VFS會知道該檔案對應的檔案系統格式,以後操作該檔案時,VFS會調用其對應的實際檔案系統的操作方法。所以,VFS調用vfat的讀檔案方法將
a.txt的資料讀入記憶體;在將a.txt在記憶體中的資料對應到b.txt對應的記憶體空間後,VFS調用ext3的寫檔案方法將b.txt寫入磁碟;從而
實現了最終的跨檔案系統的複製操作。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.