1. 程式人生 > >Linux核心閱讀--檔案路徑查詢(二)

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策略,轉入加鎖、引用計數策略。下面貼一下路徑查詢的核心程式碼,具體看一下流程。

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;
}
walk_compoment完成正是從父目錄查詢子目錄項的功能。我們可以看到,每次核心都會都會嘗試用lookup_fast查詢dentry中的快取,看一下是否命中,如果沒有命中,則會用lookup_slow下降到檔案系統層進行路徑查詢。之後我們還可以注意到一個細節,就是當遇到符號連結的時候,核心也會呼叫unlazy_walk函式來終止RCU查詢模式。

下面我從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模式。