linux文件系統基礎--VFS的重要概念及其操作

一、概述
Linux 文件系統是相當複雜的,本文只分析虛擬文件系統的實現,對具體的文件系統不涉及。
即使是虛擬文件系統,要在一篇文章中講清楚也是不可能的,況且我自己的理解也不夠透徹。
爲什麼選擇 Linux 2.4.30?因爲可以參考《Linux 源碼情景分析》一書,減少學習難度。

二、基本概念
先介紹一些文件系統的基本概念:

1、一塊磁盤(塊設備),首先要按照某種文件系統格式(如 NTFS、EXT2)進行格式化,然後才能在其上進行創建目錄、保存文件等操作。
2、 在 Linux 中,有“安裝”文件系統和“卸載”文件系統的概念。一塊經過格式化的“塊設備”(不管是剛剛格式化完的,沒有創建任何名錄和文件;還是已經創建了目錄和文 件),只有先被“安裝”,才能融入 Linux 的文件系統中,用戶纔可以在它上面進行正常的文件操作。

3、  Linux 把目錄或普通文件,統一看成“目錄節點”。通常一個“目錄節點”具有兩個重要屬性:名稱以及磁盤上實際對應的數據。本文中,“目錄節點”有時簡稱爲“節點”
“符號鏈接”是一種特殊的目錄節點,它只有一個名稱,沒有實際數據。這個名稱指向一個實際的目錄節點。

4、  “接口結構”:在 內核代碼中,經常可以看到一種結構,其成員全部是函數指針,例如:

struct file_operations {  
  struct module *owner;  
  loff_t (*llseek) (struct file *, loff_t, int);  
  ssize_t (*read) (struct file *, char *, size_t, loff_t *);  
  ssize_t (*write) (struct file *, const char *, size_t, loff_t *);  
  int (*readdir) (struct file *, void *, filldir_t);  
  unsigned int (*poll) (struct file *, struct poll_table_struct *);  
  int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);  
  int (*mmap) (struct file *, struct vm_area_struct *);  
  int (*open) (struct inode *, struct file *);  
  int (*flush) (struct file *);  
  int (*release) (struct inode *, struct file *);  
  int (*fsync) (struct file *, struct dentry *, int datasync);  
  int (*fasync) (int, struct file *, int);  
  int (*lock) (struct file *, int, struct file_lock *);  
  ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);  
  ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);  
  ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);  
  unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);  
};  

這種結構的作用類似與 C++ 中的“接口類”,它是用 C 語言進行軟件抽象設計時最重要的工具。通過它,將一組通用的操作抽象出來,核心的代碼只針對這種“接口結構”進行操作,而這些函數的具體實現由不同的“子類”去完成。
以這個 file_operations“接口”爲例,它是“目錄節點”提供的操作接口。不同的文件系統需要提供這些函數的具體實現。

三、虛擬文件系統

什麼是虛擬文件系統(後文簡稱VFS)?
Linux 支持很多種文件系統,如 NTFS、EXT2、EXT3 等等,這些都是某種具體的文件系統的實現。

VFS 是一套代碼框架(framework),它處於文件系統的使用者與具體的文件系統之間,將兩者隔離開來。這種引入一個抽象層次的設計思想,即“上層不依賴 於具體實現,而依賴於接口;下層不依賴於具體實現,而依賴於接口”,就是著名的“依賴反轉”,它在 Linux內核中隨處可見。




VFS框架的設計,需要滿足如下需求:
1、  爲上層的用戶提供統一的文件和目錄的操作接口,如  open, read, write
2、  爲下層的具體的文件系統,定義一系列統一的操作“接口”, 如 file_operations, inode_operations, dentry_operation,而具體的文件系統必須實現這些接口,才能融入VFS框架中。

爲此,VFS 需要:
1、 定義一套文件系統的統一概念
2、 在這套概念基礎上,實現提供給上層用戶的操作接口,如 open, read, write 等
3、 提供一套機制,讓下層的具體的文件系統可融入 VFS 框架中,如文件系統的“註冊”和“安裝”

