一文讀懂 Linux 虛擬文件系統(VFS)

前言

虛擬文件系統是一個很龐大的架構,如果要分析的面面俱到,會顯得特別複雜而笨拙,讓人看着看着,就不知所云了(當然主要還是筆者太菜),所以這篇博客,以 open() 函數爲切入點,來試着分析分析 VFS 文件系統的運轉機理, 本文的代碼來源於 linux3.4.2。

基礎知識

首先我們來看一張圖:

從這張圖中,我們可以看出,系統調用函數並不是直接操作真正的文件系統,而是通過一層中間層,也就是我們說的虛擬文件系統,爲什麼要有虛擬文件系統?

linux 中常見的文件系統有三類:基於磁盤的文件系統;基於內存的文件系統;網絡文件系統,(這三類文件系統是共存於文件系統層,爲不同類型的數據提供存儲服務,這三類文件系統格式是不一樣的,也就是說如果不通過虛擬文件系統,直接對真正的文件系統進行讀取,有種類型的文件系統,你就得寫幾種相對應的讀取函數),所以說虛擬文件的出現(VFS)就是爲了通過使用同一套文件 I/O 系統 調用即可對 Linux 中的任意文件進行操作而無需考慮其所在的具體文件系統格式。

VFS 的數據結構

VFS 依靠四個主要的數據結構和一些輔助的數據結構來描述其結構信息,這些數據結構表現得就像是對象;每個主要對象中都包含由操作函數表構成的操作對象,這些操作對象描述了內核針對這幾個主要的對象可以進行的操作。

1、超級塊對象

存儲一個已安裝的文件系統的控制信息,代表一個已安裝的文件系統;每次一個實際的文件系統被安裝時, 內核會從磁盤的特定位置讀取一些控制信息來填充內存中的超級塊對象。一個安裝實例和一個超級塊對象一一對應。超級塊通過其結構中的一個域 s_type 記錄它所屬的文件系統類型。

struct super_block { //超級塊數據結構
        struct list_head s_list;                /*指向超級塊鏈表的指針*/
        ……
        struct file_system_type  *s_type;       /*文件系統類型*/
       struct super_operations  *s_op;         /*超級塊方法*/
        ……
        struct list_head         s_instances;   /*該類型文件系統*/
        ……
};

struct super_operations { //超級塊方法
        ……
        //該函數在給定的超級塊下創建並初始化一個新的索引節點對象
        struct inode *(*alloc_inode)(struct super_block *sb);
       ……
        //該函數從磁盤上讀取索引節點,並動態填充內存中對應的索引節點對象的剩餘部分
        void (*read_inode) (struct inode *);
       ……
};

2、索引節點對象

索引節點對象存儲了文件的相關信息,代表了存儲設備上的一個實際的物理文件。當一個 文件首次被訪問時,內核會在內存中組裝相應的索引節點對象,以便向內核提供對一個文件進行操 作時所必需的全部信息;這些信息一部分存儲在磁盤特定位置,另外一部分是在加載時動態填充的。

struct inode {//索引節點結構
      ……
      struct inode_operations  *i_op;     /*索引節點操作表*/
     struct file_operations   *i_fop;  /*該索引節點對應文件的文件操作集*/
     struct super_block       *i_sb;     /*相關的超級塊*/
     ……
};

struct inode_operations { //索引節點方法
     ……
     //該函數爲dentry對象所對應的文件創建一個新的索引節點,主要是由open()系統調用來調用
     int (*create) (struct inode *,struct dentry *,int, struct nameidata *);

     //在特定目錄中尋找dentry對象所對應的索引節點
     struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
     ……
};

3、目錄項對象

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

struct dentry {//目錄項結構
     ……
     struct inode *d_inode;           /*相關的索引節點*/
    struct dentry *d_parent;         /*父目錄的目錄項對象*/
    struct qstr d_name;              /*目錄項的名字*/
    ……
     struct list_head d_subdirs;      /*子目錄*/
    ……
     struct dentry_operations *d_op;  /*目錄項操作表*/
    struct super_block *d_sb;        /*文件超級塊*/
    ……
};

struct dentry_operations {
    //判斷目錄項是否有效;
    int (*d_revalidate)(struct dentry *, struct nameidata *);
    //爲目錄項生成散列值;
    int (*d_hash) (struct dentry *, struct qstr *);
    ……
};

4、文件對象

文件對象是已打開的文件在內存中的表示,主要用於建立進程和磁盤上的文件的對應關係。它由 sys_open() 現場創建,由 sys_close() 銷燬。文件對象和物理文件的關係有點像進程和程序的關係一樣。

當我們站在用戶空間來看待 VFS,我們像是隻需與文件對象打交道,而無須關心超級塊,索引節點或目錄項。因爲多個進程可以同時打開和操作 同一個文件,所以同一個文件也可能存在多個對應的文件對象。

文件對象僅僅在進程觀點上代表已經打開的文件,它 反過來指向目錄項對象(反過來指向索引節點)。一個文件對應的文件對象可能不是惟一的,但是其對應的索引節點和 目錄項對象無疑是惟一的。

