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