參考:《Linux核心設計與實現》第13章 虛擬檔案系統

虛擬檔案系統(VFS)作為核心子作業系統,為使用者空間程式提供了檔案和檔案系統相關的介面。程式可以利用標準的Unix系統呼叫(如:open()、read()、write())對不同的檔案系統,甚至不同的介質上的檔案系統進行讀寫操作。它是一種抽象層,通過虛擬介面訪問檔案系統,它將各種不同的檔案系統抽象後採用統一的方式進行操作。VFS之所以能銜接各種各樣的檔案系統,是因為它定義了所有的檔案系統都支援的、基本的、概念上的介面和資料結構。同時實際檔案系統也將自身的諸如“如何開啟檔案”,“目錄是什麼”等概念在形式上與VFS的定義保持一致。 

##VFS物件及其資料結構

VFS採用的面向物件的設計思路,使用一組資料結構來代表通用檔案物件。因為核心純粹使用C程式碼實現,沒有直接利用面向物件的語言,所以核心中的資料結構都使用C語言的結構體實現,而這些結構體包含操作這些資料的函式指標,其中的操作函式由具體檔案系統實現。VFS中主要有四個主要的物件型別。每個主要物件中都包含一個操作物件,這些操作物件描述了核心針對主要物件可以使用的方法。操作物件作為一個結構體指標來實現,此結構體中包含指向操作其父物件的函式指標。對於其中許多方法來說,可以繼承使用VFS提供的通用函式,如果通用函式提供的基本功能無法滿足需要,那麼就必須使用實際檔案系統的獨有方法填充這些函式指標,使其指向檔案系統例項。四個物件之間通過指標關聯。

超級塊(super_block)物件

超級塊物件代表一個具體的已安裝檔案系統 
各種檔案系統都必須實現超級塊物件,該物件用於儲存特定檔案系統的資訊,通常對應於存放在磁碟特定扇區中的檔案系統超級塊或檔案系統控制塊。對於並非基於磁碟的檔案系統(如基於記憶體的檔案系統,如sysfs),它們會在使用現場建立超級塊並將其儲存到記憶體中。超級塊物件由super_block結構體表示,定義在中。建立、管理和撤銷超級塊物件的程式碼位於檔案fs/super.c中。超級塊物件通過alloc_super()函式建立並初始化。在檔案系統安裝時,檔案系統會呼叫該函式以便從磁碟讀取檔案系統超級塊,並且將其資訊填充到記憶體中的超級塊物件中。

超級塊操作

超級塊物件中最重要的一個域是s_op,它指向超級塊的操作函式表。超級塊操作函式表由super_operations結構體表示,定義在檔案中。該結構體中的每一項都是一個指向超級塊操作函式的指標,超級塊操作函式執行檔案系統和索引節點的低層操作。當檔案系統需要對其超級塊執行操作時,首先要在超級塊物件中尋找需要的操作方法。所有操作函式都是由VFS在程序上下文中呼叫。除了dirty_inode(),其他函式在必要時都可以阻塞。這其中的一些函式是可選的。在超級塊操作表中,檔案系統可以將不需要的函式指標設定成NULL。如果VFS發現操作函式指標是NULL,那它要麼就會呼叫通用函式執行相應操作,要麼什麼也不做,如何選擇取決於具體操作。

索引節點(inode)物件檔案

索引節點物件代表了一個具體檔案 
Unix系統將檔案的相關資訊和檔案本身這兩個概念加以區分,例如訪問控制權限、大小、擁有者、建立時間等資訊。檔案相關資訊(也叫元資料)被儲存在一個單獨的資料結構中,該結構被稱為索引節點。 
索引節點物件包含了核心在操作檔案或目錄時需要的全部資訊。對於Unix風格的檔案系統,這些資訊可以從磁碟索引節點直接讀入。如果一個檔案系統沒有索引節點,那麼,不管這些相關資訊在磁碟上是怎麼存放的,檔案系統都必須從中提取這些資訊。沒有索引節點的檔案系統通常將檔案的描述資訊作為檔案的一部分來存放,有些現代檔案系統使用資料庫來儲存檔案的資料。不管哪種情況、採用哪種方式,索引節點物件必須在記憶體中建立,以便於檔案系統使用。 
索引節點物件由inode結構體表示,它定義在檔案中。一個索引節點代表檔案系統中(但是索引節點僅當檔案被訪問時,才在記憶體中建立)的一個檔案,它也可以是裝置或管道這樣的特殊檔案。因此索引節點結構體中有一些和特殊檔案相關的項,比如i_pipe項就指向一個代表有名管道的資料結構,i_bdev指向塊裝置結構體,i_cdev指向字元裝置結構體。這三個指標被存放在一個公用體中,因為一個給定的索引節點每次只能表示三者之一(或三者均不)。 
有時,某些檔案系統可能並不能完整地包含索引節點結構體要求的所有資訊,這時,該檔案系統就可以在實現中選擇任意合適的辦法來解決這個問題。

索引節點操作

和超級塊操作一樣,索引節點物件中的inode_operations項也非常重要,因為它描述了VFS用以操作索引節點物件的所有方法,這些方法由檔案系統實現。與超級塊類似,對索引節點的操作呼叫方式如下: 
i->i_op->truncate(i) 
i指向給定的索引節點,truncate()函式是由索引節點i所在的檔案系統定義的。inode_operations結構體定義在檔案中。

目錄項(dentry)物件

VFS把目錄當作檔案對待,但由於VFS經常需要執行目錄相關的操作,路徑名查詢需要解析路徑中的每一個組成部分,不但要確保它有效,而且還需要再進一步尋找路徑中的下一個部分。為了方便查詢操作,VFS引入了目錄項的概念。每個dentry代表路徑中的一個特定部分。在路徑中(包括普通檔案在內),每一個部分都是目錄項物件。解析一個路徑並遍歷其分量絕非簡單的演練,它是耗時的、常規的字串比較過程,執行耗時、程式碼繁瑣。目錄項物件的引入使得這個過程更加簡單。目錄項也可包括安裝點。VFS在執行目錄操作(如果需要的話)會現場建立目錄項物件。 
目錄項物件由dentry結構體表示,定義在檔案中。與前面的兩個物件不同,目錄項物件沒有對應的磁碟資料結構,VFS根據字串形式的路徑名現場建立它。而且由於目錄項物件並非真正儲存在磁碟上,所以目錄項結構體沒有是否被修改的標緻(是否為髒)。

目錄項狀態

目錄項有三種有效狀態:被使用、未被使用和負狀態。 
一個被使用的目錄項對應一個有效的索引節點(即d_inode指向相應的索引節點)並且表明該物件存在一個或多個使用者(即d_count為正值)。一個目錄項牌被使用狀態,意味著它正被VFS使用並且指向有效的資料,因此不能被丟棄。 
一個未被使用的目錄項對應一個有效的索引節點,但是應指明VFS當前並未使用它(d_count為0)。該目錄項物件仍然指向一個有效的物件,而且被保留在快取中以便需要時再使用它。由於該目錄項不會過早地被撤銷,所以以後再需要它時,不必重新建立,與未快取的目錄項相比,這樣使路徑查詢更迅速。但如果要回收記憶體的話,可以撤銷未使用的目錄項。 
一個負狀態的目錄項沒有對應的有效索引節點(d_inode為NULL),因為索引節點已被刪除了,或路徑不再正確了,但是目錄項仍然保留,以便快速解析以後的路徑查詢。雖然負狀態的目錄項有些用處,但是如果有需要,可以撤銷它,因為畢竟實際上很少用到它。 
目錄項物件釋放後也可以儲存到slab物件快取中去,此時,任何VFS或檔案系統程式碼都沒有指向該目錄物件的有效引用。

目錄項快取

如果VFS層遍歷路徑名中所有的元素並將它們逐個地解析成目錄項物件,還要到達最深層目錄,這將是一件非常費力的工作,會浪費大師的時間。所以核心將目錄項物件快取在目錄項快取(dcache)中。 
目錄項快取包括三個主要部分:

  • “被使用的”目錄項鍊表。該連結串列通過索引節點物件中的i_dentry項連線相關的索引節點,因為一個給定的索引節點可能有多個連結,所以就可能有多個目錄項物件,因此用一個連結串列來連線它們。
  • “最近被使用的”雙向連結串列。該連結串列含有未被使用的和負狀態的目錄項物件。該連結串列總是在頭部插入目錄項,所以鏈頭節點的資料總比鏈尾的資料要新。當核心必須通過刪除節點項回收記憶體時,會從鏈尾刪除節點(最舊)。
  • 散列表和相應的雜湊函式用來快速地將給定路徑解析為相關目錄項物件。散列表由陣列dentry_hashtable表示,其中每一個元素都是一個指向具有相同鍵值的目錄項物件連結串列的指標。陣列的大小取決於系統中實體記憶體的大小。實際的雜湊值由d_hash()函式計算,它是核心提供給檔案系統的唯一的一個雜湊函式。查詢散列表要通過d_lookup()函式,如果該函式在dcache中發現了與其匹配的目錄項物件,則匹配的物件被返回;否則,返回NULL指標。 
    dcache在一定意義上也提供對索引節點的快取,也就是icache。和目錄項物件相關的索引節點物件不會被釋放,因為目錄項會讓相關索引節點的使用計數為正,這樣就可以確保索引節點留在記憶體中。只要目錄項被快取,其相應的索引節點也就被快取了。

目錄項操作

dentry_operation結構體了VFS操作目錄項的所有方法。該結構定義在檔案中。詳細見p224

檔案(file)物件

檔案物件表示程序已開啟的檔案,該物件(不是物理檔案)由相應的open()系統呼叫建立,由close()系統呼叫撤銷,所有這些檔案相關的呼叫實際上都是檔案操作表中定義的方法。因為多個程序可以同時開啟和操作同一個檔案,所以同一個檔案也可能存在多個對應的檔案物件。檔案物件僅僅在程序觀點上代表已開啟檔案,它反過來指向目錄項物件(反過來指向索引節點),其實只有目錄項物件才表示實際已開啟的實際檔案。雖然一個檔案對應的檔案物件不是唯一的,但對應的索引節點和目錄物件無疑是唯一的。 
檔案物件由file結構體表示,定義在中。類似於目錄項物件,檔案物件實際上沒有對應的磁碟資料。所以在結構體中沒有代表其物件是否為髒、是否需要寫回磁碟的標誌。檔案物件通過f_dentry指標指向相關的目錄項物件。目錄項會指向相關的索引節點,索引節點會記錄檔案是否為髒的。

檔案操作

與file結構體相關的操作與系統呼叫很類似,這些操作是標準Unix系統呼叫的基礎。檔案物件的操作由file_operations結構體表示,定義在檔案中。具體的檔案系統可以為每一種操作做專門的實現,或者如果存在通用操作,也可以使用通用操作。

關於ioctls

現在有三個關於ioctl的方法。unlocked_ioctl()和ioctl相同,不過前者在無大核心鎖(BKL)情況下被呼叫。因此函式的作者必須確保適當的同步。因為大核心鎖是粗粒度、低效的鎖,驅動程式應當實現unlocked_ioctl()而不是ioctl()。 
compat_ioctl()也在無大核心鎖的情況下被呼叫,但是它的目的是為64位的系統提供32位ioctl的相容方法。至於你如何實現它取決於現有的ioctl命令。早期的驅動程式隱含有確定大小的型別(如long),應該實現適用於32位應用的compat_ioctl()方法。這通常意味著把32位值轉換為64位核心中合適的型別。新驅動程式重新設計ioctl命令,應該確保所有的引數和資料都有明確大小的資料型別,在32位系統上執行32位應用是安全的,在64位系統上執行32位應用也是安全的,在64位系統上執行64位應用更是安全的。這些驅動程式可以讓compat_ioctl()函式指標和unlocked_ioctl()函式指標指向同一函式。

和檔案系統相關的資料結構

除了以上幾種VFS基礎物件外,核心還使用了另外一些標準資料結構來管理檔案系統的其他相關資料。第一個物件是file_system_type,用來描述各種特定檔案系統型別。第二個結構體是vfsmount,用來描述一個安裝檔案系統的例項。 
因為Linux支援眾多不同的檔案系統,所以核心必須由一個特殊的結構來描述每種檔案系統的功能和行為。file_system_type結構體被定義在中。每種檔案系統,不管有多少個例項被實際安裝到系統中,還是根本就沒有安裝到系統中,都只有一個file_system_type結構。當檔案系統被實際安裝時,將有一個vfsmount結構體在安裝點被建立。該結構體用來代表檔案系統的例項。vfsmount結構被定義在中。理清檔案系統和所有其他安裝點間的關係,是維護所有安裝點連結串列中最複雜的工作。所以vfsmount結構體中維護的各種連結串列就是為了能夠跟蹤這些關聯資訊。vfsmount結構還儲存了在安裝時指定的標誌資訊,該資訊儲存在mnt_flages域中。如下表:

標誌 描述
MNT_NOSUID 禁止該檔案系統的可執行檔案執行檔案設定setuid和setgid標誌
MNT_MODEV 禁止訪問該檔案系統上的裝置檔案
MNT_NOEXEC 禁止執行該檔案系統上的可執行檔案

安裝那些管理員不充分信任的移動裝置時,這些標誌很有用處。它們和其他一些很少用的標誌一起定義在中。

和程序相關的資料結構

系統中每一個程序都有自己的一組開啟的檔案,像根檔案系統、當前工作目錄、安裝點等。有三個資料結構將VFS層和系統的程序緊密聯絡在一起,它們分別是:file_struct、fs_struct和namespace結構體。 
file_struct結構體定義在檔案中,該結構體由程序描述符中的files目錄項指向。所有與單個程序(per-process)相關的資訊(如開啟的檔案及檔案描述符)都包含在其中。 
fs_struct結構由程序描述符的fs域指向。它包含和程序相關的資訊,定義在檔案中。該結構包含了當前程序的當前工作目錄(pwd)和根目錄。 
namespace結構體定義在檔案中,由程序描述符中的mmt_namespace域指向。2.4版核心以後,單程序名稱空間被加入到核心中,它捨不得每一個程序在系統中都看到唯一的安裝檔案系統——不僅是唯一的根目錄,而且是唯一的檔案系統層次結構。

上面這些資料結構都是通過程序描述符連線起來的。對多數程序來說,它們的描述符都指向唯一的files_struct和fs_struct結構體。但是,對於那些使用克隆標誌CLONE_FILES或CLONE_FS建立的程序,會共享這兩個結構體。所以多個程序描述符可能指向同一個files_struct或fs_struct結構體。每個結構體都維護一個count域作為引用計數,它防止在程序正使用該結構體時,該結構被撤銷。 
namespace結構體的使用方法卻和前兩種結構體完全不同,預設情況下,所有的程序共享同樣的名稱空間(也就是,它們都從相同的掛載表中看到同一個檔案系統層次結構)。只有在進行clone()操作時使用CLONE_NEWS標誌,才會給程序一個唯一的名稱空間結構體的拷貝。因為大多數程序都不提供這個標誌,所有程序都繼承其父程序的名稱空間。因此,在大多數系統上只有一個名稱空間,不過CLONE_NEWS標誌可以使這一功能失效。