本文重點就是學習VFS的重要概念以及在此基礎上的重要操作。

四、VFS核心概念

1、 VFS 通過樹狀結構來管理文件系統,樹狀結構的任何一個節點都是“目錄節點”
2、 樹狀結構具有一個“根節點”
3、 VFS 通過“超級塊”來了解一個具體文件系統的所有需要的信息。具體文件系統必須先向VFS註冊,註冊後,VFS就可以獲得該文件系統的“超級塊”。
4、 具體文件系統可被安裝到某個“目錄節點”上,安裝後,具體文件系統纔可以被使用
5、 用戶對文件的操作,就是通過VFS 的接口,找到對應文件的“目錄節點”,然後調用該“目錄節點”對應的操作接口。

例如下圖:
1、 綠色代表“根文件系統”
2、 黃色代表某一個文件系統 XXFS
3、 根文件系統安裝到“根目錄節點”上
4、 XXFS 安裝到目錄節點B上




五、目錄節點 

1、inode 和 file_operations
1、  inode 用以描述“目錄節點” ,它描述了一個目錄節點物理上的屬性,例如大小,創建時間,修改時間、uid、gid 等
2、  file_operations 是“目錄節點”提供的操作接口。包括 open, read, wirte, ioctl, llseek, mmap 等操作的實現。
3、  inode 通過成員 i_fop 對應一個 file_operations
4、  打開文件的過程就是尋找 “目錄節點”對應的 inode 的過程
5、  文件被打開後,inode 和 file_operation 都已經在內存中建立,file_operations 的指針也已經指向了具體文件系統提供的函數,此後都文件的操作,都由這些函數來完成。

例如打開了一個普通文件 /root/file,其所在文件系統格式是 ext2,那麼,內存中結構如下:


2、dentry
本來,inode 中應該包括“目錄節點”的名稱,但由於符號鏈接的存在,導致一個物理文件可能有多個文件名,因此把和“目錄節點”名稱相關的部分從 inode 結構中分開,放在一個專門的 dentry 結構中。這樣:
1、  一個dentry 通過成員 d_inode 對應到一個 inode上,尋找 inode 的過程變成了尋找 dentry 的過程。因此,dentry 變得更加關鍵,inode 常常被 dentry 所遮掩。可以說, dentry 是文件系統中最核心的數據結構,它的身影無處不在。
2、  由於符號鏈接的存在,導致多個 dentry 可能對應到同一個 inode 上

例如,有一個符號鏈接 /tmp/abc 指向一個普通文件 /root/file,那麼 dentry 與 inode 之間的關係大致如下:





六、超級塊

1、super_block 和 super_operations

super_block 保存了文件系統的整體信息,如訪問權限;
super_operations 則是“超級塊”提供的操作接口
 

C代碼  

struct super_operations {  
  
            struct inode *(*alloc_inode)(struct super_block *sb);  
            void (*destroy_inode)(struct inode *);  
            void (*read_inode) (struct inode *);  
            void (*read_inode2) (struct inode *, void *) ;  
            void (*dirty_inode) (struct inode *);  
            void (*write_inode) (struct inode *, int);  
            void (*put_inode) (struct inode *);  
            void (*delete_inode) (struct inode *);  
            void (*put_super) (struct super_block *);  
            void (*write_super) (struct super_block *);  
            int (*sync_fs) (struct super_block *);  
            void (*write_super_lockfs) (struct super_block *);  
            void (*unlockfs) (struct super_block *);  
            int (*statfs) (struct super_block *, struct statfs *);  
            int (*remount_fs) (struct super_block *, int *, char *);  
            void (*clear_inode) (struct inode *);  
            void (*umount_begin) (struct super_block *);  
            struct dentry * (*fh_to_dentry)(struct super_block *sb, __u32 *fh, int len, int fhtype, int parent);  
            int (*dentry_to_fh)(struct dentry *, __u32 *fh, int *lenp, int need_parent);  
            int (*show_options)(struct seq_file *, struct vfsmount *);  
};  

