VFS虛擬文件系統


一、VFS簡介

Linux 采用 Virtual Filesystem(VFS)的概念,通過內核在物理存儲介質上的文件系統和用戶之間建立起一個虛擬文件系統的軟件抽象層,使得 Linux 能夠支持目前絕大多數的文件系統,不論它是 windows、unix 還是其他一些系統的文件系統,都可以掛載在 Linux 上供用戶使用。

VFS 在 Linux 中是一個處理所有 unix 文件系統調用的軟件層,同時給不同類型的文件系統提供一個統一的接口。VFS 支持以下歸類的三種類型的文件系統:

(1) 磁盤文件系統,存儲在本地磁盤、U盤、CD等的文件系統,它包含各種不同的文件系統格式,比如 windows NTFS、VFAT,BSD 的 UFS,CD的 CD-ROM 等
(2) 網絡文件系統,它們存儲在網絡中的其他主機上,通過網絡進行訪問,例如 NFS
(3) 特殊文件系統,例如 /proc、sysfs 等

VFS 背后的思想就是建立一個通用的文件模型,使得它能兼容所有的文件系統,這個通用的文件模型是以 Linux 的文件系統 EXT 系列為模版構建的。每個特定的文件系統都需要將它物理結構轉換為通用文件模型。例如,通用文件模型中,所有的目錄都是文件,它包含文件和目錄;而在其他的文件類型中,比如 FAT,它的目錄就不屬於文件,這時,內核就會在內存中生成這樣的目錄文件,以滿足通用文件模型的要求。同時,VFS 在處理實際的文件操作時,通過指針指向特定文件的操作函數。可以認為通用文件模型是面向對象的設計,它實現了幾個文件通用模型的對象定義,而具體的文件系統就是對這些對象的實例化。通用文件模型包含下面幾個對象:

(1) superblock 存儲掛載的文件系統的相關信息
(2) inode 存儲一個特定文件的相關信息
(3) file 存儲進程中一個打開的文件的交互相關的信息
(4) dentry 存儲目錄和文件的鏈接信息
(5) 這幾個通用文件模型中的對象之間的關系如下圖所示:

 

VFS 除了提供一個統一文件系統接口,它還提供了名為 dentry cache 的磁盤緩存,用於加速文件路徑的查找過程。磁盤緩存將磁盤中文件系統的一些數據存放在系統內存中,這樣每次訪問這樣的數據就不需要操作實際的物理磁盤,可以大大提高訪問性能。

 

1. superblock

superblock 由 super_block 結構體存儲,所有的 superblock 都通過一個雙向鏈表鏈接在一起,這個鏈表的頭是 super_blocks 。通過 sget 或者 sget_fc 向這個鏈表中添加 superblock。對於 superblock 的方法存放在super_operations 數據結構中。這其中包含了對這種文件類型的各種的基礎操作,比如 write_inode & read_inode。s_dirt 標志用於指示 superblock 是否與磁盤中的 superblock 一致,如果不一致,則需要進行同步。

 

2. inode

文件系統中用於操作文件的所有信息都存儲在 inode 這樣一個數據結構中。對於一個文件而言,它的文件名只是一個標簽,是可以改變的,但是 inode 是不變的,它對應着一個真正的文件。inode 中的 i_state 用於指示當前 inode 的狀態,有下面一些比較常見的標志:

(1) I_DIRTY:與磁盤中的內容不一致,需要同步
(2) I_LOCK:在進行 IO 操作
(3) I_FREEING:這個 inode 已經被 free 了
(4) I_CLEAR:inode 中的內容已經沒有意義了
(5) I_NEW:這個 inode 是新建的,里面還沒有與磁盤中的數據進行同步

每個 inode 會被鏈接進下面三個鏈表中(通過 inode 中的 i_list 進行鏈接):

(1) unused inodes:i_count 為0,也就是沒有被引用,並且它是 no dirty 的。鏈表頭是 inode_unused
(2) in-use inodes:i_count > 0 並且是 no dirty 的,鏈表頭是 inode_in_use
(3) dirty inodes:鏈表頭是 superblock 中的 s_dirty

