歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

解析 Linux 中的 VFS 文件系統機制

本文闡述 Linux 中的文件系統部分,源代碼來自基於 IA32 的 2.4.20 內核。總體上說 Linux 下的文件系統主要可分為三大塊:一是上層的文件系統的系統調用,二是虛擬文件系統 VFS(Virtual Filesystem Switch),三是掛載到 VFS 中的各實際文件系統,例如 ext2,jffs 等。本文側重於通過具體的代碼分析來解釋 Linux 內核中 VFS 的內在機制,在這過程中會涉及到上層文件系統調用和下層實際文件系統的如何掛載。文章試圖從一個比較高的角度來解釋 Linux 下的 VFS 文件系統機制。


 

1. 摘要

本文闡述 Linux 中的文件系統部分,源代碼來自基於 IA32 的 2.4.20 內核。總體上說 Linux 下的文件系統主要可分為三大塊:一是上層的文件系統的系統調用,二是虛擬文件系統 VFS(Virtual Filesystem Switch),三是掛載到 VFS 中的各實際文件系統,例如 ext2,jffs 等。本文側重於通過具體的代碼分析來解釋 Linux 內核中 VFS 的內在機制,在這過程中會涉及到上層文件系統調用和下層實際文件系統的如何掛載。文章試圖從一個比較高的角度來解釋 Linux 下的 VFS 文件系統機制,所以在敘述中更側重於整個模塊的主脈絡,而不拘泥於細節,同時配有若干張插圖,以幫助讀者理解。

相對來說,VFS 部分的代碼比較繁瑣復雜,希望讀者在閱讀完本文之後,能對 Linux 下的 VFS 整體運作機制有個清楚的理解。建議讀者在閱讀本文前,先嘗試著自己閱讀一下文件系統的源代碼,以便建立起 Linux 下文件系統最基本的概念,比如至少應熟悉 super block, dentry, inode,vfsmount 等數據結構所表示的意義,這樣再來閱讀本文以便加深理解。

 

回頁首

2. VFS 概述

VFS 是一種軟件機制,也許稱它為 Linux 的文件系統管理者更確切點,與它相關的數據結構只存在於物理內存當中。所以在每次系統初始化期間,Linux 都首先要在內存當中構造一棵 VFS 的目錄樹(在 Linux 的源代碼裡稱之為 namespace),實際上便是在內存中建立相應的數據結構。VFS 目錄樹在 Linux 的文件系統模塊中是個很重要的概念,希望讀者不要將其與實際文件系統目錄樹混淆,在筆者看來,VFS 中的各目錄其主要用途是用來提供實際文件系統的掛載點,當然在 VFS 中也會涉及到文件級的操作,本文不闡述這種情況。下文提到目錄樹或目錄,如果不特別說明,均指 VFS 的目錄樹或目錄。圖 1 是一種可能的目錄樹在內存中的影像:


圖 1:VFS 目錄樹結構
圖 1:VFS 目錄樹結構
 

回頁首

3. 文件系統的注冊

這裡的文件系統是指可能會被掛載到目錄樹中的各個實際文件系統,所謂實際文件系統,即是指VFS 中的實際操作最終要通過它們來完成而已,並不意味著它們一定要存在於某種特定的存儲設備上。比如在筆者的 Linux 機器下就注冊有 "rootfs"、"proc"、"ext2"、"sockfs" 等十幾種文件系統。

3.1 數據結構

在 Linux 源代碼中,每種實際的文件系統用以下的數據結構表示:

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; }; 

注冊過程實際上將表示各實際文件系統的 struct file_system_type 數據結構的實例化,然後形成一個鏈表,內核中用一個名為 file_systems 的全局變量來指向該鏈表的表頭。

3.2 注冊 rootfs 文件系統