我們通過分析“獲取一個 inode ”的過程來只理解這個“接口”中兩個成員  alloc_inode  和 read_inode 的作用。
在文件系統的操作中,經常需要獲得一個“目錄節點”對應的 inode,這個 inode 有可能已經存在於內存中了,也可能還沒有,需要創建一個新的 inode,並從磁盤上讀取相應的信息來填充。
對應的代碼是 iget()   (inlcude/linux/fs.h),過程如下:
1、  通過 iget4_locked() 獲取 inode。如果 inode 在內存中已經存在,則直接返回;否則創建一個新的 inode
2、  如果是新創建的 inode,通過 super_block->s_op->read_inode() 來填充它。也就是說,如何填充一個新創建的 inode, 是由具體文件系統提供的函數實現的。
 

C代碼  

static inline struct inode *iget(struct super_block *sb, unsigned long ino)  
{  
            struct inode *inode = iget4_locked(sb, ino, NULL, NULL);  
  
  
            if (inode && (inode->i_state & I_NEW)) {  
                        sb->s_op->read_inode(inode);  
                        unlock_new_inode(inode);  
            }  
            return inode;  
}  

 
iget4_locked()  首先在全局的 inode hash table 中尋找,如果找不到,則調用 get_new_inode() ,進而調用 alloc_inode() 來創建一個新的 inode
在 alloc_inode() 中可以看到,如果具體文件系統提供了創建 inode 的方法,則由具體文件系統來負責創建,否則採用系統默認的的創建方法。
 

C代碼  

static struct inode *alloc_inode(struct super_block *sb)  
{  
  
            static struct address_space_operations empty_aops;  
            static struct inode_operations empty_iops;  
            static struct file_operations empty_fops;  
            struct inode *inode;  
  
             if (sb->s_op->alloc_inode)  
                        inode = sb->s_op->alloc_inode(sb);  
            else {  
                        inode = (struct inode *) kmem_cache_alloc(inode_cachep, SLAB_KERNEL);  
                         if (inode)  
                                    memset(&inode->u, 0, sizeof(inode->u));  
            }  
  
             if (inode) {  
                        struct address_space * const mapping = &inode->i_data;  
                        inode->i_sb = sb;  
                        inode->i_dev = sb->s_dev;  
                        inode->i_blkbits = sb->s_blocksize_bits;  
                        inode->i_flags = 0;  
                        atomic_set(&inode->i_count, 1);  
                        inode->i_sock = 0;  
                        inode->i_op = &empty_iops;  
                        inode->i_fop = &empty_fops;  
                        inode->i_nlink = 1;  
                        atomic_set(&inode->i_writecount, 0);  
                        inode->i_size = 0;  
                        inode->i_blocks = 0;  
                        inode->i_bytes = 0;  
                        inode->i_generation = 0;  
                        memset(&inode->i_dquot, 0, sizeof(inode->i_dquot));  
                        inode->i_pipe = NULL;  
                        inode->i_bdev = NULL;  
                        inode->i_cdev = NULL;  
  
                        mapping->a_ops = &empty_aops;  
                        mapping->host = inode;  
                        mapping->gfp_mask = GFP_HIGHUSER;  
                        inode->i_mapping = mapping;  
            }  
            return inode;  
}  

super_block 是在安裝文件系統的時候創建的,後面會看到它和其它結構之間的關係。


七、 註冊文件系統

一個具體的文件系統,必須首先向VFS註冊,才能被使用。

通過register_filesystem() ,可以將一個“文件系統類型”結構 file_system_type註冊到內核中一個全局的鏈表file_systems 上。

文件系統註冊的主要目的,就是讓 VFS 創建該文件系統的“超級塊”結構。


C代碼  

struct file_system_type {  
  
            const char *name;  
            int fs_flags;  
            struct super_block *(*read_super) (struct super_block *, void *, int);  
            struct module *owner;  
            struct file_system_type * next;  
            struct list_head fs_supers;  
};  
   
