天天看點

深入剖析虛拟檔案系統

作者:雲原生驿站

1.前言

Linux 采用 Virtual Filesystem(VFS)的概念,通過核心在實體存儲媒體上的檔案系統和使用者之間建立起一個虛拟檔案系統的軟體抽象層,使得 Linux 能夠支援目前絕大多數的檔案系統,不論它是 windows、unix 還是其他一些系統的檔案系統,都可以挂載在 Linux 上供使用者使用。

VFS,Virtual File System虛拟檔案系統,也稱為虛拟檔案系統開關(Virtual Filesystem Switch),就是采用标準的Linux系統調用讀寫位于不同實體媒體上的不同檔案系統,即為各類檔案系統提供了一個統一的操作界面和應用程式設計接口,VFS是一個核心軟體層。

VFS是一個可以讓open()、read()、write()等系統調用不用關心底層的存儲媒體和檔案系統類型就可以工作的抽象層:

深入剖析虛拟檔案系統

2.VFS結構

這裡以Ext4檔案系統示例

深入剖析虛拟檔案系統

VFS中包含着向實體檔案系統轉換的一系列資料結構,如VFS超級塊(Super Block)、VFS的Inode、各種操作函數的轉換入口等。Linux中VFS依靠四個主要的資料結構來描述其結構資訊,分别為超級塊、索引結點、目錄項和檔案對象,這些資料結構大都會與磁盤上的對應上。

VFS對每種類型的對象都定義了一組必須實作的操作。這些類型的每一個對象都包含了一個指向函數表的指針。函數表列出了實際上實作特定對象的操作函數。
  • 超級塊(Super Block):超級塊對象表示一個檔案系統。它存儲一個已安裝的檔案系統的控制資訊,包括檔案系統名稱(比如Ext2)、檔案系統的大小和狀态、塊裝置的引用和中繼資料資訊(比如空閑清單等等)。超級塊與磁盤上檔案系統的超級塊對應。 所有超級塊對象都以雙向循環連結清單的形式連結在一起,對象的自旋鎖(sb_lock)保護連結清單免受多處理器系統上的同時通路。
  • 索引結點(Inode):索引結點對象存儲檔案的相關中繼資料資訊,例如:檔案大小、裝置辨別符、使用者辨別符、使用者組辨別符等等。Inode分為兩種:一種是VFS的Inode,一種是具體檔案系統的Inode。前者在記憶體中,後者在磁盤中。是以每次其實是将磁盤中的Inode調進填充記憶體中的Inode,這樣才是算使用了磁盤檔案Inode。當建立一個檔案的時候,就給檔案配置設定了一個Inode。一個Inode隻對應一個實際檔案,一個檔案也會隻有一個Inode(Unix/Linux系統中目錄也是一種檔案,打開目錄實際上就是打開目錄檔案。目錄檔案的結構非常簡單,就是一系列目錄項(dirent)的清單。每個目錄項,由兩部分組成:所包含檔案的檔案名,以及該檔案名對應的inode号碼)。 從檔案的角度來看,目錄就是一個特殊的檔案
  • 目錄項(Dentry):引入目錄項對象的概念主要是出于友善查找檔案的目的。不同于前面的兩個對象,目錄項對象隻存在于記憶體中,實際對應的是磁盤的目錄innode對象。VFS在查找的時候,根據一層一層的目錄項找到對應的每個目錄項的Inode,那麼沿着目錄項進行操作就可以找到最終的檔案。
  • 檔案對象(File):檔案對象描述的是程序已經打開的檔案。因為一個檔案可以被多個程序打開,是以一個檔案可以存在多個檔案對象,但多個檔案對象其對應的索引節點和目錄項對象肯定是惟一的

2.1 SuperBlock

SuperBlock 表示特定加載的檔案系統,用于描述和維護檔案系統的狀态,由 VFS 定義,但裡面的資料根據具體的檔案系統填充。每個 SuperBlock 代表了一個具體的磁盤分區,裡面包含了目前磁盤分區的資訊,如檔案系統類型、剩餘空間等。SuperBlock 的一個重要成員是連結清單s_list,包含所有修改過的 INode,使用該連結清單很容易區分出來哪個檔案被修改過,并配合核心線程将資料寫回磁盤。SuperBlock 的另一個重要成員是s_op,定義了針對其 INode 的所有操作方法,例如标記、釋放索引節點等一系列操作。

