天天看點

走馬觀花: Linux 系統調用 open 七日遊(三)

    接着上回,當對“.”和“..”處理完成後就直接傳回進入下一個子路徑循環了,但如果目前子路徑不是“.”或“..”呢?

【fs/namei.c】sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component

點選(此處)折疊或打開

  ...

    err = lookup_fast(nd, path, &inode);

    if (unlikely(err)) {

        if (err 0)

            goto out_err;

        err = lookup_slow(nd, path);

        inode = path->dentry->d_inode;

    }

    err = -ENOENT;

    if (!inode || d_is_negative(path->dentry))

        goto out_path_put;

    在 Kernel 中任何一個常用操作都會有兩套以上的政策,其中一個是高效率的相對而言另一個就是系統開銷比較大的。比如在上面的代碼中就能直覺的發現 Kernel 會首先嘗試 fast(1534) ,如果失敗了才會啟動 slow(1539)。其實在我們目前的場景中不止這兩種政策,别忘了在這裡還有 rcu-walk 和 ref-walk,現在我們先簡單介紹一下 Kernel 在這裡進行“路徑行走”的政策,讓大家有一個感性認識,然後再進入這幾個函數中進行理性分析。首先 Kernel 會在 rcu-walk 模式下進入 lookup_fast 進行嘗試,如果失敗了那麼就嘗試就地轉入 ref-walk,如果還是不行就回到 do_filp_open 從頭開始。Kernel 在 ref-walk 模式下會首先在記憶體緩沖區查找相應的目标(lookup_fast),如果找不到就啟動具體檔案系統自己的 lookup 進行查找(lookup_slow)。注意,在 rcu-walk 模式下是不會進入 lookup_slow 的。如果這樣都還找不到的話就一定是是出錯了,那就報錯傳回吧,這時螢幕就會出現喜聞樂見的“No such file or directory”。

    我們這就進入 lookup_fast,看看它到底有多快。

【fs/namei.c】sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component > lookup_fast

static int lookup_fast(struct nameidata *nd,

         struct path *path, struct inode **inode)

{

