轉自http://hi.baidu.com/_kouu/item/4e9db87580328244ef1e53d0html
######node
虛擬文件系統(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.linux
數據結構總結
最後, 咱們對上面的一些數據結構及其函數指針集合進行一下整理, 這些東西實在容易讓人找不着北.shell
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_opsocket