// https://elixir.bootlin.com/linux/v6.0/source/include/linux/fs.h#L1451 結構體已删減
struct super_block {
    struct list_head    s_list;               // 指向連結清單的指針
    dev_t               s_dev;                // 裝置辨別符
    unsigned long       s_blocksize;          // 以位元組為機關的塊大小
    loff_t              s_maxbytes;           // 檔案大小上限
    struct file_system_type    *s_type;       // 檔案系統類型
    const struct super_operations    *s_op;   // SuperBlock 操作函數,write_inode、put_inode 等
    const struct dquot_operations    *dq_op;  // 磁盤限額函數
    struct dentry        *s_root;             // 根目錄
}
           
深入剖析虛拟檔案系統

2.2 DEntry 和 INode

Linux檔案系統會為每個檔案都配置設定兩個資料結構,目錄項(DEntry, Directory Entry)和索引節點(INode, Index Node)。

DEntry 用來儲存檔案路徑和 INode 之間的映射,進而支援在檔案系統中移動。DEntry 由 VFS 維護,所有檔案系統共享,不和具體的程序關聯。dentry對象從根目錄“/”開始,每個dentry對象都會持有自己的子目錄和檔案,這樣就形成了檔案樹。舉例來說,如果要通路**"/home/ccs/a.txt"檔案并對他操作,系統會解析檔案路徑,首先從“/”根目錄的dentry對象開始通路,然後找到"home/“目錄,其次是“ccs/”,最後找到“a.txt”**的dentry結構體,該結構體裡面d_inode字段就對應着該檔案。

// https://elixir.bootlin.com/linux/v6.0/source/include/linux/dcache.h#L81 結構體已删減
struct dentry {
    struct dentry *d_parent;     // 父目錄
    struct qstr d_name;          // 檔案名稱
    struct inode *d_inode;       // 關聯的 inode
    struct list_head d_child;    // 父目錄中的子目錄和檔案
    struct list_head d_subdirs;  // 目前目錄中的子目錄和檔案
}
           
深入剖析虛拟檔案系統

每一個dentry對象都持有一個對應的inode對象,表示 Linux 中一個具體的目錄項或檔案。INode 包含管理檔案系統中的對象所需的所有中繼資料,以及可以在該檔案對象上執行的操作。

// https://elixir.bootlin.com/linux/v6.0/source/include/linux/fs.h#L593 結構體已删減
struct inode {
    umode_t                 i_mode;          // 檔案權限及類型
    kuid_t                  i_uid;           // user id
    kgid_t                  i_gid;           // group id

    const struct inode_operations    *i_op;  // inode 操作函數,如 create,mkdir,lookup,rename 等
    struct super_block      *i_sb;           // 所屬的 SuperBlock

    loff_t                  i_size;          // 檔案大小
    struct timespec         i_atime;         // 檔案最後通路時間
    struct timespec         i_mtime;         // 檔案最後修改時間
    struct timespec         i_ctime;         // 檔案中繼資料最後修改時間(包括檔案名稱)
    const struct file_operations    *i_fop;  // 檔案操作函數,open、write 等
    void                    *i_private;      // 檔案系統的私有資料
}
           

虛拟檔案系統維護了一個 DEntry Cache 緩存,用來儲存最近使用的 DEntry,加速查詢操作。當調用open()函數打開一個檔案時,核心會第一時間根據檔案路徑到 DEntry Cache 裡面尋找相應的 DEntry,找到了就直接構造一個file對象并傳回。如果該檔案不在緩存中,那麼 VFS 會根據找到的最近目錄一級一級地向下加載,直到找到相應的檔案。期間 VFS 會緩存所有被加載生成的dentry。

INode 存儲的資料存放在磁盤上,由具體的檔案系統進行組織,當需要通路一個 INode 時,會由檔案系統從磁盤上加載相應的資料并構造 INode。一個 INode 可能被多個 DEntry 所關聯,即相當于為某一檔案建立了多個檔案路徑(通常是為檔案建立硬連結)。

對于inode結構而言,可能有三種主要情況:

存在記憶體中,未關聯到任何檔案,也不處于活動使用狀态;

存在記憶體中,正在由一個或多個程序使用,正在由一個或多個程序使用,通常表示一個檔案。兩個計數器(i_count和i_nlink)的值都必須大于0。檔案内容和inode中繼資料都與底層塊裝置上的資訊相同。也就是表示從上一次與媒體同步依賴,該inode沒有改變過;

處于活動使用狀态。其資料内容已經改變,與存儲媒體上的内容不同。這種狀态的inode被稱作髒的。

深入剖析虛拟檔案系統

2.3 fd 與 file

每個程序都持有一個fd[]數組,數組裡面存放的是指向file結構體的指針,同一程序的不同fd可以指向同一個file對象;

file是核心中的資料結構,表示一個被程序打開的檔案,和程序相關聯。當應用程式調用open()函數的時候,VFS 就會建立相應的file對象。它會儲存打開檔案的狀态,例如檔案權限、路徑、偏移量等等。

