檔案系統考古4:如何支援多個檔案系統

2023-07-07 12:00:50

Steve Kleiman 在 1986 年撰寫了《Vnodes: An Architecture for Multiple File System Types in Sun UNIX》一文。這篇論文幅較短,大部分內容是資料結構的列舉,以及 C 語言結構之間相互指向的圖表。

Steve Kleiman是分散式檔案系統領域的專家,在 Sun Microsystem 工作了多年,曾參與開發 Sun Network File System(NFS)等專案,為分散式檔案系統領域做出了重要貢獻。

Kleiman 希望在 Unix 中能夠擁有多個檔案系統,並希望這些檔案系統能夠共用介面和記憶體。具體而言,他希望設計一個能夠提供以下功能的架構:

  • 一個可以支援多個實現的通用介面;
  • 支援 BSD FFS,以及兩個遠端檔案系統 NFS 和 RFS,還有特定的非 Unix 檔案系統,如MS-DOS;
  • 介面定義的操作需要是原子性的。

並且,能夠在不影響效能的情況下動態地處理記憶體和資料結構,支援重入(reentrant) 和多核,並且具有一定物件導向進行程式設計的特性。

重入(reentrant) 是指程式或子程式在尚未完成上一次呼叫之前,可以再次被呼叫且不會出錯或發生衝突。

兩個抽象概念

Steven 研究了檔案系統的各種操作,決定將他們抽象為兩個概念

  • vfs,虛擬檔案系統,代表檔案系統
  • vnode,虛擬 inode,代表檔案

vfs,虛擬檔案系統,它提供統一的介面,使作業系統可以以一致的方式存取不同的檔案系統,無論是本地檔案系統還是網路檔案系統。

vnode,虛擬 inode, 表示一個檔案,每個檔案都有一個相關聯的索引節點,其中包含了檔案的後設資料(如檔案許可權、所有者、大小等)以及指向檔案資料儲存位置的指標。

採用了 C++風格(實際使用 C 語言),每一個型別會匹配一個虛擬函式表,通過虛擬函式表,系統在執行時根據物件的實際型別來呼叫適當的虛擬函式,實現動態繫結:

  • 對於 vfs 型別,其虛擬函式表 struct vfsops,包含了一系列的函數指標,用來執行諸如 mount、unmount、sync 和 vget 等操作。在論文的後面,會解釋這些函數的原型和功能;
  • 對於 vnode 型別也是類似的,其虛擬函式表 struct vnodeops,包含 open、rdwr 和 close 等函數,還有create、unlink 和 rename 等函數。一些函數是針對特定的檔案型別的,比如 readlink、mkdir、readdir 和 rmdir。

通過 vfs 物件來進行跟蹤實際的掛載,其虛擬函式表 struct vfsops 指向適用於該特定子樹的檔案系統操作。

類似地,vnode 範例用來進行跟蹤開啟的檔案。它包含 struct *vnodeops 指標,作為 vfs 的一部分,有指標 struct *vfs 指向檔案系統範例。

vfs 和 vnode 這兩個結構體都需要一些用於儲存特定實現資料的欄位(如「子類私有欄位」)。他們都以 caddr_t ...data 指標結尾。這些私有資料並不是 vfs 和 vnode 的一部分,而是位於其他位置,並通過指標進行參照。

Vnodes 實操

在論文中,有一整頁的內容專門用於展示各種相互指向的結構。乍一看可能會感到困惑,但一旦追蹤下來,就會發現它非常直觀和優雅。

Kleiman 詳細解釋瞭如何使用 lookuppn() 函數來解釋事物的工作原理,該函數替代了傳統 Unix 中的 namei() 函數。類似於 namei() ,這個函數接受一個路徑,並返回表示該路徑所代表的 vnode 的 struct vnode 指標。

路徑遍歷始於根 vnode 或當前程序的當前目錄 vnode,具體取決於路徑的第一個字元是否為/。

然後,這個函數會依次取出路徑的每一個子項,並呼叫當前 vnode 的 lookup 函數,它接受一個路徑子項和一個假設是目錄的當前 vnode,並返回代表那個子項的 vnode。