int register_filesystem(struct file_system_type * fs)  
{  
            int res = 0;  
            struct file_system_type ** p;  
   
            if (!fs)  
                        return -EINVAL;  
  
            if (fs->next)  
                        return -EBUSY;  
  
            INIT_LIST_HEAD(&fs->fs_supers);  
            write_lock(&file_systems_lock);  
            p = find_filesystem(fs->name);  
  
            if (*p)  
                        res = -EBUSY;  
            else  
                        *p = fs;  
  
            write_unlock(&file_systems_lock);  
            return res;  
}  

這個結構中最關鍵的就是 read_super() 這個函數指針,它就是用於創建並設置 super_block 的目的的。
因爲安裝一個文件系統的關鍵一步就是要爲“被安裝設備”創建和設置一個 super_block,而不同的具體的文件系統的 super_block 有自己特定的信息,因此要求具體的文件系統首先向內核註冊,並提供 read_super() 的實現。

八、 安裝文件系統
1、  一個經過格式化的塊設備,只有安裝後,才能融入 Linux 的 VFS 之中。
2、  安裝一個文件系統,必須指定一個目錄作爲安裝點。
3、  一個設備可以同時被安裝到多個目錄上。
4、  如果某個目錄下原來有一些文件和子目錄,一旦將一個設備安裝到目錄下後,則原有的文件和子目錄消失。因爲這個目錄已經變成了一個安裝點。
5、  一個目錄節點下可以同時安裝多個設備。


1、“根安裝點”、“根設備”和“根文件系統”
安裝一個文件系統,除了需要“被安裝設備”外,還要指定一個“安裝點”。“安裝點”是已經存在的一個目錄節點。例如把 /dev/sda1 安裝到 /mnt/win 下,那麼 /mnt/win 就是“安裝點”。
可是文件系統要先安裝後使用。因此,要使用 /mnt/win 這個“安裝點”,必然要求它所在文件系統已也經被安裝。
也就是說,安裝一個文件系統,需要另外一個文件系統已經被安裝。
這是一個雞生蛋,蛋生雞的問題:最頂層的文件系統是如何被安裝的?
答案是,最頂層文件系統在內核初始化的時候被安裝在“根安裝點”上的,而根安裝點不屬於任何文件系統,它對應的 dentry 、inode 等結構是由內核在初始化階段憑空構造出來的。

最頂層的文件系統叫做“根文件系統”。Linux 在啓動的時候,要求用戶必須指定一個“根設備”,內核在初始化階段,將“根設備”安裝到“根安裝點”上,從而有了根文件系統。這樣,文件系統纔算準備就緒。此後,用戶就可以通過 mount 命令來安裝新的設備。
 
2、安裝連接件 vfsmount
“安裝”一個文件系統涉及“被安裝設備”和“安裝點”兩個部分,安裝的過程就是把“安裝點”和“被安裝設備”關聯起來,這是通過一個“安裝連接件”結構 vfsmount 來完成的。
vfsmount  將“安裝點”dentry 和“被安裝設備”的根目錄節點 dentry 關聯起來。
每安裝一次文件系統,會導致:
1、  創建一個 vfsmount
2、  爲“被安裝設備”創建一個 super_block,並由具體的文件系統來設置這個 super_block。(我們在“註冊文件系統”一節將再來分析這一步)
3、  爲被安裝設備的根目錄節點創建 dentry
4、  爲被安裝設備的根目錄節點創建 inode, 並由 super_operations->read_inode() 來設置此 inode
5、  將 super_block 與“被安裝設備“根目錄節點 dentry 關聯起來
6、  將 vfsmount 與“被安裝設備”的根目錄節點 dentry 關聯起來
在內核將根設備安裝到“根安裝點”上後,內存中有如下結構關係:


現在假設我們在 /mnt/win 下安裝了 /dev/sda1, /dev/sda1 下有 dir1,然後又在 dir1 下安裝了 /dev/sda2,那麼內存中就有了如下的結構關係