在眾多的實際文件系統中,之所以單獨介紹 rootfs 文件系統的注冊過程,實在是因為該文件系統 VFS 的關系太過密切,如果說 ext2/ext3 是 Linux 的本土文件系統,那麼 rootfs 文件系統則是 VFS 存在的基礎。一般文件系統的注冊都是通過 module_init 宏以及 do_initcalls() 函數來完成(讀者可通過閱讀module_init 宏的聲明及 arch\i386\vmlinux.lds 文件來理解這一過程),但是 rootfs 的注冊卻是通過 init_rootfs() 這一初始化函數來完成,這意味著 rootfs 的注冊過程是 Linux 內核初始化階段不可分割的一部分。

init_rootfs() 通過調用 register_filesystem(&rootfs_fs_type) 函數來完成 rootfs 文件系統注冊的,其中rootfs_fs_type 定義如下:

 struct file_system_type rootfs_fs_type = { \  name:  "rootfs", \  read_super: ramfs_read_super, \  fs_flags: FS_NOMOUNT|FS_LITTER, \  owner:  THIS_MODULE, \  }  

注冊之後的 file_systems 鏈表結構如下圖2所示:


圖 2: file_systems 鏈表結構
圖 2: file_systems 鏈表結構
 

回頁首

4. VFS 目錄樹的建立

既然是樹,所以根是其賴以存在的基礎,本節闡述 Linux 在初始化階段是如何建立根結點的,即 "/"目錄。這其中會包括掛載 rootfs 文件系統到根目錄 "/" 的具體過程。構造根目錄的代碼是在 init_mount_tree() 函數 (fs\namespace.c) 中

首先,init_mount_tree() 函數會調用 do_kern_mount("rootfs", 0, "rootfs", NULL) 來掛載前面已經注冊了的 rootfs 文件系統。這看起來似乎有點奇怪,因為根據前面的說法,似乎是應該先有掛載目錄,然後再在其上掛載相應的文件系統,然而此時 VFS 似乎並沒有建立其根目錄。沒關系,這是因為這裡我們調用的是 do_kern_mount(),這個函數內部自然會創建我們最關心也是最關鍵的根目錄(在 Linux 中,目錄對應的數據結構是 struct dentry)。

在這個場景裡,do_kern_mount() 做的工作主要是:

1)調用 alloc_vfsmnt() 函數在內存裡申請了一塊該類型的內存空間(struct vfsmount *mnt),並初始化其部分成員變量。

2) 調用 get_sb_nodev() 函數在內存中分配一個超級塊結構 (struct super_block) sb,並初始化其部分成員變量,將成員 s_instances 插入到 rootfs 文件系統類型結構中的 fs_supers 指向的雙向鏈表中。

3) 通過 rootfs 文件系統中的 read_super 函數指針調用 ramfs_read_super() 函數。還記得當初注冊rootfs 文件系統時,其成員 read_super 指針指向了 ramfs_read_super() 函數,參見圖2.

4) ramfs_read_super() 函數調用 ramfs_get_inode() 在內存中分配了一個 inode 結構 (struct inode) inode,並初始化其部分成員變量,其中比較重要的有 i_op、i_fop 和 i_sb:

inode->i_op = &ramfs_dir_inode_operations; inode->i_fop = &dcache_dir_ops; inode->i_sb = sb; 

這使得將來通過文件系統調用對 VFS 發起的文件操作等指令將被 rootfs 文件系統中相應的函數接口所接管。


圖3
圖3

5) ramfs_read_super() 函數在分配和初始化了 inode 結構之後,會調用 d_alloc_root() 函數來為 VFS的目錄樹建立起關鍵的根目錄 (struct dentry)dentry,並將 dentry 中的 d_sb 指針指向 sb,d_inode 指針指向 inode。

6) 將 mnt 中的 mnt_sb 指針指向 sb,mnt_root 和 mnt_mountpoint 指針指向 dentry,而 mnt_parent指針則指向自身。

