Linux核心閱讀--檔案路徑查詢(二)
Linux檔案路徑查詢的基本策略,是從查詢根(一般是根目錄或當前目錄)開始,逐級向下查詢。具體到程式碼中,每個查詢的節點被表示為path,path的定義如下。
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
};
我們可以看到,linux用掛載點和目錄項來唯一表示一個路徑節點。目錄查詢,就是不斷的從父路徑節點找到子目錄節點的過程。在路徑的查詢中,可能會遇到一些跳轉,比如遇到掛載點,遇到符號連結,遇到"..",就需要跳轉到相應的位置繼續查詢。
如果沒有遇到需要跳轉的情況,則主要依賴dentry中儲存的資訊進行路徑查詢。dentry中的資訊,一個是inode、superblock等檔案系統相關的資訊,這些資訊可以用來從磁碟中查詢子目錄。除了檔案系統資訊,dentry也維護了一個Cache,用來儲存之前查詢過的目錄項。
一般來說,dentry有很大的機率被同時訪問。併發訪問的時候,為了維護dentry結構的內部一致性,每次查詢子目錄項,都需要對父目錄加鎖。在高併發的情形下,因為'/'之類的目錄很容易被訪問文,鎖衝突的概率比較大,路徑查詢的效能就會降低。
linux的解決方案是採用RCU演算法,dentry的資料結構被設計為讀安全的,即在不加鎖的情況下讀,可能讀到老資料,但不會造成飛指標指之類的惡性事故。linux在每個dentry記錄一個版本號,在查詢之前會將這個版本號記錄下來,等查詢操作完成之後,再對比一個版本號有無變化,沒有變化,說明這次讀操作訪問的資料結構是一致的,結果有效。
因為每次路徑查詢,往往都是對最後一個節點進行修改,最容易衝突的dentry節點往往是最不常被修改的,因此這種演算法可以比較有效的解決鎖衝突問題。不過這裡存在一個問題,如果讀操作失敗該如何處理?難道要接著重試麼?這種重試會不會造成死迴圈?
linux採用的策略是,如果某次讀dentry失敗,則放棄RCU策略,轉為層層加鎖策略。另外,假如查詢需要下放到檔案系統層,linux也會放棄RCU策略,轉入加鎖、引用計數策略。下面貼一下路徑查詢的核心程式碼,具體看一下流程。
walk_compoment完成正是從父目錄查詢子目錄項的功能。我們可以看到,每次核心都會都會嘗試用lookup_fast查詢dentry中的快取,看一下是否命中,如果沒有命中,則會用lookup_slow下降到檔案系統層進行路徑查詢。之後我們還可以注意到一個細節,就是當遇到符號連結的時候,核心也會呼叫unlazy_walk函式來終止RCU查詢模式。static inline int walk_component(struct nameidata *nd, struct path *path, int follow) { struct inode *inode; int err; /* * "." and ".." are special - ".." especially so because it has * to be able to know about the current root directory and * parent relationships. */ if (unlikely(nd->last_type != LAST_NORM)) return handle_dots(nd, nd->last_type); err = lookup_fast(nd, path, &inode); if (unlikely(err)) { if (err < 0) goto out_err; err = lookup_slow(nd, path); if (err < 0) goto out_err; inode = path->dentry->d_inode; } err = -ENOENT; if (!inode) goto out_path_put; if (should_follow_link(inode, follow)) { if (nd->flags & LOOKUP_RCU) { if (unlikely(unlazy_walk(nd, path->dentry))) { err = -ECHILD; goto out_err; } } BUG_ON(inode != path->dentry->d_inode); return 1; } path_to_nameidata(path, nd); nd->inode = inode; return 0; out_path_put: path_to_nameidata(path, nd); out_err: terminate_walk(nd); return err; }
下面我從lookup_fast中擷取一段核心程式碼 。
if (nd->flags & LOOKUP_RCU) {
unsigned seq;
dentry = __d_lookup_rcu(parent, name, &seq, nd->inode);
if (!dentry)
goto unlazy;
/*
* This sequence count validates that the inode matches
* the dentry name information from lookup.
*/
*inode = dentry->d_inode;
if (read_seqcount_retry(&dentry->d_seq, seq))
return -ECHILD;
/*
* This sequence count validates that the parent had no
* changes while we did the lookup of the dentry above.
*
* The memory barrier in read_seqcount_begin of child is
* enough, we can use __read_seqcount_retry here.
*/
if (__read_seqcount_retry(&parent->d_seq, nd->seq))
return -ECHILD;
nd->seq = seq;
if (unlikely(d_need_lookup(dentry))) {
goto unlazy;
}
path->mnt = mnt;
path->dentry = dentry;
if (unlikely(!__follow_mount_rcu(nd, path, inode)))
goto unlazy;
if (unlikely(path->dentry->d_flags & DCACHE_NEED_AUTOMOUNT))
goto unlazy;
return 0;
unlazy:
if (unlazy_walk(nd, dentry))
return -ECHILD;
} else {
dentry = __d_lookup(parent, name);
}
這裡我們能夠更清晰的看到,核心在RCU模式下會不加鎖查詢dentry 快取,在非RCU模式下,則會用加鎖的方式。當RCU讀不成功,則讀取結果不靠譜,終止當前查詢流程,然後用非RCU的方式從當前進度繼續查詢。如果查詢dentry 快取找不到所需目錄項,則會呼叫unlazy_walk,在當前查詢流程裡直接轉入非RCU模式。