// https://elixir.bootlin.com/linux/v6.0/source/include/linux/fs.h#L940 結構體已删減
struct file {
    struct path                   f_path;
    struct inode                  *f_inode;
    const struct file_operations  *f_op;
    unsigned int                  f_flags;
    fmode_t                       f_mode;
    loff_t                        f_pos;
    struct fown_struct            f_owner;
}

// https://elixir.bootlin.com/linux/v6.0/source/include/linux/path.h#L8
struct path {
    struct vfsmount  *mnt;
    struct dentry    *dentry;
}
           

3. 抽象層VFS到實作層檔案系統

3.1挂載

VFS可以管理各種檔案系統,那麼VFS和檔案系統怎麼關聯的呢?給使用者如何展示的呢?通過挂載。

如下圖所示,該系統根檔案系統是Ext3檔案系統,而在其/mnt目錄下面又分别挂載了Ext4檔案系統和XFS檔案系統。最後形成了一個由多個檔案系統組成的檔案系統樹。

深入剖析虛拟檔案系統

挂載是在使用者态發起的指令,也就是我們使用的mount指令,該指令執行的時候需要指定檔案系統的類型(這裡假設是Ext2)和檔案系統資料的位置(也就是device)。通過這些關鍵資訊,VFS就可以完成Ext2檔案系統的初始化,并将其關聯到目前已經存在的檔案系統當中,也就是建立起上面所示的檔案系統樹。

挂載的過程中,最重要的資料結構就是vfsmount,vfsmount代表的是一個挂載點。其次再是dentry和inode,這兩個都是對檔案的表示,且都會緩存在哈希表中以提高查找的效率。

其中inode是對磁盤上檔案的唯一表示,其中包含檔案的中繼資料(管理資料)和檔案資料等内容,但不含檔案名稱。而dentry則是為了Linux核心中查找檔案友善虛拟出來的一個資料結構,其中包含檔案名稱、子目錄(如果存在的話)和關聯的inode等資訊。

dentry結構體最為關鍵,其維護了核心中的檔案目錄樹。其中裡面比較重要的幾個結構體分别是d_name、d_hash和d_subdirs。其中d_name代表一個路徑節點的名稱(檔案夾名稱)、d_hash則用于建構哈希表,d_subdirs則是下級目錄(或檔案)的清單。這樣,通過dentry就可以形成一個非常複雜的目錄樹。

3.2檔案處理流程

檔案處理流程包括兩步:我們在通路一個檔案之前首先要打開它(open)檔案通路,然後進行檔案的讀寫操作(read或者write)。

我們知道,在使用者态打開一個檔案是傳回的是一個檔案描述符,其實也就是一個整數值;同時,通路檔案也是通過這個檔案描述符進行的。那麼作業系統是怎麼通過這個整數值實作不同類型檔案系統的通路呢?不同檔案系統的差異其實就是inode中初始化的函數指針的差異。

在Linux作業系統中,檔案的打開必須要與程序(或者線程)關聯,也就是說一個打開的檔案必須隸屬于某個程序。

在linux核心當中一個程序通過task_struct結構體描述,而打開的檔案則用file結構體描述,打開檔案的過程也就是對file結構體的初始化的過程。在打開檔案的過程中會将inode部分關鍵資訊填充到file中,特别是檔案操作的函數指針。在task_struct中儲存着一個file類型的數組,而使用者态的檔案描述符其實就是數組的下标。這樣通過檔案描述符就可以很容易到找到file,然後通過其中的函數指針通路資料。

我們以Ext2檔案系統的寫資料為例來看看檔案處理流程和各個層級之間的關系,如下圖。

深入剖析虛拟檔案系統

在調用使用者态的寫資料接口的時候,需要傳入檔案描述符。核心根據檔案描述符找到file,然後調用函數接口(file->f_op->write)檔案磁盤資料。其中file結構體的f_op指針就是在打開檔案的時候通過inode初始化的。

4.總結

虛拟檔案系統是作業系統中非常重要的一層抽象,其主要作用在于讓上層的軟體,能夠用統一的方式,與底層不同的檔案系統溝通。在作業系統與底層的各類檔案系統之間,虛拟檔案系統提供了标準的操作接口,讓作業系統能夠很快地支援新的檔案系統。也因為 VFS 的支援,衆多不同的實際檔案系統才能在 Linux 中共存,跨檔案系統的操作才能實作。

附上一張各元件互動圖吧
深入剖析虛拟檔案系統

參考資料:

《深入了解Linux核心》第三版

《Linux 虛拟檔案系統》