這樣,當 do_kern_mount() 函數返回時,以上分配出來的各數據結構和 rootfs 文件系統的關系將如上圖 3 所示。圖中 mnt、sb、inode、dentry 結構塊下方的數字表示它們在內存裡被分配的先後順序。限於篇幅的原因,各結構中只給出了部分成員變量,讀者可以對照源代碼根據圖中所示按圖索骥,以加深理解。

最後,init_mount_tree() 函數會為系統最開始的進程(即 init_task 進程)准備它的進程數據塊中的namespace 域,主要目的是將 do_kern_mount() 函數中建立的 mnt 和 dentry 信息記錄在了 init_task 進程的進程數據塊中,這樣所有以後從 init_task 進程 fork 出來的進程也都先天地繼承了這一信息,在後面用sys_mkdir 在 VFS 中創建一個目錄的過程中,我們可以看到這裡為什麼要這樣做。為進程建立 namespace 的主要代碼如下:

 namespace = kmalloc(sizeof(*namespace), GFP_KERNEL);    list_add(&mnt->mnt_list, &namespace->list);  //mnt is returned by do_kern_mount()  namespace->root = mnt;  init_task.namespace = namespace;  for_each_task(p) {   get_namespace(namespace);   p->namespace = namespace;  }  set_fs_pwd(current->fs, namespace->root, namespace->root->mnt_root);  set_fs_root(current->fs, namespace->root, namespace->root->mnt_root);  

該段代碼的最後兩行便是將 do_kern_mount() 函數中建立的 mnt 和 dentry 信息記錄在了當前進程的 fs結構中。

以上講了一大堆數據結構的來歷,其實最終目的不過是要在內存中建立一顆 VFS 目錄樹而已,更確切地說, init_mount_tree() 這個函數為 VFS 建立了根目錄 "/",而一旦有了根,那麼這棵數就可以發展壯大,比如可以通過系統調用 sys_mkdir 在這棵樹上建立新的葉子節點等,所以系統設計者又將 rootfs 文件系統掛載到了這棵樹的根目錄上。關於 rootfs 這個文件系統,讀者如果看一下前面圖 2 中它的file_system_type 結構,會發現它的一個成員函數指針 read_super 指向的是 ramfs_read_super,單從這個函數名稱中的 ramfs,讀者大概能猜測出這個文件所涉及的文件操作都是針對內存中的數據對象,事實上也的確如此。從另一個角度而言,因為 VFS 本身就是內存中的一個數據對象,所以在其上的操作僅限於內存,那也是非常合乎邏輯的事。在接下來的章節中,我們會用一個具體的例子來討論如何利用 rootfs所提供的函樹為 VFS 增加一個新的目錄節點。

VFS 中各目錄的主要用途是為以後掛載文件系統提供掛載點。所以真正的文件操作還是要通過掛載後的文件系統提供的功能接口來進行。

 

回頁首

5. VFS 下目錄的建立

為了更好地理解 VFS,下面我們用一個實際例子來看看 Linux 是如何在 VFS 的根目錄下建立一個新的目錄 "/dev" 的。

要在 VFS 中建立一個新的目錄,首先我們得對該目錄進行搜索,搜索的目的是找到將要建立的目錄其父目錄的相關信息,因為"皮之不存,毛將焉附"。比如要建立目錄 /home/ricard,那麼首先必須沿目錄路徑進行逐層搜索,本例中先從根目錄找起,然後在根目錄下找到目錄 home,然後再往下,便是要新建的目錄名 ricard,那麼前面講得要先對目錄搜索,在該例中便是要找到 ricard 這個新目錄的父目錄,也就是 home 目錄所對應的信息。

當然,如果搜索的過程中發現錯誤,比如要建目錄的父目錄並不存在,或者當前進程並無相應的權限等等,這種情況系統必然會調用相關過程進行處理,對於此種情況,本文略過不提。

Linux 下用系統調用 sys_mkdir 來在 VFS 目錄樹中增加新的節點。同時為配合路徑搜索,引入了下面一個數據結構:

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

