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

接着上回,當對“.”和“..”處理完成後就直接返回進入下一個子路徑循環了,但如果當前子路徑不是“.”或“..”呢?
【fs/namei.c】sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component

點擊(此處)摺疊或打開

  ...

  1.     err = lookup_fast(nd, path, &inode);
  2.     if (unlikely(err)) {
  3.         if (err < 0)
  4.             goto out_err;

  5.         err = lookup_slow(nd, path);
  6.         if (err < 0)
  7.             goto out_err;

  8.         inode = path->dentry->d_inode;
  9.     }
  10.     err = -ENOENT;
  11.     if (!inode || d_is_negative(path->dentry))
  12.         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

點擊(此處)摺疊或打開

  1. static int lookup_fast(struct nameidata *nd,
  2.          struct path *path, struct inode **inode)
  3. {

  ...

  1.     if (nd->flags & LOOKUP_RCU) {
  2.         unsigned seq;
  3.         dentry = __d_lookup_rcu(parent, &nd->last, &seq);
  4.         if (!dentry)
  5.             goto unlazy;

  ...

  1.         *inode = dentry->d_inode;
  2.         if (read_seqcount_retry(&dentry->d_seq, seq))
  3.             return -ECHILD;

  ...

  1.         if (__read_seqcount_retry(&parent->d_seq, nd->seq))
  2.             return -ECHILD;
  3.         nd->seq = seq;

  ...

  1.         path->mnt = mnt;
  2.         path->dentry = dentry;
  3.         if (unlikely(!__follow_mount_rcu(nd, path, inode)))
  4.             goto unlazy;
  5.         if (unlikely(path->dentry->d_flags & DCACHE_NEED_AUTOMOUNT))
  6.             goto unlazy;
  7.         return 0;
  8. unlazy:
  9.         if (unlazy_walk(nd, dentry))
  10.             return -ECHILD;
  11.     } 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 呢?咱們接着往下看:
【fs/namei.c】sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component > lookup_fast

點擊(此處)摺疊或打開

  ...

  1. unlazy:
  2.         if (unlazy_walk(nd, dentry))
  3.             return -ECHILD;
  4.     } else {
  5.         dentry = __d_lookup(parent, &nd->last);
  6.     }

  7.     if (unlikely(!dentry))
  8.         goto need_lookup;

  ...

  1.     path->mnt = mnt;
  2.     path->dentry = dentry;
  3.     err = follow_managed(path, nd->flags);
  4.     if (unlikely(err < 0)) {
  5.         path_put_conditional(path, nd);
  6.         return err;
  7.     }
  8.     if (err)
  9.         nd->flags |= LOOKUP_JUMPED;
  10.     *inode = path->dentry->d_inode;
  11.     return 0;

  12. need_lookup:
  13.     return 1;
  14. }
    還是結合 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

點擊(此處)摺疊或打開

  1. static int lookup_slow(struct nameidata *nd, struct path *path)
  2. {

  ...

  1.     mutex_lock(&parent->d_inode->i_mutex);
  2.     dentry = __lookup_hash(&nd->last, parent, nd->flags);
  3.     mutex_unlock(&parent->d_inode->i_mutex);

  ...

  1. }
    看到這裏大家就一定會明白爲什麼是 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

點擊(此處)摺疊或打開

  1. static struct dentry *__lookup_hash(struct qstr *name,
  2.         struct dentry *base, unsigned int flags)
  3. {
  4.     bool need_lookup;
  5.     struct dentry *dentry;

  6.     dentry = lookup_dcache(name, base, flags, &need_lookup);
  7.     if (!need_lookup)
  8.         return dentry;

  9.     return lookup_real(base->d_inode, dentry, flags);
  10. }
    請先看 1344 行,大家可能會奇怪:不是在內存中沒找到才進來的嗎,怎麼這裏又去內存中找一遍?別忘了,上一級函數使用了互斥鎖,這將有可能導致進程睡眠,也就有可能恰好有人在我們睡覺的時候這個目標加載進了內存,所以這裏需要檢查一下,而且反正是在內存中查找,不會太費事的。要是真找到了呢,那就撞大運了,高高興興的返回吧,要還是沒有就只好自己動手豐衣足食老老實實的啓動 lookup_real ,從真正的文件系統上讀取吧。lookup_real 我們就不深入進去了,在裏面主要是調用了具體文件系統自己的 lookup 函數去完成工作,而這些函數很有可能會啓動文件系統所在設備的驅動程序,從真正的設備上讀取(例如硬盤)數據,所以就更慢了,這纔是名副其實的“lookup_slow”。
    lookup_slow 剩下的工作和 fast 差不多,這裏就不重複了。現在回到 walk_component:
【fs/namei.c】sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component

點擊(此處)摺疊或打開

  ...

  1.     if (should_follow_link(path->dentry, follow)) {
  2.         if (nd->flags & LOOKUP_RCU) {
  3.             if (unlikely(unlazy_walk(nd, path->dentry))) {
  4.                 err = -ECHILD;
  5.                 goto out_err;
  6.             }
  7.         }
  8.         BUG_ON(inode != path->dentry->d_inode);
  9.         return 1;
  10.     }
  11.     path_to_nameidata(path, nd);
  12.     nd->inode = inode;
  13.     return 0;

  ...

  1. }
    當走到這裏的時候 nd 還是指向父級目錄,但 path 已經指向子目錄項了,這時只需確定該目錄項是一個正常的目錄,就可以更新 nd 然後繼續下一個子目錄項(1559)。但如果真是一個符號鏈接呢?這時就需要先切換到 ref-walk 模式(1551),然後返回 1,讓 link_path_walk 接着處理這個符號鏈接。問題來了,爲什麼要切換到 ref-walk 模式呢?這是因爲在處理符號鏈接的時候需要調用具體文件系統自己的處理函數,而在這些函數裏很有可能會因爲申請系統資源導致的進程阻塞,我們知道 rcu-walk 期間是禁止阻塞的,所以在這裏需要先退出 rcu-walk 模式。
    既然退出 rcu-walk 就可以睡眠了,那我們也休息一下,明天再接着去遊覽有趣的符號鏈接。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章