當一個目錄是個掛載點,它的 vfsmountedhere 會被設定為一個指向 struct vfs 的指標。lookuppn 函數會跟隨這個指標,並呼叫 vfs 的根函數,以獲取該檔案系統的根 vnode,替換當前正在處理的 vnode。

反過來也是可能的:當解析父目錄(".. ")時,如果當前 vnode 的 "flags" 欄位中設定了根標誌,我們會跟隨 vfsmountedhere 指標從當前 vnode 到 vfs。然後,我們可以使用該 vfs 中的 vnodecovered 欄位來獲取上層檔案系統的 vnode。

無論如何,在成功完成後,會返回一個 struct vnode 指標,即所使用的路徑。

新增的系統呼叫

為了使系統高效地執行,需要新增一些新的系統呼叫來完善介面。

在 Unix 的歷史中,我們看到引入了 statsfs 和 fstatsfs ,通過這兩個函數可以獲得與使用者空間中的檔案系統進行互動的介面。getdirentries 函數可以讓使用者一次性獲取多個目錄條目(取決於提供的緩衝區大小),這大大加快了遠端檔案系統的目錄讀取速度。

在 Linux 系統中

通過檢視 Linux 核心原始碼,我們可以找到 Kleiman 設計的總體結構,儘管 Linux 核心的複雜性和豐富性掩蓋了其中大部分內容。Linux 核心擁有豐富的檔案系統型別,並且還新增了許多在 40 年前的 BSD 中不存在的功能。因此,我們可以找到更多的資料結構和系統呼叫,它們被用於實現名稱空間、配額、屬性、唯讀模式、目錄名稱快取等功能。

檔案

如果你仔細觀察,原始的結構仍然可以找到:Linux 記憶體中的檔案相關結構分為兩部分,一個是已開啟的檔案,它是一個帶有當前位置的 inode;另一個是 inode,它代表整個檔案。

我們可以在此處找到[檔案物件](](https://github.com/torvalds/linux/blob/v6.3/include/linux/fs.h),struct file 的範例。在檔案的所有其他內容中,最值得注意的是一個欄位 loff_t f_pos,它表示檔案當前位置距離檔案起始位置的偏移量(以位元組為單位)。

檔案的類是通過一個虛擬函式表來定義。我們可以找到一個指標 struct file_operations *f_op 。它展示了檔案可以執行的所有操作,其中最常見的是開啟(open)、關閉(close)、定位(lseek)、讀取(read)和寫入(write)。

檔案還包含指向 inode 的指標,即 struct inode *f_inode

索引節點

對於不需要偏移量的檔案操作,它們是針對整個檔案進行的,定義為 struct inode *

檢視此處的定義。我們可以看到這裡還有其他的定義,40 年前的 BSD 中沒有類似的定義,比如 ACL(存取控制列表)和屬性(attributes)。

我們發現 inode 的類通過虛擬函式表來定義,即 struct inode_operations *i_op。同樣的,這其中很多函數涉及新特性,比如 ACL(存取控制列表)和擴充套件屬性,但我們也會找到我們期望的功能,比如連結(link)、刪除(unlink)、重新命名(rename)等。

Inode 還包含一個指向檔案系統的指標,即 struct super_block *i_sb

超級塊

掛載點用 struct super_block 來表示,在此處檢視其定義。同樣地,它有 struct super_operations *s_op 定義的各個操作,在此處檢視其定義。

支援的檔案系統不再有限,可以通過核心模組動態地新增新的檔案系統,通過資料結構 struct file_system_type 來表示,它只有一個用於建立 superblock 的工廠函數 mount。

小結

Unix 發生了變化。它的執行時變得更加複雜,增加了許多新的功能,並增加了系統呼叫。系統變得更有結構。

但是,由 Steve Kleiman 和 Bill Joy(BSD 作業系統的共同創始人之一) 構思的原始設計和資料結構仍然存在,在當前的 Linux 系統中仍然可以找到,雖然已經過去了40年。

回顧