虛擬文件系統(tǒng)(VFS)
在我看來, “虛擬”二字主要有兩層含義:
1, 在同一個目錄結(jié)構(gòu)中, 可以掛載著若干種不同的文件系統(tǒng)。 VFS隱藏了它們的實現(xiàn)細節(jié), 為使用者提供統(tǒng)一的接口;
2, 目錄結(jié)構(gòu)本身并不是絕對的, 每個進程可能會看到不一樣的目錄結(jié)構(gòu)。 目錄結(jié)構(gòu)是由“地址空間(namespace)”來描述的, 不同的進程可能擁有不同的namespace, 不同的namespace可能有著不同的目錄結(jié)構(gòu)(因為它們可能掛載了不同的文件系統(tǒng))。
操作已打開的文件
VFS的使用者是進程(用戶訪問文件系統(tǒng)總是需要啟動進程)。 描述進程的task_struct結(jié)構(gòu)中files指針指向了一個files_struct結(jié)構(gòu), 后者描述了進程已打開的文件集合。
files_struct結(jié)構(gòu)維護了一個已打開文件所對應(yīng)的file結(jié)構(gòu)的指針數(shù)組, 數(shù)組下標被用作用戶程序操作已打開文件的句柄(通常稱作fd)。 files_struct還維護著已使用的fd位圖, 以便在需要打開文件時, 為其分配一個未使用的fd.
file結(jié)構(gòu)是一個已打開文件實例。 用戶程序通過fd操作一個已打開文件的過程比較簡單, 由fd索引到對應(yīng)的file結(jié)構(gòu), 再執(zhí)行file結(jié)構(gòu)的f_op中對應(yīng)的操作即可(比如read, write)。
不同的file結(jié)構(gòu)可能擁有不同的f_op, 因為它們的文件類型不同(比如, 普通文件, socket, fifo, 等等)。
而這個對應(yīng)的f_op是在文件打開時被賦值的, 對于已打開的文件, 只管使用f_op中的函數(shù)即可, 不用再判斷到底這個文件是什么類型。 而至于具體的f_op中的函數(shù)是如何實現(xiàn)的, 本文不作描述(實際上這一部分也是很復(fù)雜的, 參見《linux內(nèi)核文件讀寫淺析》)。
用戶程序操作一個已打開的文件也未必就會調(diào)用到f_op中的函數(shù), 有些操作是只涉及file結(jié)構(gòu)本身的。 比如file結(jié)構(gòu)中維護了文件的當(dāng)前位置(f_pos), lseek系統(tǒng)調(diào)用只負責(zé)移動這個pos值。
類似f_pos, f_mode(文件的訪問模式), 等這樣的屬性, 是存放在file結(jié)構(gòu)中的, 這意味著這些屬性都是跟一個已打開文件的實例相關(guān)的。 一個文件可能會打開多個實例(在一個或多個進程中), 每個實例中的這些值都有可能不同。
比如, 兩個進程同時打開同一個文件, 進行讀操作。 由于兩個實例(file結(jié)構(gòu))對應(yīng)的f_pos不同, 兩個讀操作互不影響。
而有時候多個進程也會共享同一個打開文件實例, 當(dāng)使用clone系統(tǒng)調(diào)用創(chuàng)建子進程時, 如果設(shè)置了CLONE_FILES標志, 則父子進程將共享files_struct結(jié)構(gòu), 從而共享全部已打開的文件實例。 典型的例子是多線程。
打開文件
相比于對已打開文件的操作的簡單, 打開一個文件的過程卻是很復(fù)雜的。 從上面的圖中也可以看出, 操作已打開的文件只占了很少的篇幅, 而其他的內(nèi)容則都與打開文件有關(guān)。
要打開一個文件, 首先需要文件路徑, 如“dir0/dir1/file”。 這個路徑被‘/’拆分成多級, 每一級都是一個文件(目錄也是文件, 如dir0, dir1)。
在尋找這個文件路徑的一開始, 我們需要一個起點。 如果文件路徑以‘/’開頭, 則以根目錄為起點; 否則以當(dāng)前路徑為起點。
這兩個可能的起點都保存在進程的task_struct所對應(yīng)的fs_struct結(jié)構(gòu)中。 每個文件在目錄結(jié)構(gòu)中由目錄項(dentry)結(jié)構(gòu)來表示, “起點”本身也是一個dentry結(jié)構(gòu)。
我們在shell中執(zhí)行cd命令時, 實際上就是改變了fs_struct結(jié)構(gòu)中代表當(dāng)前路徑的那個dentry.
進程也可以通過chroot系統(tǒng)調(diào)用來改變fs_struct結(jié)構(gòu)中代表根路徑的那個dentry. 這樣一來, 這個dentry之上的那些路徑對該進程將不可見。
作為文件的索引結(jié)構(gòu), 若干dentry描繪了一個樹型的目錄結(jié)構(gòu), 這就是用戶所看到的目錄結(jié)構(gòu)。 (我們暫且將其稱為dentry樹。)
每個dentry指向一個索引節(jié)點(inode)結(jié)構(gòu), 后者才是實際描述這個文件信息的結(jié)構(gòu)。 而多個dentry可以指向同一個inode, 這樣就實現(xiàn)了link.
dentry中實現(xiàn)了一組方法(d_op), 主要是用于匹配子節(jié)點。 dentry實現(xiàn)了一個散列表, 以便于查找子節(jié)點。
d_op可能隨文件系統(tǒng)類型的不同而不同, 比如, 散列方法可能不同, 節(jié)點的匹配方法也可能不同(有的文件系統(tǒng)文件名大小寫敏感, 有的則不)。
尋找文件路徑的過程就是在這個dentry樹中不斷查找子dentry, 直到找到路徑中的最后一個dentry的過程。
雖然dentry樹描繪了文件系統(tǒng)的目錄結(jié)構(gòu), 但是, 這些dentry結(jié)構(gòu)并不是常駐內(nèi)存的。 整個目錄結(jié)構(gòu)可能會非常大, 以致于內(nèi)存根本裝不下。
初始狀態(tài)下, 系統(tǒng)中只有代表根目錄的dentry和它所指向的inode(這是在根文件系統(tǒng)掛載時生成的, 見下文)。 此時要打開一個文件, 文件路徑中對應(yīng)的節(jié)點都是不存在的, 根目錄的dentry無法找到需要的子節(jié)點(它現(xiàn)在還沒有子節(jié)點)。 這時候就要通過inode-》i_op中的lookup方法來尋找需要的inode的子節(jié)點(這往往是通過特定的文件系統(tǒng)類型定義的方法, 從文件系統(tǒng)存儲介質(zhì)中去查找的。參見《linux文件系統(tǒng)實現(xiàn)淺析》), 找到以后(此時inode已被載入內(nèi)存), 再創(chuàng)建一個dentry與之關(guān)聯(lián)上。
由這一過程可見, 其實是先有inode再有dentry. inode本身是存在于文件系統(tǒng)的存儲介質(zhì)上的, 而dentry則是在內(nèi)存中生成的。 dentry的存在加速了對inode的查詢。
既然整個目錄結(jié)構(gòu)可能不能全部載入內(nèi)存, 在內(nèi)存中生成的dentry將在無人使用時被釋放。 d_count字段記錄了dentry的引用計數(shù), 引用為0時, dentry將被釋放。
這里所謂的釋放dentry并不是直接銷毀并回收, 而是將dentry放入一個“最近最少使用(LRU)”隊列(與對應(yīng)的超級塊相關(guān)聯(lián))。 當(dāng)隊列過大, 或系統(tǒng)內(nèi)存緊缺時, 最近最少使用的一些dentry才真正被釋放。
這個LRU隊列就像是一個緩存池, 加速了對重復(fù)的路徑的訪問。 而當(dāng)dentry被真正釋放時, 它所對應(yīng)的inode將被減引用。 如果引用為0, inode也被釋放。
當(dāng)尋找一個文件路徑時, 對于其中經(jīng)歷的每一個節(jié)點, 有三種情況:
1, 對應(yīng)的dentry引用計數(shù)尚未減為0, 它們還在dentry樹中, 直接使用即可;
2, 如果對應(yīng)的dentry不在dentry樹中, 則試圖從LRU隊列去尋找。 LRU隊列中的dentry同時被散列到一個散列表中, 以便查找。 查找到需要的dentry后, 這個dentry被從LRU隊列中拿出來, 重新添加到dentry樹中;
3, 如果對應(yīng)的dentry在LRU隊列中也找不到, 則只好去文件系統(tǒng)的存儲介質(zhì)里面查找inode了。 找到以后dentry被創(chuàng)建, 并添加以dentry樹中;
文件系統(tǒng)掛載
VFS允許多種不同的文件系統(tǒng)掛載在同一個目錄結(jié)構(gòu)中, 文件系統(tǒng)掛載的路徑稱為掛載點。
如, 磁盤有兩個分區(qū)A和B, A作為根文件系統(tǒng)被掛載在“/”路徑下, 而B作為A的子文件系統(tǒng), 掛載在“/mnt/B/”下。
要完成這一掛載, A文件系統(tǒng)中必須有“/mnt/”這個目錄。 而不管A中有沒有“/mnt/B”, 都會生成一個dentry與之對應(yīng), 但是這個dentry并不對應(yīng)A中的“/mnt/B”所對應(yīng)的inode(即使這個inode存在)。 這個dentry中的d_mounted標記被置位, 表示這是一個掛載點。
如果在尋找文件路徑的過程中遇到這樣的一個掛載點, 則代表當(dāng)前路徑的指針將從當(dāng)前dentry切換到掛載的文件系統(tǒng)的“/”所對應(yīng)的dentry. 即是說, 訪問A分區(qū)中的“/mnt/B”這個路徑時, 實際訪問到的是B分區(qū)中的“/”路徑。
文件系統(tǒng)使用vfsmount結(jié)構(gòu)來描述, 多個掛載的文件系統(tǒng)也被組織成樹型結(jié)構(gòu)。
vfsmount結(jié)構(gòu)中有兩個指向dentry的指針, mnt_mountpoint指向其父文件系統(tǒng)的掛載點dentry(例如A分區(qū)中的“/mnt/B”), 而mnt_root指向本文件系統(tǒng)的根路徑dentry(例如B分區(qū)中的“/”)。 通過這兩個指針, 可以完成上面提到的當(dāng)前路徑的切換。
于是, 尋找文件路徑的過程中, 除了要記錄當(dāng)前dentry, 還要記錄當(dāng)前vfsmount. 如果當(dāng)前dentry是一個掛載點, 則通過當(dāng)前vfsmount, 找到其兒子中掛載點為當(dāng)前dentry的子vfsmount, 然后得到這個子vfsmount的mnt_root.
可能會有多個vfsmount都掛載在同一個dentry上, 這時候, 只有其中一個vfsmount會被選中, 而其他vfsmount將被隱藏。 直到被選中的那個vfsmount被卸載后, 被隱藏的vfsmount才可能被選中。 利用這個特點, 我們可以實現(xiàn)目錄的隱藏。 比如/home/kouu/secret下保存著一些不希望別人看到的文件, 可以在這個目錄上mount一下tmpfs, 以達到隱藏的目的。
子文件系統(tǒng)總是被掛載在父文件系統(tǒng)的某個dentry上, 而根文件系統(tǒng)則是由mnt_namespace對象來引用的。 不同的mnt_namespace可以引用不同的根文件系統(tǒng), 組織不同的文件系統(tǒng)掛載樹, 形成不同的目錄結(jié)構(gòu)。
一般而言, 新創(chuàng)建的進程總是與其父進程共用mnt_namespace. 而所有進程都是1號進程(init)的子孫進程, 則一般情況下所有進程都使用相同的mnt_namespace, 都生活在相同的目錄結(jié)構(gòu)中。
但是在通過clone系統(tǒng)調(diào)用創(chuàng)建新進程時, 可以指定CLONE_NEWNS標志, 為子進程創(chuàng)建新的名字空間(其中就包含了mnt_namespace, 此外名字空間還有其他內(nèi)容)。
前面只是說某個設(shè)備被掛載, 其實掛載文件系統(tǒng)除了要添加相應(yīng)的存儲介質(zhì)的設(shè)備文件, 還要在內(nèi)核中注冊文件系統(tǒng)類型(對應(yīng)file_system_type結(jié)構(gòu))(如ext2, ext3, tmpfs)。 一個文件系統(tǒng)總是包含設(shè)備和類型兩個要素的。
已注冊file_system_type被存儲在鏈表結(jié)構(gòu)中, 通過它們注冊的名字(比如ext3)來找到它們。 它們是文件數(shù)據(jù)的解釋器, 解釋設(shè)備文件所對應(yīng)的物理存儲介質(zhì)中的數(shù)據(jù)。
每個文件系統(tǒng)都有一個超級塊(對應(yīng)super_block結(jié)構(gòu)), 這個超級塊通過file_system_type結(jié)構(gòu)的get_sb方法從塊設(shè)備中讀出來。
而一個文件系統(tǒng)可以被掛載多次, 形成多個vfsmount結(jié)構(gòu)。 它們都對應(yīng)同一個super_block. 實際上只有文件系統(tǒng)第一次被掛載時, 才會去讀它的super_block. 否則這個super_block已經(jīng)是存在的, 直接引用即可。
在get_sb的過程中, 這個文件系統(tǒng)的根路徑所對應(yīng)的inode也會從存儲介質(zhì)中載入, 并創(chuàng)建對應(yīng)的dentry. super_block-》s_root就指向根路徑的dentry.
數(shù)據(jù)結(jié)構(gòu)總結(jié)
最后, 我們對上面的一些數(shù)據(jù)結(jié)構(gòu)及其函數(shù)指針集合進行一下整理, 這些東西實在容易讓人找不著北。
file_system_type
含義: 文件系統(tǒng)類型, 如ext2, ext3, 等等
創(chuàng)建: 內(nèi)核啟動或內(nèi)核模塊加載時, 為每一種文件系統(tǒng)類型創(chuàng)建一個對應(yīng)的file_system_type結(jié)構(gòu)
函數(shù): get_sb, 獲取超級塊的方法。 在注冊文件系統(tǒng)類型時提供
super_block
含義: 超級塊, 對應(yīng)一個存儲文件的設(shè)備
創(chuàng)建: 文件系統(tǒng)掛載時, 通過對應(yīng)的file_system_type-》get_sb從設(shè)備中讀取, 并初始化(可見, super_block結(jié)構(gòu)中一部分信息是保存在設(shè)備中的, 一部分則是在內(nèi)在中初始化的)
函數(shù): s_op, 超級塊的函數(shù)集, 主要包含對索引節(jié)點和文件系統(tǒng)實例的操作。 file_system_type-》get_sb從設(shè)備中讀取超級塊后, 用file_system_type對應(yīng)的特定函數(shù)集進行初始化
inode
含義: 索引節(jié)點, 對應(yīng)設(shè)備上存放的一個文件
創(chuàng)建: 1)在超級塊被載入時, 作為根的inode一并被載入; 2)通過mknod調(diào)用創(chuàng)新新的索引節(jié)點; 3)在尋找文件路徑的過程中, 從設(shè)備中讀取, 并初始化(跟super_block一樣, inode結(jié)構(gòu)中一部分信息是保存在設(shè)備中的, 一部分則是在內(nèi)在中初始化的)
函數(shù): i_op, 索引節(jié)點函數(shù)集, 主要包含對子inode的創(chuàng)建, 刪除等操作。 f_op, 文件函數(shù)集, 主要包含對本inode的讀寫等操作。 在inode被創(chuàng)建后, 1)如果是特殊文件, 則根據(jù)對應(yīng)文件的類型(包括塊設(shè)備, 字符設(shè)備, fifo, 等等)賦予特定的函數(shù)集(并不直接與設(shè)備和文件系統(tǒng)類型相關(guān)); 2)否則, 對應(yīng)的文件系統(tǒng)類型會提供相應(yīng)的函數(shù)集, 并且目錄和文件函數(shù)集很可能不同
dentry
含義: 目錄項, 尋找文件路徑的過程中使用的樹型結(jié)構(gòu), 與inode關(guān)聯(lián)
創(chuàng)建: inode被創(chuàng)建后, dentry就要被創(chuàng)建并初始化
函數(shù): d_op, 目錄項函數(shù)集, 主要包含對子dentry的查詢操作。 由文件系統(tǒng)類型確定
file
含義: 打開文件的實例
創(chuàng)建: 在open調(diào)用時創(chuàng)建, 并與一個inode對應(yīng)
函數(shù): f_op, 文件讀寫等操作。 1)等于inode-》f_op, 對于普通文件, 塊設(shè)備文件, 等; 2)由inode-》f_op-》open函數(shù)在文件打開時指定, 典型的情況是字符設(shè)備。 所有字符設(shè)備具有相同的inode-》f_op, 在inode-》f_op-》open過程中, 找到對應(yīng)設(shè)備驅(qū)動注冊的f_op, 賦給file-》f_op
評論
查看更多