1. 程式人生 > >Linux 檔案系統呼叫open七日遊(三)

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 就可以睡眠了,那我們也休息一下,明天再接著去遊覽有趣的符號連結。
轉自:http://blog.chinaunix.net/uid-20522771-id-4419703.html