    if (nd->flags & LOOKUP_RCU) {

        unsigned seq;

        dentry = __d_lookup_rcu(parent, &nd->last, &seq);

        if (!dentry)

            goto unlazy;

        *inode = dentry->d_inode;

        if (read_seqcount_retry(&dentry->d_seq, seq))

            return -ECHILD;

        if (__read_seqcount_retry(&parent->d_seq, nd->seq))

        nd->seq = seq;

        path->mnt = mnt;

        path->dentry = dentry;

        if (unlikely(!__follow_mount_rcu(nd, path, inode)))

        if (unlikely(path->dentry->d_flags & DCACHE_NEED_AUTOMOUNT))

        return 0;

unlazy:

        if (unlazy_walk(nd, dentry))

    } else {

    首先調用 __d_lookup_rcu 在記憶體中的某個散清單裡通過字元串比較查找目标 dentry,如果找到了就傳回該 dentry;如果沒找到就需要跳轉到 unlazy 标号處(1374),在這裡會使用 unlazy_walk 就地将查找模式切換到 ref-walk,如果還不行就隻好傳回到 do_filp_open 從頭來過(1412)。

如果順利找到了目标 dentry 則還需要進行一系列的檢查(1381、1391)確定在我們做讀取操作的期間沒有人對這些結構進行改動。然後就是更新臨時變量 path,為啥不更新 nd 呢?别忘了 nd 是很有脾氣的,挂載點和符号連結人家都看不上,非真正目錄不嫁。而這個時候還不知道這個目标是不是一個挂載點,如果是挂載點則還需要沿着被挂載的 mount 結構走到真正的目标上;退一步來說,就算這個目标不是挂載點,但它要是具備自動挂載特性呢(1407);再退一步來說,它是不是符号連結我們也不知道,是以現在先不忙着更新 nd。緊接着就通過 __follow_mount_rcu 跨過挂載點這些“僞目标”(1405),這個函數和上一篇裡 follow_dotdot_rcu 的第二部分很相似我們就不深入進去了,有興趣的同學結合代碼自己研究一下就好了。如果一切順利傳回 0,請參考上面 walk_component 的代碼,如果傳回 0 就會跳過 1535 行那個 if,這也就是說在 rcu-walk 模式下是不會啟動 lookup_slow 的。

        那麼什麼時候才會啟動 lookup_slow 呢?咱們接着往下看:

        dentry = __d_lookup(parent, &nd->last);

    if (unlikely(!dentry))

        goto need_lookup;

    path->mnt = mnt;

    path->dentry = dentry;

    err = follow_managed(path, nd->flags);

    if (unlikely(err 0)) {

        path_put_conditional(path, nd);

        return err;

    if (err)

        nd->flags |= LOOKUP_JUMPED;

    *inode = path->dentry->d_inode;

    return 0;

need_lookup:

    return 1;

}

    還是結合 walk_component 的代碼,我們發現隻有在 lookup_fast 傳回值大于 0 的時候才會啟動 lookup_slow,而在 lookup_fast 裡面我們看到隻有一種情況傳回值會大于 0,那就是 1417 行 dentry 為 NULL 的情況下會傳回 1。也就是說啟動 lookup_slow 的先決條件就是記憶體中還沒有讀入這個目标。接下來的代碼已經切換到了 ref-walk 模式中,但其處理方式和 rcu-walk 差不多,結合 rcu-walk 部分的講解自己研究一下吧。需要提一句的就是 follow_managed,這個函數會檢查目前 dentry 是否是個挂載點,如果是就跟下去(的确和 rcu-walk 差不多,是吧),不過這個函數還會檢查另外兩個特性 DCACHE_MANAGE_TRANSIT 和 DCACHE_NEED_AUTOMOUNT,大家要有興趣自己去研究一下吧。

    接下來我們就來看看 lookup_slow:

【fs/namei.c】sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component > lookup_slow

static int lookup_slow(struct nameidata *nd, struct path *path)

    mutex_lock(&parent->d_inode->i_mutex);

    dentry = __lookup_hash(&nd->last, parent, nd->flags);

    mutex_unlock(&parent->d_inode->i_mutex);

    看到這裡大家就一定會明白為什麼是 slow 了,互斥鎖(mutex)是有可能引起程序阻塞的,而在 lookup_fast 裡面沒有使用任何可能導緻程序睡眠的操作,這将導緻 lookup_slow 的效率遠遠低于 lookup_fast。還不僅僅如此,我們繼續看看 __lookup_hash:

【fs/namei.c】sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component > lookup_slow > __lookup_hash

static struct dentry *__lookup_hash(struct qstr *name,

        struct dentry *base, unsigned int flags)

    bool need_lookup;

    struct dentry *dentry;

    dentry = lookup_dcache(name, base, flags, &need_lookup);

    if (!need_lookup)

        return dentry;

    return lookup_real(base->d_inode, dentry, flags);

    請先看 1344 行,大家可能會奇怪:不是在記憶體中沒找到才進來的嗎,怎麼這裡又去記憶體中找一遍?别忘了,上一級函數使用了互斥鎖,這将有可能導緻程序睡眠,也就有可能恰好有人在我們睡覺的時候這個目标加載進了記憶體,是以這裡需要檢查一下,而且反正是在記憶體中查找,不會太費事的。要是真找到了呢,那就撞大運了,高高興興的傳回吧,要還是沒有就隻好自己動手豐衣足食老老實實的啟動 lookup_real ,從真正的檔案系統上讀取吧。lookup_real 我們就不深入進去了,在裡面主要是調用了具體檔案系統自己的 lookup 函數去完成工作,而這些函數很有可能會啟動檔案系統所在裝置的驅動程式,從真正的裝置上讀取(例如硬碟)資料,是以就更慢了,這才是名副其實的“lookup_slow”。

    lookup_slow 剩下的工作和 fast 差不多,這裡就不重複了。現在回到 walk_component:

    if (should_follow_link(path->dentry, 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;

    當走到這裡的時候 nd 還是指向父級目錄,但 path 已經指向子目錄項了,這時隻需确定該目錄項是一個正常的目錄,就可以更新 nd 然後繼續下一個子目錄項(1559)。但如果真是一個符号連結呢?這時就需要先切換到 ref-walk 模式(1551),然後傳回 1,讓 link_path_walk 接着處理這個符号連結。問題來了,為什麼要切換到 ref-walk 模式呢?這是因為在處理符号連結的時候需要調用具體檔案系統自己的處理函數,而在這些函數裡很有可能會因為申請系統資源導緻的程序阻塞,我們知道 rcu-walk 期間是禁止阻塞的,是以在這裡需要先退出 rcu-walk 模式。

    既然退出 rcu-walk 就可以睡眠了,那我們也休息一下,明天再接着去遊覽有趣的符号連結。

繼續閱讀