linux虛擬文件系統-文件的打開

本質上,一個文件打開的過程就是建立fd,file,dentry,inode,address_space的關聯過程。關聯過程中關鍵的一個過程是如何根據路徑名尋找到對應的dentry,對於用戶程序來說路徑只是一個特殊的字符串來表示全路徑,但是內核中不是以全路徑來管理文件的,而是分解成多個dentry,他們之間相互組成樹的形式,從該dentry到根文件系統的root dentry之間所有的dentry的名稱組合到一起並且在其中加上“/”組成了全路徑字符串;中間有一個映射過程,需要將用戶程序中路徑名映射到某個dentry上。
linux系統支持兩種路徑:絕對路徑和相對路徑,絕對路徑就是以根文件系統的root dentry爲起點的路徑名,例如/mnt/test/a,相對路徑以當前進程所在的工作目錄爲起點的路徑名,例如./a/b
另外系統還支持軟鏈接文件和掛載,在查找路徑的時候還需要對其進行轉換,該問題在下面路徑查找的時候進一步說明。

文件對象關聯

文件打開的過程中主要涉及到幾種對象,fd,file,dentry,inode,address_space,最後建立的映射關係如下圖。
fd,file,dentry,inode,address_space

  • files_struct中默認內嵌有一個fdtable和file的指針數組fd_array,一般進程打開的文件很少,所以內嵌的對象一般就能滿足使用。對於進程打開大量文件的情況,它會新申請fdtable對象並且對file指針數組進行擴張來容納更多的文件信息,並且將舊的fdtable信息和file指針數組拷貝到新的對象上去。fd代表索引,對應着fdtable->fd指針數組的下標和fdtable中open_fds和close_on_exec的兩個bitmap的位置。

  • fdtable中分別有open_fds和close_on_exec兩個指針,分別指向兩個bitmap,前者標識文件描述符是否可用,而後者描述了在進程exec時是否將文件描述符進行關閉。當進程exec時默認是繼承原有的fdtable信息的,這會造成兩種情況,一個是文件描述符的泄露,另一個是文件打開異常。例如當程序hello啓動時通過flock文件鎖的形式來檢查是否可以運行,第一次打開文件對他進行上鎖,之後通過exec啓動它自身,有進行文件打開和上鎖,此時就上鎖失敗了,這種情況時需要在open時指定O_CLOEXEC,在exec時自動關閉這個描述符。

  • 文件的各種信息都是拿到path之後進行實際關聯的,path中指向具體的dentry,進而關聯到inode和address_space。找到對應的dentry之後在do_dentry_open進一步關聯,file可以直接到inode和address_space,file的file_operations的對象更是原來的inode的i_fop,這樣接下來的文件讀寫就基本上可以直接面向address_space和它管理的page cache了。不同類型的文件file的file_operations是不同的,例如普通文件有readdir就毫無意義,反過來目錄文件有llseek操作也很奇怪,所以在根據文件系統上信息創建inode對象的時候根據文件類型爲他指定了不同的file_operations,這樣在和file關聯的時候就能得到正確的文件操作方法。

路徑查找過程

path:文件路徑,內核中一個文件所在的路徑使用vfsmount,dentry的形式即掛載點下的某個dentry來表示唯一路徑。主要是一個分區可能被多次掛載到不同的路徑,文件路徑雖然不同但是都是同一個文件。
nameidata來源於name to inode文件路徑到inode的解析,也可以理解爲路徑解析(name interprets),表示一次路徑查找的過。主要有幾個重要的成員:struct path path,root;int last_type;struct qstr last。在查找之前會對其進行初始化,root成員指向進程的根路徑,根據路徑是否以/開頭來決定path成員指向當前進程的工作目錄還是根路徑,這樣nameidata就能處理絕對路徑和相對路徑兩種形式了。path指向當前解析到的路徑,當查找結束的時候指向目標路徑。last_type表示上一次路徑的類型,總共有LAST_NORM, LAST_ROOT, LAST_DOT,LAST_DOTDOT幾種類型。struct qstr last表示下一次要查找的文件名稱。