九、尋找目標節點
VFS 中一個最關鍵以及最頻繁的操作,就是根據路徑名尋找目標節點的 dentry 以及 inode 。
例如要打開 /mnt/win/dir1/abc 這個文件,就是根據這個路徑,找到‘abc’ 對應的 dentry ,進而得到 inode 的過程。

1、 尋找過程
尋找過程大致如下:
1、  首先找到根文件系統的根目錄節點 dentry 和 inode
2、  由這個 inode 提供的操作接口 i_op->lookup(),找到下一層節點 ‘mnt’ 的 dentry 和 inode
3、  由 ‘mnt’ 的 inode 找到 ‘win’ 的 dentry 和 inode
4、  由於 ‘win’ 是個“安裝點”,因此需要找到“被安裝設備”/dev/sda1 根目錄節點的 dentry 和 inode,只要找到 vfsmount B,就可以完成這個任務。
5、  然後由 /dev/sda1 根目錄節點的 inode 負責找到下一層節點 ‘dir1’ 的 dentry 和 inode
6、  由於 dir1 是個“安裝點”,因此需要藉助 vfsmount C 找到 /dev/sda2 的根目錄節點 dentry 和 inode
7、  最後由這個 inode 負責找到 ‘abc’ 的 dentry 和 inode
可以看到,整個尋找過程是一個遞歸的過程。
完成尋找後,內存中結構如下,其中紅色線條是尋找目標節點的路徑





現在有兩個問題:
1、在尋找過程的第一步,如何得到“根文件系統”的根目錄節點的 dentry?
答案是這個 dentry 是被保存在進程的 task_struct 中的。後面分析進程與文件系統關係的時候再說這個。
2、如何尋找 vfsmount B 和 C?
這是接下來要分析的。

2、vfsmount 之間的關係
我們知道, vfsmount A、B、C 之間形成了一種父子關係,爲什麼不根據 A 來找到 B ,根據 B 找到 C 了?
這是因爲一個文件系統可能同時被安裝到不同的“安裝點”上。
假設把 /dev/sda1 同時安裝到 /mnt/win 和 /mnt/linux 下
現在 /mnt/win/dir1 和 /mnt/linux/dir1 對應的是同一個 dentry!!!
然後,又把 /dev/sda2 分別安裝到 /mnt/win/dir1 和 /mnt/linux/dir1 下

現在, vfsmount 與 dentry 之間的關係大致如下。可以看到:
1、  現在有四個 vfsmount A, B, C, D
2、  A 和B對應着不同的安裝點 ‘win’ 和 ‘linux’,但是都指向 /dev/sda1 根目錄的 dentry
3、  C 和D 對應着這相同的安裝點 ‘dir1’,也都指向 /dev/sda2 根目錄的 dentry
4、  C 是 A 的 child, A是 C 的 parent
5、  D 是 B 的 child, B 是 D 的 parent






3、 搜索輔助結構 nameidata
在遞歸尋找目標節點的過程中,需要藉助一個搜索輔助結構 nameidata,這是一個臨時結構,僅僅用在尋找目標節點的過程中。
 

C代碼  

struct nameidata {  
  
       struct dentry *dentry;  
       struct vfsmount *mnt;  
       struct qstr last;  
       unsigned int flags;  
       int last_type;  
};  



在搜索初始化時,創建 nameidata,其中 mnt 指向 current->fs->rootmnt,dentry 指向 current->fs->root
dentry 隨着目錄節點的深入而不斷變化;
而 mnt 則在每進入一個新的文件系統後發生變化
以尋找 /mnt/win/dir1/abc 爲例
開始的時候, mnt 指向 vfsmount A,dentry 指向根設備的根目錄
隨後,dentry  先後指向 ‘mnt’ 和 ‘win’ 對應的 dentry
然後當尋找到 vfsmount B 後,mnt 指向了它,而 dentry 則指向了 /dev/sda1 根目錄的 dentry
有了這個結構,上一節的問題就可以得到解決了:
在尋找 /mnt/win/dir1/abc 的過程中,首先找到 A,接下來在要決定選 C 還是 D,因爲是從 A 搜索下來的, C 是 A 的 child,因此選擇 C 而不是 D;同樣,如果是尋找 /mnt/linux/dir1/abc,則會依次選擇 B 和D。這就是爲什麼 nameidata 中要帶着一個 vfsmount 的原因。
 

