本文闡述 Linux 中的文件系統部分,源代碼來自基於 IA32 的 2.4.20 內核。整體上說 Linux 下的文件系統主要可分爲三大塊:一是上層的文件系統的系統調用,二是虛擬文件系統 VFS(Virtual Filesystem Switch),三是掛載到 VFS 中的各實際文件系統,例如 ext2,jffs 等。本文側重於經過具體的代碼分析來解釋 Linux 內核中 VFS 的內在機制,在這過程當中會涉及到上層文件系統調用和下層實際文件系統的如何掛載。文章試圖從一個比較高的角度來解釋 Linux 下的 VFS 文件系統機制。node
3 評論: linux
Ricard Chen (ricard_chen@yahoo.com)
安全
XML error: Please enter a value for the author element's jobtitle attribute, or the company-name element, or both. |
關閉 [x]數據結構
Ricard Chen,男,感興趣的領域:Linux 系統內核,BIOS,文件系統,XScale 等。讀者能夠經過email: ricard_chen@yahoo.com 和他聯繫。函數
2005 年 4 月 01 日oop
本文闡述 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 等數據結構所表示的意義,這樣再來閱讀本文以便加深理解。
VFS 是一種軟件機制,也許稱它爲 Linux 的文件系統管理者更確切點,與它相關的數據結構只存在於物理內存當中。因此在每次系統初始化期間,Linux 都首先要在內存當中構造一棵 VFS 的目錄樹(在 Linux 的源代碼裏稱之爲 namespace),實際上即是在內存中創建相應的數據結構。VFS 目錄樹在 Linux 的文件系統模塊中是個很重要的概念,但願讀者不要將其與實際文件系統目錄樹混淆,在筆者看來,VFS 中的各目錄其主要用途是用來提供實際文件系統的掛載點,固然在 VFS 中也會涉及到文件級的操做,本文不闡述這種狀況。下文提到目錄樹或目錄,若是不特別說明,均指 VFS 的目錄樹或目錄。圖 1 是一種可能的目錄樹在內存中的影像:
這裏的文件系統是指可能會被掛載到目錄樹中的各個實際文件系統,所謂實際文件系統,便是指VFS 中的實際操做最終要經過它們來完成而已,並不意味着它們必定要存在於某種特定的存儲設備上。好比在筆者的 Linux 機器下就註冊有 "rootfs"、"proc"、"ext2"、"sockfs" 等十幾種文件系統。
在 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 的全局變量來指向該鏈表的表頭。
在衆多的實際文件系統中,之因此單獨介紹 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所示:
既然是樹,因此根是其賴以存在的基礎,本節闡述 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 文件系統中相應的函數接口所接管。
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 中各目錄的主要用途是爲之後掛載文件系統提供掛載點。因此真正的文件操做仍是要經過掛載後的文件系統提供的功能接口來進行。
爲了更好地理解 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 等函數都曾被調用過。
在本節中,將描述在 VFS 的目錄樹中向其中某個目錄(安裝點 mount point)上掛載(mount)一個文件系統的過程。
這一過程可簡單描述爲:將某一設備(dev_name)上某一文件系統(file_system_type)安裝到VFS目錄樹上的某一安裝點(dir_name)。它要解決的問題是:將對 VFS 目錄樹中某一目錄的操做轉化爲具體安裝到其上的實際文件系統的對應操做。好比說,若是將 hda2 上的根文件系統(假設文件系統類型爲 ext2)安裝到了前一節中新創建的 "/dev" 目錄上(此時,"/dev" 目錄就成爲了安裝點),那麼安裝成功以後應達到這樣的目的,即:對 VFS 文件系統的 "/dev" 目錄執行 "ls" 指令,該條指令應能列出 hda2 上 ext2 文件系統的根目錄下全部的目錄和文件。很顯然,這裏的關鍵是如何將對 VFS 樹中 "/dev" 的目錄操做指令轉化爲安裝在其上的 ext2 這一實際文件系統中的相應指令。因此,接下來的敘述將抓住如何轉化這一核心問題。在敘述以前,讀者不妨本身設想一下 Linux 系統會如何解決這一問題。記住:對目錄或文件的操做將最終由目錄或文件所對應的 inode 結構中的 i_op 和 i_fop 所指向的函數表中對應的函數來執行。因此,無論最終解決方案如何,均可以設想必然要經過將對 "/dev" 目錄所對應的 inode 中 i_op 和 i_fop 的調用轉換到 hda2 上根文件系統 ext2 中根目錄所對應的 inode 中 i_op 和 i_fop 的操做。
初始過程由 sys_mount() 系統調用函數發起,該函數原型聲明以下:
asmlinkage long sys_mount(char * dev_name, char * dir_name, char * type, unsigned long flags, void * data);
其中,參數 char *type 爲標識將要安裝的文件系統類型字符串,對於 ext2 文件系統而言,就是"ext2"。參數 flags 爲安裝時的模式標識數,和接下來的 data 參數同樣,本文不將其作爲重點。
爲了幫助讀者更好地理解這一過程,筆者用一個具體的例子來講明:咱們準備未來自主硬盤第 2 分區(hda2)上的 ext2 文件系統安裝到前面建立的 "/dev" 目錄中。那麼對於 sys_mount() 函數的調用便具體爲:
sys_mount("hda2","/dev ","ext2",…);
該函數在將這些來自用戶內存空間(user space)的參數拷貝到內核空間後,便調用 do_mount() 函數開始真正的安裝文件系統的工做。一樣,爲了便於敘述和講清楚主流程,接下來的說明將不嚴格按照具體的函數調用細節來進行。
do_mount() 函數會首先調用 path_lookup() 函數來獲得安裝點的相關信息,如同建立目錄過程當中敘述的那樣,該安裝點的信息最終記錄在 struct nameidata 類型的一個變量當中,爲敘述方便,記該變量爲nd。在本例中當 path_lookup() 函數返回時,nd 中記錄的信息以下:nd.entry = new_entry; nd.mnt = mnt; 這裏的變量如圖 3 和 4 中所示。
而後,do_mount() 函數會根據調用參數 flags 來決定調用如下四個函數之一:do_remount()、 do_loopback()、do_move_mount()、do_add_mount()。
在咱們當前的例子中,系統會調用 do_add_mount() 函數來向 VFS 樹中安裝點 "/dev " 安裝一個實際的文件系統。在 do_add_mount() 中,主要完成了兩件重要事情:一是得到一個新的安裝區域塊,二是將該新的安裝區域塊加入了安裝系統鏈表。它們分別是調用 do_kern_mount() 函數和 graft_tree() 函數來完成的。這裏的描述可能有點抽象,諸如安裝區域塊、安裝系統鏈表等,不過不用着急,由於它們都是筆者本身定義出來的概念,等一下到後面會有專門的圖表解釋,到時便會清楚。
do_kern_mount() 函數要作的事情,即是創建一新的安裝區域塊,具體的內容在前面的章節 VFS 目錄樹的創建中已經敘述過,這裏再也不贅述。
graft_tree() 函數要作的事情即是將 do_kern_mount() 函數返回的一 struct vfsmount 類型的變量加入到安裝系統鏈表中,同時 graft_tree() 還要將新分配的 struct vfsmount 類型的變量加入到一個hash表中,其目的咱們將會在之後看到。
這樣,當 do_kern_mount() 函數返回時,在圖 4 的基礎上,新的數據結構間的關係將如圖 5 所示。其中,紅圈區域裏面的數據結構即是被稱作安裝區域塊的東西,其中不妨稱 e2_mnt 爲安裝區域塊的指針,藍色箭頭曲線即構成了所謂的安裝系統鏈表。
在把這些函數調用後造成的數據結構關係理清楚以後,讓咱們回到本章節開始提到的問題,即將 ext2 文件系統安裝到了 "/dev " 上以後,對該目錄上的操做如何轉化爲對 ext2 文件系統相應的操做。從圖 5上看到,對 sys_mount() 函數的調用並無直接改變 "/dev " 目錄所對應的 inode (即圖中的 new_inode變量)結構中的 i_op 和 i_fop 指針,並且 "/dev " 所對應的 dentry(即圖中的 new_dentry 變量)結構仍然在 VFS 的目錄樹中,並無被從其中隱藏起來,相應地,來自 hda2 上的 ext2 文件系統的根目錄所對應的 e2_entry 也不是如當初筆者所想象地那樣將 VFS 目錄樹中的 new_dentry 取而代之,那麼這之間的轉化究竟是如何實現的呢?
請讀者注意下面的這段代碼:
while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry));
這段代碼在 link_path_walk() 函數中被調用,而 link_path_walk() 最終又會被 path_lookup() 函數調用,若是讀者閱讀過 Linux 關於文件系統部分的代碼,應該知道 path_lookup() 函數在整個 Linux 繁瑣的文件系統代碼中屬於一個重要的基礎性的函數。簡單說來,這個函數用於解析文件路徑名,這裏的文件路徑名和咱們平時在應用程序中所涉及到的概念相同,好比在 Linux 的應用程序中 open 或 read 一個文件 /home/windfly.cs 時,這裏的 /home/windfly.cs 就是文件路徑名,path_lookup() 函數的責任就是對文件路徑名中進行搜索,直到找到目標文件所屬目錄所對應的 dentry 或者目標直接就是一個目錄,筆者不想在有限的篇幅裏詳細解釋這個函數,讀者只要記住 path_lookup() 會返回一個目標目錄便可。
上面的代碼很是地不起眼,以致於初次閱讀文件系統的代碼時常常會忽略掉它,可是前文所提到從 VFS 的操做到實際文件系統操做的轉化倒是由它完成的,對 VFS 中實現的文件系統的安裝可謂功不可沒。如今讓咱們仔細剖析一下該段代碼: d_mountpoint(dentry) 的做用很簡單,它只是返回 dentry 中 d_mounted 成員變量的值。這裏的dentry 仍然仍是 VFS 目錄樹上的東西。若是 VFS 目錄樹上某個目錄被安裝過一次,那麼該值爲 1。對VFS 中的一個目錄可進行屢次安裝,後面會有例子說明這種狀況。在咱們的例子中,"/dev" 所對應的new_dentry 中 d_mounted=1,因此 while 循環中第一個條件知足。下面再來看__follow_down(&nd->mnt, &dentry)代
碼作了什麼?到此咱們應該記住,這裏 nd 中的 dentry 成員就是圖 5 中的 new_dentry,nd 中的 mnt成員就是圖 5 中的 mnt,因此咱們如今能夠把 __follow_down(&nd->mnt, &dentry) 改寫成__follow_down(&mnt, &new_dentry),接下來咱們將 __follow_down() 函數的代碼改寫(只是去處掉一些不太相關的代碼,而且爲了便於說明,在部分代碼行前加上了序號)以下:
static inline int __follow_down(struct vfsmount **mnt, struct dentry **dentry) { struct vfsmount *mounted; [1] mounted = lookup_mnt(*mnt, *dentry); if (mounted) { [2] *mnt = mounted; [3] *dentry = mounted->mnt_root; return 1; } return 0; }
代碼行[1]中的 lookup_mnt() 函數用於查找一個 VFS 目錄樹下某一目錄最近一次被 mount 時的安裝區域塊的指針,在本例中最終會返回圖 5 中的 e2_mnt。至於查找的原理,這裏粗略地描述一下。記得當咱們在安裝 ext2 文件系統到 "/dev" 時,在後期會調用 graft_tree() 函數,在這個函數裏會把圖 5 中的安裝區域塊指針 e2_mnt 掛到一 hash 表(Linux 2.4.20源代碼中稱之爲 mount_hashtable)中的某一項,而該項的鍵值就是由被安裝點所對應的 dentry(本例中爲 new_dentry)和 mount(本例中爲 mnt)所共同產生,因此天然地,當咱們知道 VFS 樹中某一 dentry 被安裝過(該 dentry 變成爲一安裝點),而要去查找其最近一次被安裝的安裝區域塊指針時,一樣由該安裝點所對應的 dentry 和 mount 來產生一鍵值,以此值去索引 mount_hashtable,天然可找到該安裝點對應的安裝區域塊指針造成的鏈表的頭指針,而後遍歷該鏈表,當發現某一安裝區域塊指針,記爲 p,知足如下條件時:
(p->mnt_parent == mnt && p->mnt_mountpoint == dentry)
P 便爲該安裝點所對應的安裝區域塊指針。當找到該指針後,便將 nd 中的 mnt 成員換成該安裝區域塊指針,同時將 nd 中的 dentry 成員換成安裝區域塊中的 dentry 指針。在咱們的例子中,e2_mnt->mnt_root成員指向 e2_dentry,也就是 ext2 文件系統的 "/" 目錄。這樣,當 path_lookup() 函數搜索到 "/dev"時,nd 中的 dentry 成員爲 e2_dentry,而再也不是原來的 new_dentry,同時 mnt 成員被換成 e2_mnt,轉化便在不知不覺中完成了。
如今考慮一下對某一安裝點屢次安裝的狀況,一樣做爲例子,咱們假設在 "/dev" 上安裝完一個 ext2文件系統後,再在其上安裝一個 ntfs 文件系統。在安裝以前,一樣會對安裝點所在的路徑調用path_lookup() 函數進行搜索,可是此次因爲在 "/dev" 目錄上已經安裝過了 ext2 文件系統,因此搜索到最後,由 nd 返回的信息是:nd.dentry = e2_dentry, nd.mnt = e2_mnt。因而可知,在第二次安裝時,安裝點已經由 dentry 變成了 e2_dentry。接下來,一樣地,系統會再分配一個安裝區域塊,假設該安裝區域塊的指針爲 ntfs_mnt,區域塊中的 dentry 爲 ntfs_dentry。ntfs_mnt 的父指針指向了e2_mnt,mnfs_mnt 中的 mnt_root 指向了表明 ntfs 文件系統根目錄的 ntfs_dentry。而後,系統經過 e2_dentry和 e2_mnt 來生成一個新的 hash 鍵值,利用該值做爲索引,將 ntfs_mnt 加入到 mount_hashtable 中,同時將 e2_dentry 中的成員 d_mounted 值設定爲 1。這樣,安裝過程便告結束。
讀者可能已經知道,對同一安裝點上的最近一次安裝會隱藏起前面的若干次安裝,下面咱們經過上述的例子解釋一下該過程:
在前後將 ext2 和 ntfs 文件系統安裝到 "/dev" 目錄以後,咱們再調用 path_lookup() 函數來對"/dev" 進行搜索,函數首先找到 VFS 目錄樹下的安裝點 "/dev" 所對應的 dentry 和 mnt,此時它發現dentry 成員中的 d_mounted 爲 1,因而它知道已經有文件系統安裝到了該 dentry 上,因而它經過 dentry 和 mnt 來生成一個 hash 值,經過該值來對 mount_hashtable 進行搜索,根據安裝過程,它應該能找到 e2_mnt 指針並返回之,同時原先的 dentry 也已經被替換成 e2_dentry。回頭再看一下前面已經提到的下列代碼: while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry)); 當第一次循環結束後, nd->mnt 已是 e2_mnt,而 dentry 則變成 e2_dentry。此時因爲 e2_dentry 中的成員 d_mounted 值爲 1,因此 while 循環的第一個條件知足,要繼續調用 __follow_down() 函數,這個函數前面已經剖析過,當它返回後 nd->mnt 變成了 ntfs_mnt,dentry 則變成了 ntfs_dentry。因爲此時 ntfs_dentry 沒有被安裝過其餘文件,因此它的成員 d_mounted 應該爲 0,循環結束。對 "/dev" 發起的 path_lookup() 函數最終返回了 ntfs 文件系統根目錄所對應的 dentry。這就是爲何 "/dev" 自己和安裝在其上的 ext2 都被隱藏的緣由。若是此時對 "/dev" 目錄進行一個 ls 命令,將返回安裝上去的 ntfs 文件系統根目錄下全部的文件和目錄。
有了前面章節 5 的基礎,理解 Linux 下根文件系統的安裝並不困難,由於無論怎麼樣,安裝一個文件系統到 VFS 中某一安裝點的過程原理畢竟都是同樣的。
這個過程大體是:首先要肯定待安裝的 ext2 文件系統的來源,其次是肯定 ext2 文件系統在 VFS中的安裝點,而後即是具體的安裝過程。
關於第一問題,Linux 2.4.20 的內核另有一大堆的代碼去解決,限於篇幅,筆者不想在這裏去具體說明這個過程,大概記住它是要解決到哪裏去找要安裝的文件系統的就能夠了,這裏咱們不妨就認爲要安裝的根文件系統就來自於主硬盤的第一分區 hda1.
關於第二個問題,Linux 2.4.20 的內核把來自於 hda1 上 ext2 文件系統安裝到了 VFS 目錄樹中的"/root" 目錄上。其實,把 ext2 文件系統安裝到 VFS 目錄樹下的哪一個安裝點並不重要(VFS 的根目錄除外),只要是這個安裝點在 VFS 樹中是存在的,而且內核對它沒有另外的用途。若是讀者喜歡,盡能夠本身在 VFS 中建立一個 "/Windows" 目錄,而後將 ext2 文件系統安裝上去做爲未來用戶進程的根目錄,沒有什麼不能夠的。問題的關鍵是要將進程的根目錄和當前工做目錄設定好,由於畢竟只用用戶進程纔去關心現實的文件系統,要知道筆者的這篇稿子但是要存到硬盤上去的。
在 Linux 下,設定一個進程的當前工做目錄是經過系統調用 sys_chdir() 進行的。初始化期間,Linux 在將 hda1 上的 ext2 文件系統安裝到了 "/root" 上後,經過調用 sys_chdir("/root") 將當前進程,也就是 init_task 進程的當前工做目錄(pwd)設定爲 ext2 文件系統的根目錄。記住此時 init_task進程的根目錄仍然是圖 3 中的 dentry,也就是 VFS 樹的根目錄,這顯然是不行的,由於之後 Linux 世界中的全部進程都由這個 init_task 進程派生出來,無一例外地要繼承該進程的根目錄,若是是這樣,意味着用戶進程從根目錄搜索某一目錄時,其實是從 VFS 的根目錄開始的,而事實上倒是從 ext2 的根文件開始搜索的。這個矛盾的解決是靠了在調用完 mount_root() 函數後,系統調用的下面兩個函數:
sys_mount(".", "/", NULL, MS_MOVE, NULL); sys_chroot(".");
其主要做用即是將 init_task 進程的根目錄轉化成安裝上去的 ext2 文件系統的根目錄。有興趣的讀者能夠自行去研究這一過程。
因此在用戶空間下,更多地狀況是隻能見到 VFS 這棵大樹的一葉,並且仍是被安裝過文件系統了的,實際上對用戶空間來講仍是不可見。我想,VFS 更多地被內核用來實現本身的功能,並以系統調用的方式提供過用戶進程使用,至於在其上實現的不一樣文件系統的安裝,也只是其中的一個功能罷了。
文件系統在整個 Linux 的內核中具備舉足輕重的地位,代碼量也很複雜繁瑣。可是由於其重要的地位,要想對 Linux 的內核有比較深刻的理解,必需要能越過文件系統這一關。固然閱讀其源代碼即是其中最好的方法,本文試圖給那些已經嘗試着去閱讀,可是目前尚有困惑的讀者畫一張 VFS 文件系統的草圖,但願能對讀者有些許啓發。可是想在如此有限的篇幅裏去闡述清楚 Linux 中整個文件系統的前因後果,是根本不現實的。並且本文也只是側重於剖析 VFS 的機制,對於象具體的文件讀寫,爲提升效率而引入的各類 buffer,hash 等內容以及文件系統的安全性方面,都沒有提到。畢竟,本文只想幫助讀者理清一個大致的脈絡,最終的理解與領悟,還得靠讀者本身去潛心研究源代碼。最後,對本文相關的任何問題或建議,都歡迎用 email 和筆者聯繫。