struct file {
    ……
     struct list_head        f_list;        /*文件對象鏈表*/
    struct dentry          *f_dentry;       /*相關目錄項對象*/
    struct vfsmount        *f_vfsmnt;       /*相關的安裝文件系統*/
    struct file_operations *f_op;           /*文件操作表*/
    ……
};

struct file_operations {
    ……
    //文件讀操作
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ……
    //文件寫操作
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ……
    int (*readdir) (struct file *, void *, filldir_t);
    ……
    //文件打開操作
    int (*open) (struct inode *, struct file *);
    ……
};

正篇

經過基礎知識點的介紹後,我們開始來探究,當我們通 open() 嘗試去打開一個文件的時候,Linux 內部是如何找到對應的存儲在硬件上的該文件的數據。

(圖 2)

(圖 3)

首先我們來看看上面這兩張圖,files_struct 主要就是一個 file 指針數組,我們通常說的文件描述符是一個整數,而這個整數正好可以作爲下標,從而從 files_struct 中獲得 file 結構。

task_struct 爲進程描述符,代表的是打開文件的這麼一個動作,這裏我想表達的知識點:當文件第一次被打開時(打開成功),會建立起如上圖所示的聯繫,返回 fd 文件描述符就這樣和底層的存儲結構聯繫在了一起,fd 作爲文件描述符,文件作爲數據的載體,我們可以將它們理解爲密碼和保險櫃之間的關係,第一打開文件就是相當初始化時設置密碼(建立起了密碼和保險櫃的聯繫),當我們以後再需要拿取保險櫃中的東西時,只需要通過第一次設置的密碼就可以對保險櫃進程操作。

內核中,對應於每個進程都有一個文件描述符表,表示這個進程打開的所有文件。文件描述表中每一項都是一個指針,指向一個用於描述打開的文件的數據塊 ——— file 對象,file 對象中描述了文件的打開模式,讀寫位置等重要信息,當進程打開一個文件時,內核就會創建一個新的 file 對象。

需要注意的是,file 對象不是專屬於某個進程的,不同進程的文件描述符表中的指針可以指向相同的 file 對象,從而共享這個打開的文件。 file 對象有引用計數,記錄了引用這個對象的文件描述符個數,只有當引用計數爲 0 時,內核才銷燬 file 對象,因此某個進程關閉文件,不影響與之共享同 一個 file 對象的進程.

下面我們來分析具體的代碼。

應用層:

int open(const char * pathname,int oflag, mode_t mode )
    /*pathname:代表需要打開的文件的文件名;

       oflag:表示打開的標識 (只讀打開/只寫打開/讀寫打開 ...........)
       
      mode: 當新創建一個文件時,需要指定mode參數(設置權限)
     */

內核層:

當 open() 系統調用進入內核時候,最終調用的函數爲:

SYSCALL_DEFINE3(open, const char __user , filename, int, flags, int,mode)

該函數位於 fs/open.c 中,下面將會分析其具體的實現過程。

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)
{
 long ret;
 //判斷系統是否支持大文件,即判斷long的位數,如果64則表示支持大文件; 
 if (force_o_largefile())
  flags |= O_LARGEFILE;
 
 //完成主要的open工作,AT_FDCWD表示從當前目錄開始查找
 ret = do_sys_open(AT_FDCWD, filename, flags, mode);
 /* avoid REGPARM breakage on x86: */
 asmlinkage_protect(3, ret, filename, flags, mode);
 return ret;
}

該函數主要調用 do_sys_open() 來完成打開工作,do_sys_open() 的代碼分析如下。

long do_sys_open(int dfd, const char__user *filename, int flags, int mode)
{
 //將欲打開的文件名拷貝到內核中,該函數的分析見下文;
 char *tmp = getname(filename);
 int fd = PTR_ERR(tmp);

 if (!IS_ERR(tmp)) {
  //從進程的文件表中找到一個空閒的文件表指針,如果出錯,則返回,見下文說明;
  fd = fd = get_unused_fd();
  if (fd >= 0) {
   //執行打開操作,見下文說明,dfd=AT_FDCWD;
   struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);
   if (IS_ERR(f)) {
    put_unused_fd(fd);
    fd = PTR_ERR(f);
   } else {
    fsnotify_open(f);//作用是將 filp 的監控點打開,並將其添加到監控系統中
    //添加打開的文件表f到當前進程的文件表數組中,見下文說明;
    fd_install(fd, f);
   }
  }
  putname(tmp);
 }
 return fd;
}

(圖 4)

那我們反向思考一下,現在我們已經得到 fd,如何找到對應 file, 在當前進程中我們保留着文件描述符,文件描述符中(files_structs),文件描述符中又保留着文件描述表(fatable), 通過文件描述符表中 file 類型的指針數組對應的 fd 的項,我們可以找到 file

這篇文章到這裏就算結束了,還留有一個工作沒有完成,如 do_filp_open(dfd, tmp, flags, mode) 是如何得到 file?,這是一個很複雜的過程,以後有空,筆者會嘗試着去分析。

原文鏈接:
https://blog.csdn.net/KUNPLAYBOY/article/details/123191919

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/OoncSmMvQv7Smf5qsODTyw