同時,屬於同一個文件系統的 inode 會通過 i_sb_list 字段鏈接到一個鏈表中,這個鏈表的頭存放在 superblock 中 s_inodes 字段。為了加快查找速度,inode 存放在一個 inode_hashtable 的 hash 表中,其中的 i_hash 則用於 hash 查找沖突時的鏈表。inode 的方法則存放在 inode_operations 結構數據中。


3. file

file 是用於進程與文件之間進行交互的一個數據結構,它是一個純軟件的對象,沒有關聯的磁盤內容,所以它沒有 dirty 的概念。file 中最重要的內容是文件指針,也就是當前文件的操作位置。同一個文件可能被不同的進程打開,它們都是對同一個 inode 進行操作,不過由於文件指針的不同,它們操作的位置也就不一樣。file 結構體通過一個 flip 的 slab cache 進行分配。所以 file 的數目存在一個限制,它通過 files_stat 中 max_files 指示最大的可分配的 file 的數量。in-use file 被鏈接在屬於各自文件系統的 superblock 中,鏈表的頭是 s_files 。f_count 字段則記錄着文件被引用的計數。

VFS 在操作文件時候,類似面向對象中多態概念,不同的文件系統會關聯不同的 file operations 。這樣 VFS 統一的文件操作接口存放在 file 中的 f_op 中。當打開一個文件的時候,VFS 會根據 inode 中的 i_fop 對 f_op 進行初始化,而在之后的操作中,VFS 可能會更改 f_op 中的值,也就是改變文件操作的行為。

 

4. dentry

在 VFS 的統一文件模型中,目錄也是被當作文件的,它有着對應的 inode 。每當一個目錄項被讀入內存的時候,就會通過一個 dentry 的結構數據存放,它通過 dentry_cache 的 slab cache 進行分配。

每個 dentry 可能存在下面4種狀態:

(1) free:VFS沒有在使用,只是一個空的空間
(2) unused:d_count = 0,不過里面的數據還是有效的,這樣的 dentry 會按順序被回收
(3) in-use:d_count > 0,數據有效,正在被系統所使用
(4) negative:d_count > 0,d_inode = NULL,也就是這個 dentry 沒有關聯的 inode,不過它依舊在路徑查找中被使用
(5) unused dentry 會被鏈接到一個 LRU 鏈表中,當 dentry cache 需要 shrink 的時候,就會從這個鏈表的尾部回收空間。這個 LRU 鏈表的頭是 dentry_unused ,通過 dentry 中的 d_lru 字段進行鏈接。in-use dentry 則會被鏈接到 inode 中的 i_dentry 鏈表,這是因為同一個 inode 可能存在好幾個硬鏈接。同時為了加快目錄的查找,dentry 也會被加入到名為 dentry_hashtable 的 hash 表中。

每一個進程,都有一個 fs_struct 結構數據,用於存放 root 目錄、當前目錄等文件相關的信息。還有一個 file_struct 的數據結構用於存放進程打開的文件相關信息。file_struct 中的 fd 字段是一個指向當前進程打開的文件的一個數組,這個數據的大小由 max_fds 字段指示。這個數組的默認大小是 32,如果打開的文件多於這個數,那么就會重新開辟一個大的數組進行擴展。我們在用戶態的文件描述符就是這個數組的序號,通常0號文件是標准輸入,1號文件是標准輸出,2號文件是錯誤輸出。通過 dup()、fcnt() 這樣的系統調用,我們可以讓這個數組不同的序號指向同一個文件。一個進程能打開的文件數是有限制的,在運行過程中,這個最大值由 signal->rlim[RLIMIT_NOFILE] 指示,它可以在運行時動態的修改,默認值是 1024。而在代碼中,NR_OPEN 這個宏指示着進程所能支持的最大的文件打開數。在 file_struct 中 open_fds 是一個用來標志打開文件序號的數組,這個數組中的每個 bit 表示對應序號的文件是否被打開,max_fdset 指示着這個數組的長度,它的默認值是 1024 個 bit。如果打開的文件數超過這個值,那么就需要動態擴展這個數組的大小。

內核中通過 fget 函數來引用文件,它將返回 current->files->fd[fd],同時增加 f_count 的計數,釋放文件則通過 fput 函數,它將減少 f_count 計數,如果減少到0,內核將調用 release 方法做真正的釋放動作。

 