十、文件的打開與讀寫

1、 “打開文件”結構 file
一個文件每被打開一次,就對應着一個 file 結構。
我們知道,每個文件對應着一個 dentry 和 inode,每打開一個文件,只要找到對應的 dentry 和 inode 不就可以了麼?爲什麼還要引入這個 file 結構?
這是因爲一個文件可以被同時打開多次,每次打開的方式也可以不一樣。
而dentry 和 inode 只能描述一個物理的文件,無法描述“打開”這個概念。
因此有必要引入 file 結構,來描述一個“被打開的文件”。每打開一個文件,就創建一個 file 結構。

file 結構中包含以下信息:
打開這個文件的進程的 uid,pid
打開的方式
讀寫的方式
當前在文件中的位置
實際上,打開文件的過程正是建立file, dentry, inode 之間的關聯的過程。






2、文件的讀寫
文件一旦被打開,數據結構之間的關係已經建立,後面對文件的讀寫以及其它操作都變得很簡單。就是根據 fd 找到 file 結構,然後找到 dentry 和 inode,最後通過 inode->i_fop 中對應的函數進行具體的讀寫等操作即可。
 
十一、進程與文件系統的關聯
最後,來了解一下一個進程,與文件系統有哪些關聯。


1、 “打開文件”表和 files_struct結構
一個進程可以打開多個文件,每打開一個文件,創建一個 file 結構。所有的 file 結構的指針保存在一個數組中。而文件描述符正是這個數組的下標。
我記得以前剛開始學習編程的時候,怎麼都無法理解這個“文件描述符”的概念。現在從內核的角度去看,就很容易明白“文件描述符”是怎麼回事了。用戶僅僅看到一個“整數”,實際底層對應着的是 file, dentry, inode 等複雜的數據結構。
files_struct 用於管理這個“打開文件”表。
 

C代碼  

struct files_struct {  
    atomic_t count;  
    rwlock_t file_lock;    /* Protects all the below members.  Nests inside tsk->alloc_lock */  
    int max_fds;  
    int max_fdset;  
    int next_fd;  
    struct file ** fd;    /* current fd array */  
    fd_set *close_on_exec;  
    fd_set *open_fds;  
    fd_set close_on_exec_init;  
    fd_set open_fds_init;  
    struct file * fd_array[NR_OPEN_DEFAULT];  
};  

其中的 fd_arrar[] 就是“打開文件”表。
task_struct 中通過成員 files 與 files_struct 關聯起來。

2、  struct  fs_struct
task_struct 中與文件系統相關的還有另外一個成員 fs,它指向一個 fs_struct 。

C代碼  

struct fs_struct {  
  
       atomic_t count;  
       rwlock_t lock;  
       int umask;  
       struct dentry * root, * pwd, * altroot;  
       struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;  
};  

其中:
root 指向此進程的“根目錄”,通常就是“根文件系統”的根目錄 dentry
pwd 指向此進程當前所在目錄的 dentry
因此,通過 task_struct->fs->root,就可以找到“根文件系統”的根目錄 dentry,這就回答了 5.1 小節的第一個問題。
rootmnt :指向“安裝”根文件系統時創建的那個 vfsmount

pwdmnt:指向“安裝”當前工作目錄所在文件系統時創建的那個 vfsmount
這兩個域用於初始化 nameidata 結構。


3、 進程與文件系統的結構關係圖
下圖描述了進程與文件系統之間的結構關係圖:




十二、參考資料
1、《Linux 源碼情景分析》上冊
2、Linux 2.4.30 源碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章