struct nameidata {
    struct path path; 	//當前路徑,隨着路徑名的解析它會逐漸的變化,最後指向路徑名的path
    struct qstr last;	//在路徑名解析過程中,會根據“/”將路徑名分解成目錄項,例如“/mnt/test”則會分解成mnt和test兩個目錄項;代表當前目錄下要查找的文件名
    struct path root; 	//進程的根文件路徑
    struct inode    *inode; /* path.dentry.d_inode */ 
    unsigned int    flags; 
    unsigned    seq, m_seq;
    int     last_type; 
    unsigned    depth; 
    struct file *base;  
    char *saved_names[MAX_NESTED_LINKS + 1];          //主要是防止軟鏈接的不斷循環嵌套
}; 

在查找的過程中,它將文件路徑以/爲分隔符來分解成一串路徑,我們先不考慮特殊的路徑查找,以普通的路徑查找爲例。
file open上圖中是文件打開的基本過程:

  1. 分配空閒的文件描述符,當文件成功打開之後就能在最後的fd_install進行關聯
  2. 分配空閒的file,當路徑查找成功的時候可以進行和對應的dentry,inode進行關聯
  3. 文件路徑的分解link_path_walk,它將一個路徑分解成兩部分:最後一級文件名和其他部分,例如在圖中/mnt/test/a文件,分解成/mnt/test/a兩部分,當然這是一個最簡單的實例,實際的路徑更加複雜:最後一級文件可以是普通文件,目錄文件,軟連接文件,其他部分可能只是普通的路徑,也可能包含軟連接文件,還有可能經過掛載點。對於查找一個/mnt/test/a文件,它將路徑名拆解成mnt,test,通過walk_component循環查找每一級路徑並且更新last_type和last:查找/下是否有mnt,之後查找mnt下test,一直到剩下最後一級a。
  4. 最後do_last處理最後一級路徑a的問題,即真正開始進行文件打開的操作,從文件系統中讀取信息創建對應的dentry和inode,address_space信息並且關聯起來。

walk_component是路徑查找的主要實現,它裏面需要處理很多瑣碎的問題,最主要的就是併發問題。加入正在將a/b通過rename操作變成a/c/b,另一個進程正在查找a/b/..它可能最終返回的是a/c目錄文件。rename本身的操作應該是原子性的,查找的結果應該有兩種,當rename操作之前返回的是a,操作之後應該這個路徑就是非法的,無論如何也不應該返回給一個a/c目錄文件。上面的查找過程總體而言沒啥難度,但是這種併發操作讓這個過程的實現太晦澀了,而且也繞不開這個問題,所以裏面會使用各種的鎖和引用來防止併發。

首先說一下普通的慢速查找lookup_slow,對於/mnt/test/a路徑查找,例如查找/時是否有mnt目錄項,它首先需要獲取到(/)dentry->d_inode->i_mutex,之後通過目錄的inode->i_op->lookup進行查找,這個可能是需要進行實際讀取文件系統數據來進行查找的,文件系統可能是基於塊設備的也可能是網絡文件系統類型,意味着它需要發起磁盤IO或者網絡查詢來得到確切的答案。
內核中除了使用dcache來緩存文件系統的信息,避免進行底層文件系統操作之外,還通過dentry_hashtable哈希表來維護dentry,這樣可以通過父dentry和當前文件名信息struct qstr來計算hash值來查找當前文件的dentry,這樣的速度是最快的了。爲了應對併發問題,它使用RCU技術來保證對象不會在使用中被釋放掉,整個的link_path_walk過程都處於read臨界區,rcu_read_lock();link_path_walk();rcu_read_unlock()

目前的查找過程中,優先使用RCU walk的形式,當失敗之後再次使用slow的方式去文件系統中獲取對應的路徑。

軟鏈接文件

follow_link處理軟鏈接文件的路徑問題,通過dentry->d_inode->i_op->follow_link將軟鏈接文件的內容讀取到nameidata->save_names[nameidata->depth]中,之後根據鏈接路徑來重新初始化path。軟鏈接形式是絕對路徑時即以/開頭,復位nameidata->path到nameidata->root再次通過link_path_walk進行查找。
對於路徑中間存在軟鏈接形式的文件時,walk_component負責跳轉到軟鏈接中。
對於最後一級文件是軟鏈接時,當open時允許跟蹤軟鏈接文件時,do_last嘗試打開時會返回錯誤並且should_follow_link返回真,之後它會通過trailing_symlink再次進行查找和打開。