二、文件系統類型

1. 相對於以網絡和磁盤為載體的文件系統,特殊文件系統是一些數據的集合,利用了 VFS 的接口,方便內核或者用戶的使用。常見的特殊文件系統有:rootfs、proc、sysfs、pipefs、tmpfs等。這些特殊的文件系統沒有物理載體,不過內核中把它們都統一分配一個虛擬的塊設備(編號為0)。這些特殊的文件系統通過調用 set_anon_super 函數來初始化 superblock。

2. file_system_type 是用於存放注冊到內核的文件系統類型的信息的數據結構,所有的 file_system_type 鏈接在一個單向鏈表中,表頭是 file_systems 。字段 fs_supers 則是這個文件類型下所有的 superblock 的鏈表。get_sb & kill_sb 則是用於構建和銷毀 superblock 的方法。

3. 文件系統通過調用 register_filesystem & unregister_filessytem 函數實現文件系統在內核的注冊和注銷。

4. rootfs 文件系統是內核在啟動階段注冊的,它包含了文件系統初始化的腳本以及一些重要的系統程序。其他的文件系統則可以通過腳本或者命令 mount 到系統的目錄節點中。


三、namespaces

1. 通常的 unix 系統中,只有一顆文件系統樹,都是從 rootfs 這個根開始,進程通過在這個文件樹中特定的路徑名訪問文件。而在 Linux 中,引入了 namespace 的概念,也就是每一個進程都可以有自己的一個文件系統樹。通常進程都是共享系統中的 rootfs(也就是 init 進程的 namespace )。不過如果在 clone() 系統調用的時候,設置了 CLONE_NEWNS 標志,那么這個進程就會創建一個新的 namespace,通過 pivot_root() 系統調用可以修改進程的 namespace。不同 namespace 之間的文件系統 mount & unmount 互不影響。

2. 進程中的文件系統 namespace 信息通過一個 namespace 數據結構進行存儲。list 字段是一個鏈接了所有屬於這個 namespace 的文件系統的鏈表,而 root 字段則是表示這個 namespace 的根文件系統。

3. 在 Linux 中,文件系統的 mount 具有以下一些特性:

(1) 同一個文件系統可以被多次 mount 到不同的路徑點,這樣同一個文件系統可以通過不同的路徑進行訪問,不過代表這個文件系統的 superblock 只會有一個
(2) mount 的文件系統下的路徑可以 mount 其他的文件系統,以此類推,可以形成一個 mount 的等級圖
(3) 同一個路徑點,可以被棧式地進行 mount,新的文件系統被 mount 后就會覆蓋老的文件系統的路徑,而 unmount 之后,路徑又會恢復之前一個的文件系統

用於存儲這些 mount 的關系信息的數據結構叫做 vfsmount ,vfsmount 數據被鏈接在下面這些鏈表中:

(1) 所有的 vfsmount 加入到 mount_hashtable hash表中
(2) 對於每一個 namespace ,通過一個環形鏈表,鏈接屬於自己的 vfsmount
(3) 對於每一個文件系統,通過一個環形鏈表,鏈接 mount 到自己路徑下的子文件系統 vfsmount

 

四、mount 根文件系統

1. 根文件系統的 mount 過程分為2個階段:

(1) 內核 mount 一個特殊的文件系統——rootfs,它提供了一個空的路徑作為初始的掛載點
(2) 內核 mount 實際的文件系統系統,覆蓋之前的空路徑

2. 內核之所以搞一個 rootfs 這樣一個特殊的文件系統,而不是直接使用實際的文件系統作為根文件系統,其原因是為了方便在系統運行時更換根文件系統。目前用於啟動的 Ramdisk 就是這樣一個例子,在系統起來后,先加載一個包含最小驅動文件和啟動腳本的 Ramdisk 文件系統作為根文件系統,然后將系統中其他的設備加載起來后,再選擇一個完整的文件系統替換這個最小系統。

 

五、文件路徑查找

