首頁 > 軟體

解析 Linux 中的 VFS 檔案系統機制

2020-06-16 17:12:06

本文闡述 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 目錄樹結構

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 宏的宣告及 archi386vmlinux.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 連結串列結構

4. VFS 目錄樹的建立

既然是樹,所以根是其賴以存在的基礎,本節闡述 Linux 在初始化階段是如何建立根結點的,即 "/"目錄。這其中會包括掛載 rootfs 檔案系統到根目錄 "/" 的具體過程。構造根目錄的程式碼是在 init_mount_tree() 函數 (fsnamespace.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

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 中各目錄的主要用途是為以後掛載檔案系統提供掛載點。所以真正的檔案操作還是要通過掛載後的檔案系統提供的功能介面來進行。

更多詳情見請繼續閱讀下一頁的精彩內容http://www.linuxidc.com/Linux/2017-06/145065p2.htm


IT145.com E-mail:sddin#qq.com