掛載點

當一個目錄被掛載之後,它和下面所有的文件都被隱藏掉了,後續的文件操作是看不到這些文件的,直至被卸載掉。當路徑查找的過程經過掛載點時,即當前dentry->d_flags|DCACHE_MOUNTED時表明當前目錄是一個掛載點,需要轉換到掛載點中的路徑也就是當前的可見路徑。
mount convert掛載點mount通過mount_hashtable表管理,它的hash值是通過掛載點test所在的vfsmount(test)和dentry(test)計算出來的,之後通過mount->mnt_parent->mnt == vfsmount(test) && mount->mnt_mountpoint == dentry(test)來確定mount;內核中使用lookup_mnt來查找一個掛載點dentry對應的mount信息,linux允許一個目錄可以被多次掛載,第一次查詢返回最後被掛載的信息,以後逐次返回上一次掛載的信息,最後返回空。

 * mount /dev/sda1 /mnt                                                             
 * mount /dev/sda2 /mnt                                                             
 * mount /dev/sda3 /mnt                                                             
 *                                                                                  
 * Then lookup_mnt() on the base /mnt dentry in the root mount will                 
 * return successively the root dentry and vfsmount of /dev/sda1, then              
 * /dev/sda2, then /dev/sda3, then NULL.   
 struct vfsmount *lookup_mnt(const struct path *path)

當經過掛載點路徑時,通過follow_managed來切換路徑,查找到對應的mount信息之後,改變nameidata->path中的信息:

path->mnt = mounted;
path->dentry = dget(mounted->mnt_root);

link_path_walk

link_path_walk只是循環查找中間的路徑名而不處理最後一級的路徑問題,除了普通的open之外還有不同的場景也會進行文件名解析,例如通過stat或者chmod系統調用也需要進行文件名解析,它只需在最後一級文件存在的情況下返回對應的dentry就足夠了。而當通過open系統調用進行文件打開時,它允許添加各種參數裏,例如O_CREAT,O_NOFOLLOW時,O_CREAT在最後一級dentry沒有對應的inode時進行創建inode動作,O_NOFOLLOW在最後一級dentry是個軟連接文件時不再進行follow link而是直接打開文件,此時讀到的就是軟鏈接文件的內容:它的鏈接路徑:/home/linux/project/linux/scripts/gdb/vmlinux-gdb.py,不帶O_NOFOLLOW時會根據它的鏈接路徑繼續查找link_path_walk找到最終的普通文件。
softlink

dcache的精確性問題

內核使用dentry和inode cache來保存底層文件系統的信息,這樣就不需要頻發查找磁盤了,但是對於網絡文件系統,dcache完全不能保證和實際文件系統的一致性,實際的文件系統在另外一臺機器上,可能被幾個機器網絡共享訪問,dcache不能精確代表當前的狀態,所以在查找的過程需要重新revalidate操作。

文件的關閉

因爲每一層存在一對多的關係,例如多個fd引用同一個file,多個file引用同一個inode,它通過引用計數來表示當前的使用狀態,對於文件的關閉操作直接觸發的就非常簡單,僅僅是解除fd和file的關聯,清除fdtable中的bitmap相關位,並對file引用計數減一。fput時當file的引用計數爲零時準備釋放file對象,它這裏使用了RCU異步延遲釋放的方式進行釋放,真正幹活的是__fput,對dentry減一,之後dput觸發inode的引用計數,一環扣一環,環環相扣來實現了精準引用統計,避免內存泄露和錯誤釋放。

VFS本身看起來比較簡單,實際上它非常複雜,處理的細節問題非常多,fs/namei.c文件就有5000行,還有幾個主要的問題沒有解決,留待下一篇吧

  1. dentry的各種鎖及應用場景分析,VFS本身支持多場景併發,並且它要處理各種底層文件系統的特性,這就有了各種鎖和引用計數
  2. 在查找過程中如何防止循環軟鏈接的
  3. RCU形式的查找和slow形式查找的切換
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章