1. 文件路徑查找的簡單過程:文件路徑通過 / 划分為一個個的目錄項,查找到匹配的目錄,然后讀取它的 inode ,尋找到符合下一級路徑的目錄,如此循環,知道最后。dentry cache 的機制可以加快對於目錄項的訪問速度。然而Linux中下面的這些特性,讓這個循環查找的過程變得非常復雜:

(1) 每一級路徑需要匹配用戶的訪問權限
(2) 一個文件名可能是任意一個路徑的軟鏈接
(3) 軟鏈接可能存在循環引用的情況,需要發現並打破這樣的無限循環
(4) 一個路徑名可以是另一個文件系統的掛載點
(5) 路徑名需要在進程的 namespace 中查找,不能超出這個范圍

2. 文件路徑的查找通過函數 path_lookup_at 實現

 

六、文件鎖

1. 當一個文件可以被多個進程同時訪問的時候,同步的問題就產生了。POSIX 標准是要求通過 fcntl() 系統調用實現一個文件鎖的機制,這樣就能避免競爭關系。這個文件鎖可以對整個文件或者文件中的某個區域(小到一個字節)進行鎖定,由於可以鎖文件的部分內容,所以一個進程可以同時獲取同一個文件的多個鎖。

2. POSIX 標准的這種文件鎖被稱之為 advisory lock,就跟用於同步的信號量一樣,只有雙方約定在訪問臨界區的時候都先查看一下鎖的狀態,這種同步機制才能實現,如果某個進程單方面不獲取鎖的狀態就直接操作臨界區,那么這個鎖是管不住的。與勸告鎖相對的就是強制鎖。

3. 在 Linux 中可以通過 fcntl() & flock() 系統調用對文件進行上鎖。通常在類 unix 系統中,flock() 系統調用會無視 MS_MANDLOCK 的掛載標志,只產生勸告鎖。

4. Linux 對於勸告鎖和強制鎖都有實現。一個進程獲取勸告鎖的方式有2種:

(1) 通過 flock() 系統調用,這個鎖只能對整個文件上鎖
(2) 通過 fcntl() 系統調用,可以文件的特定部分進行上鎖

5. 一個進程獲取強制鎖比較麻煩,需要下面幾個步驟:

(1) mount 文件系統的時候,添加強制鎖的標志 MS_MANDLOCK,也就是 mount 命令添加 -o 選項。mount 的默認情況下是不帶這個標志的
(2) 使能 Set-Group-ID,清除 Group-Execute-Bit ,chmod g+s,g-x xxx.file
(3) 使用 fcntl() 系統調用,獲得強制鎖
(4) fcntl() 系統調用還支持一種名為 lease 的強制鎖,A進程在訪問被B進程上鎖的文件時,A進程會被阻塞,同時B進程會收到一個信號,此時B進程應該盡快處理完並主動釋放文件鎖。如果在一定時間內(/proc/sys/fs/lease-break-time中配置,通常是45s)B進程還沒有釋放這個鎖,那么這個鎖就會被內核自動釋放掉,此時A進程就能繼續訪問這個文件了。

6. fcntl() 是 POSIX 標准的用於文件鎖的系統調用,而 flock() 是許多其他類 unix 系統中實現的系統調用,Linux 對這2者都支持。它們的鎖也是互不影響的,也就是通過 fcntl() 上的鎖對 flock() 是無效的,反之亦然,之所以這樣,是為了防止不同的用戶程序使用不同的接口,會導致死鎖的情況發生。

7. 由於強制鎖會帶來一些程序兼容性的問題,所以一般不鼓勵使用強制鎖,並且 Linux 中的強制鎖還存在一些 bug,會導致無法真正地實現全面的強制鎖,同時從 4.5版本開始,強制鎖已經變成一個可配置的選項,在后面的內核版本中將會移除這個特性。(詳情請看編程手冊 fcntl 頁面)


七、總結

VFS 全篇看來就那4個對象的關系圖,其次就是文件操作的接口標准,代碼部分值得一看的應該是文件路徑查找&文件系統mount的處理。而文件鎖作為一個同步機制,也是值得去研究一下它的實現的。

 

 

 

 

參考:

https://wushifublog.com/2020/05/22/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Linux%E5%86%85%E6%A0%B8%E2%80%94%E2%80%94VFS/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM