虛擬文件系統(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
評論
查看更多