VFS簡介

虛擬文件系統(VFS)
在我看來, "虛擬"二字主要有兩層含義:
1, 在同一個目錄結構中, 可以掛載着若干種不同的文件系統. VFS隱藏了它們的實現細節, 爲使用者提供統一的接口;
2, 目錄結構本身並不是絕對的, 每個進程可能會看到不一樣的目錄結構. 目錄結構是由"地址空間(namespace)"來描述的, 不同的進程可能擁有不同的namespace, 不同的namespace可能有着不同的目錄結構(因爲它們可能掛載了不同的文件系統).


操作已打開的文件
VFS的使用者是進程(用戶訪問文件系統總是需要啓動進程). 描述進程的task_struct結構中files指針指向了一個files_struct結構, 後者描述了進程已打開的文件集合.
files_struct結構維護了一個已打開文件所對應的file結構的指針數組, 數組下標被用作用戶程序操作已打開文件的句柄(通常稱作fd). files_struct還維護着已使用的fd位圖, 以便在需要打開文件時, 爲其分配一個未使用的fd.


file結構是一個已打開文件實例. 用戶程序通過fd操作一個已打開文件的過程比較簡單, 由fd索引到對應的file結構, 再執行file結構的f_op中對應的操作即可(比如read, write).
不同的file結構可能擁有不同的f_op, 因爲它們的文件類型不同(比如, 普通文件, socket, fifo, 等等).
而這個對應的f_op是在文件打開時被賦值的, 對於已打開的文件, 只管使用f_op中的函數即可, 不用再判斷到底這個文件是什麼類型. 而至於具體的f_op中的函數是如何實現的, 本文不作描述(實際上這一部分也是很複雜的, 參見<linux內核文件讀寫淺析>).


用戶程序操作一個已打開的文件也未必就會調用到f_op中的函數, 有些操作是隻涉及file結構本身的. 比如file結構中維護了文件的當前位置(f_pos), lseek系統調用只負責移動這個pos值.
類似f_pos, f_mode(文件的訪問模式), 等這樣的屬性, 是存放在file結構中的, 這意味着這些屬性都是跟一個已打開文件的實例相關的. 一個文件可能會打開多個實例(在一個或多個進程中), 每個實例中的這些值都有可能不同.
比如, 兩個進程同時打開同一個文件, 進行讀操作. 由於兩個實例(file結構)對應的f_pos不同, 兩個讀操作互不影響.
而有時候多個進程也會共享同一個打開文件實例, 當使用clone系統調用創建子進程時, 如果設置了CLONE_FILES標誌, 則父子進程將共享files_struct結構, 從而共享全部已打開的文件實例. 典型的例子是多線程.


打開文件
相比於對已打開文件的操作的簡單, 打開一個文件的過程卻是很複雜的. 從上面的圖中也可以看出, 操作已打開的文件只佔了很少的篇幅, 而其他的內容則都與打開文件有關.


要打開一個文件, 首先需要文件路徑, 如"dir0/dir1/file". 這個路徑被'/'拆分成多級, 每一級都是一個文件(目錄也是文件, 如dir0, dir1).
在尋找這個文件路徑的一開始, 我們需要一個起點. 如果文件路徑以'/'開頭, 則以根目錄爲起點; 否則以當前路徑爲起點.
這兩個可能的起點都保存在進程的task_struct所對應的fs_struct結構中. 每個文件在目錄結構中由目錄項(dentry)結構來表示, "起點"本身也是一個dentry結構.
我們在shell中執行cd命令時, 實際上就是改變了fs_struct結構中代表當前路徑的那個dentry.
進程也可以通過chroot系統調用來改變fs_struct結構中代表根路徑的那個dentry. 這樣一來, 這個dentry之上的那些路徑對該進程將不可見.


作爲文件的索引結構, 若干dentry描繪了一個樹型的目錄結構, 這就是用戶所看到的目錄結構. (我們暫且將其稱爲dentry樹.)
每個dentry指向一個索引節點(inode)結構, 後者纔是實際描述這個文件信息的結構. 而多個dentry可以指向同一個inode, 這樣就實現了link.


dentry中實現了一組方法(d_op), 主要是用於匹配子節點. dentry實現了一個散列表, 以便於查找子節點.
d_op可能隨文件系統類型的不同而不同, 比如, 散列方法可能不同, 節點的匹配方法也可能不同(有的文件系統文件名大小寫敏感, 有的則不).
尋找文件路徑的過程就是在這個dentry樹中不斷查找子dentry, 直到找到路徑中的最後一個dentry的過程.


雖然dentry樹描繪了文件系統的目錄結構, 但是, 這些dentry結構並不是常駐內存的. 整個目錄結構可能會非常大, 以致於內存根本裝不下.
初始狀態下, 系統中只有代表根目錄的dentry和它所指向的inode(這是在根文件系統掛載時生成的, 見下文). 此時要打開一個文件, 文件路徑中對應的節點都是不存在的, 根目錄的dentry無法找到需要的子節點(它現在還沒有子節點). 這時候就要通過inode->i_op中的lookup方法來尋找需要的inode的子節點(這往往是通過特定的文件系統類型定義的方法, 從文件系統存儲介質中去查找的。參見《linux文件系統實現淺析》), 找到以後(此時inode已被載入內存), 再創建一個dentry與之關聯上.
由這一過程可見, 其實是先有inode再有dentry. inode本身是存在於文件系統的存儲介質上的, 而dentry則是在內存中生成的. dentry的存在加速了對inode的查詢.


既然整個目錄結構可能不能全部載入內存, 在內存中生成的dentry將在無人使用時被釋放. d_count字段記錄了dentry的引用計數, 引用爲0時, dentry將被釋放.
這裏所謂的釋放dentry並不是直接銷燬並回收, 而是將dentry放入一個"最近最少使用(LRU)"隊列(與對應的超級塊相關聯). 當隊列過大, 或系統內存緊缺時, 最近最少使用的一些dentry才真正被釋放.
這個LRU隊列就像是一個緩存池, 加速了對重複的路徑的訪問. 而當dentry被真正釋放時, 它所對應的inode將被減引用. 如果引用爲0, inode也被釋放.
當尋找一個文件路徑時, 對於其中經歷的每一個節點, 有三種情況:
1, 對應的dentry引用計數尚未減爲0, 它們還在dentry樹中, 直接使用即可;
2, 如果對應的dentry不在dentry樹中, 則試圖從LRU隊列去尋找. LRU隊列中的dentry同時被散列到一個散列表中, 以便查找. 查找到需要的dentry後, 這個dentry被從LRU隊列中拿出來, 重新添加到dentry樹中;
3, 如果對應的dentry在LRU隊列中也找不到, 則只好去文件系統的存儲介質裏面查找inode了. 找到以後dentry被創建, 並添加以dentry樹中;


文件系統掛載
VFS允許多種不同的文件系統掛載在同一個目錄結構中, 文件系統掛載的路徑稱爲掛載點.
如, 磁盤有兩個分區A和B, A作爲根文件系統被掛載在"/"路徑下, 而B作爲A的子文件系統, 掛載在"/mnt/B/"下.
要完成這一掛載, A文件系統中必須有"/mnt/"這個目錄. 而不管A中有沒有"/mnt/B", 都會生成一個dentry與之對應, 但是這個dentry並不對應A中的"/mnt/B"所對應的inode(即使這個inode存在). 這個dentry中的d_mounted標記被置位, 表示這是一個掛載點.
如果在尋找文件路徑的過程中遇到這樣的一個掛載點, 則代表當前路徑的指針將從當前dentry切換到掛載的文件系統的"/"所對應的dentry. 即是說, 訪問A分區中的"/mnt/B"這個路徑時, 實際訪問到的是B分區中的"/"路徑.


文件系統使用vfsmount結構來描述, 多個掛載的文件系統也被組織成樹型結構.
vfsmount結構中有兩個指向dentry的指針, mnt_mountpoint指向其父文件系統的掛載點dentry(例如A分區中的"/mnt/B"), 而mnt_root指向本文件系統的根路徑dentry(例如B分區中的"/"). 通過這兩個指針, 可以完成上面提到的當前路徑的切換.
於是, 尋找文件路徑的過程中, 除了要記錄當前dentry, 還要記錄當前vfsmount. 如果當前dentry是一個掛載點, 則通過當前vfsmount, 找到其兒子中掛載點爲當前dentry的子vfsmount, 然後得到這個子vfsmount的mnt_root. 
可能會有多個vfsmount都掛載在同一個dentry上, 這時候, 只有其中一個vfsmount會被選中, 而其他vfsmount將被隱藏. 直到被選中的那個vfsmount被卸載後, 被隱藏的vfsmount纔可能被選中. 利用這個特點, 我們可以實現目錄的隱藏. 比如/home/kouu/secret下保存着一些不希望別人看到的文件, 可以在這個目錄上mount一下tmpfs, 以達到隱藏的目的.


子文件系統總是被掛載在父文件系統的某個dentry上, 而根文件系統則是由mnt_namespace對象來引用的. 不同的mnt_namespace可以引用不同的根文件系統, 組織不同的文件系統掛載樹, 形成不同的目錄結構.
一般而言, 新創建的進程總是與其父進程共用mnt_namespace. 而所有進程都是1號進程(init)的子孫進程, 則一般情況下所有進程都使用相同的mnt_namespace, 都生活在相同的目錄結構中.
但是在通過clone系統調用創建新進程時, 可以指定CLONE_NEWNS標誌, 爲子進程創建新的名字空間(其中就包含了mnt_namespace, 此外名字空間還有其他內容).


前面只是說某個設備被掛載, 其實掛載文件系統除了要添加相應的存儲介質的設備文件, 還要在內核中註冊文件系統類型(對應file_system_type結構)(如ext2, ext3, tmpfs). 一個文件系統總是包含設備和類型兩個要素的.
已註冊file_system_type被存儲在鏈表結構中, 通過它們註冊的名字(比如ext3)來找到它們. 它們是文件數據的解釋器, 解釋設備文件所對應的物理存儲介質中的數據.
每個文件系統都有一個超級塊(對應super_block結構), 這個超級塊通過file_system_type結構的get_sb方法從塊設備中讀出來.
而一個文件系統可以被掛載多次, 形成多個vfsmount結構. 它們都對應同一個super_block. 實際上只有文件系統第一次被掛載時, 纔會去讀它的super_block. 否則這個super_block已經是存在的, 直接引用即可.
在get_sb的過程中, 這個文件系統的根路徑所對應的inode也會從存儲介質中載入, 並創建對應的dentry. super_block->s_root就指向根路徑的dentry.


數據結構總結
最後, 我們對上面的一些數據結構及其函數指針集合進行一下整理, 這些東西實在容易讓人找不着北.


file_system_type
含義: 文件系統類型, 如ext2, ext3, 等等
創建: 內核啓動或內核模塊加載時, 爲每一種文件系統類型創建一個對應的file_system_type結構
函數: get_sb, 獲取超級塊的方法. 在註冊文件系統類型時提供


super_block
含義: 超級塊, 對應一個存儲文件的設備
創建: 文件系統掛載時, 通過對應的file_system_type->get_sb從設備中讀取, 並初始化(可見, super_block結構中一部分信息是保存在設備中的, 一部分則是在內在中初始化的)
函數: s_op, 超級塊的函數集, 主要包含對索引節點和文件系統實例的操作. file_system_type->get_sb從設備中讀取超級塊後, 用file_system_type對應的特定函數集進行初始化


inode
含義: 索引節點, 對應設備上存放的一個文件
創建: 1)在超級塊被載入時, 作爲根的inode一併被載入; 2)通過mknod調用創新新的索引節點; 3)在尋找文件路徑的過程中, 從設備中讀取, 並初始化(跟super_block一樣, inode結構中一部分信息是保存在設備中的, 一部分則是在內在中初始化的)
函數: i_op, 索引節點函數集, 主要包含對子inode的創建, 刪除等操作. f_op, 文件函數集, 主要包含對本inode的讀寫等操作. 在inode被創建後, 1)如果是特殊文件, 則根據對應文件的類型(包括塊設備, 字符設備, fifo, 等等)賦予特定的函數集(並不直接與設備和文件系統類型相關); 2)否則, 對應的文件系統類型會提供相應的函數集, 並且目錄和文件函數集很可能不同


dentry
含義: 目錄項, 尋找文件路徑的過程中使用的樹型結構, 與inode關聯
創建: inode被創建後, dentry就要被創建並初始化
函數: d_op, 目錄項函數集, 主要包含對子dentry的查詢操作. 由文件系統類型確定


file
含義: 打開文件的實例
創建: 在open調用時創建, 並與一個inode對應
函數: f_op, 文件讀寫等操作. 1)等於inode->f_op, 對於普通文件, 塊設備文件, 等; 2)由inode->f_op->open函數在文件打開時指定, 典型的情況是字符設備. 所有字符設備具有相同的inode->f_op, 在inode->f_op->open過程中, 找到對應設備驅動註冊的f_op, 賦給file->f_op



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章