這個數據結構在路徑搜索的過程中用來記錄相關信息,起著類似"路標"的作用。其中前兩項中的 dentry記錄的是要建目錄的父目錄的信息,mnt 成員接下來會解釋到。後三項記錄的是所查找路徑的最後一個節點(即待建目錄或文件)的信息。 現在為建立目錄 "/dev" 而調用 sys_mkdir("/dev", 0700),其中參數 0700 我們不去管它,它只是限定將要建立的目錄的某種模式。sys_mkdir 函數首先調用 path_lookup("/dev", LOOKUP_PARENT, &nd);來對路徑進行查找,其中 nd 為 struct nameidata nd 聲明的變量。在接下來的敘述中,因為函數調用關系的繁瑣,為了突出過程主線,將不再嚴格按照函數的調用關系來進行描述。

path_lookup 發現 "/dev" 是以 "/" 開頭,所以它從當前進程的根目錄開始往下查找,具體代碼如下:

nd->mnt = mntget(current->fs->rootmnt); nd->dentry = dget(current->fs->root); 

記得在 init_mount_tree() 函數的後半段曾經將新建立的 VFS 根目錄相關信息記錄在了 init_task 進程的進程數據塊中,那麼在這個場景裡,nd->mnt 便指向了圖 3 中 mnt 變量,nd->dentry 便指向了圖 3 中的 dentry 變量。

然後調用函數 path_walk 接著往下查找,找到最後通過變量 nd 返回的信息是 nd.last.name="dev",nd.last.len=3,nd.last_type=LAST_NORM,至於 nd 中 mnt 和 dentry 成員,在這個場景裡還是前面設置的值,並無變化。這樣一圈下來,只是用 nd 記錄下相關信息,實際的目錄建立工作並沒有真正展開,但是前面所做的工作卻為接下來建立新的節點收集了必要的信息。

好,到此為止真正建立新目錄節點的工作將會展開,這是由函數 lookup_create 來完成的,調用這個函數時會傳入兩個參數:lookup_create(&nd, 1);其中參數 nd 便是前面提到的變量,參數1表明要建立一個新目錄。

這裡的大體過程是:新分配了一個 struct dentry 結構的內存空間,用於記錄 dev 目錄所對應的信息,該dentry 結構將會掛接到其父目錄中,也就是圖 3 中 "/" 目錄對應的 dentry 結構中,由鏈表實現這一關系。接下來會再分配一個 struct inode 結構。Inode 中的 i_sb 和 dentry 中的 d_sb 分別都指向圖 3 中的 sb,這樣看來,在同一文件系統下建立新的目錄時並不需要重新分配一個超級塊結構,因為畢竟它們都屬於同一文件系統,因此一個文件系統只對應一個超級塊。

這樣,當調用 sys_mkdir 成功地在 VFS 的目錄樹中新建立一個目錄 "/dev" 之後,在圖 3 的基礎上,新的數據結構之間的關系便如圖 4 所示。圖 4 中顏色較深的兩個矩形塊 new_inode 和 new_entry 便是在sys_mkdir() 函數中新分配的內存結構,至於圖中的 mnt,sb,dentry,inode 等結構,仍為圖 3 中相應的數據結構,其相互之間的鏈接關系不變(圖中為避免過多的鏈接曲線,忽略了一些鏈接關系,如 mnt 和 sb,dentry之間的鏈接,讀者可在圖 3 的基礎上參看圖 4)。

需要強調一點的是,既然 rootfs 文件系統被 mount 到了 VFS 樹上,那麼它在 sys_mkdir 的過程中必然會參與進來,事實上在整個過程中,rootfs 文件系統中的 ramfs_mkdir、ramfs_lookup 等函數都曾被調用過。


圖 4: 在 VFS 樹中新建一目錄 "dev"
圖 4: 在 VFS 樹中新建一目錄 "dev"
 

Copyright © Linux教程網 All Rights Reserved