1. 程式人生 > >linux 系統呼叫open 七日遊(二)

linux 系統呼叫open 七日遊(二)

接著昨日的旅程,我們應該開始處理具體的子路徑了:
【fs/namei.c】
sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk

點選(此處)摺疊或開啟

  ...

  1.         err = walk_component(nd, &next, LOOKUP_FOLLOW);
  2.         if
     (err < 0)
  3.             return err;

  4.         if (err) {
  5.             err = nested_symlink(
    &next, nd);
  6.             if (err)
  7.                 return err;
  8.         }

  ...

    現在,我們可以肯定當前的子路徑一定是一箇中間節點(資料夾或符號連結),既然是中間節點,那麼就需要“走過”這個節點。咱們來看看 walk_component 是怎麼“走過”中間節點的。在此之前先小小劇透一下,當 walk_component 返回時,如果當前子路徑是一個真正的目錄的話,那麼 nd 已經“站在”當前節點上並等著下一次迴圈再往前“站”一步。但如果當前子路徑只是一個符號連結呢?這時 nd 會原地不動,告訴你個小祕密:nd 是很有脾氣的,如果不是真正的目錄絕不會站上去;而 next 就隨和得多,它會自告奮勇不管是不是真正的目錄先站上去再說。接著 next 會和 nest_symlink 聯手幫助 nd 在下一個真正的目錄“上位”。

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

點選(此處)摺疊或開啟

  1. static inline int walk_component(struct nameidata *nd, struct path *path,
  2.         int follow)
  3. {

  ...

  1.     if (unlikely(nd->last_type != LAST_NORM))
  2.         return handle_dots(nd, nd->last_type);

  ...

    對於這個子路徑可以分成三種情況,第一,它可能是“.”或“..”;第二,這就是一個普通的目錄;第三,它是一個符號連結。我們先來看第一種可能,對於“.”或“..”,是在 handle_dots 單獨處理的。
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk->walk_component->handle_dots

點選(此處)摺疊或開啟

  1. static inline int handle_dots(struct nameidata *nd, int type)
  2. {
  3.     if (type == LAST_DOTDOT) {
  4.         if (nd->flags & LOOKUP_RCU) {
  5.             if (follow_dotdot_rcu(nd))
  6.                 return -ECHILD;
  7.         } else
  8.             follow_dotdot(nd);
  9.     }
  10.     return 0;
  11. }
    這裡只是針對“..”做處理(1489);如果是“.”的話那就就代表的是當前路徑,直接返回就好了。前面說過 do_filp_open 會首先使用 RCU 策略進行操作,如果不行再用普通策略。這裡就可以看出只有 RCU 失敗才會返回 -ECHILD 以啟動普通策略。但是大家有沒有發現,這裡並沒有對 follow_dotdot(_rcu) 的返回值進行檢查,為什麼?這是因為“..”出現在路徑裡就表示要向“上”走一層,也就是要走到父目錄裡面去,而父目錄一定是存在記憶體中而且對於當前的程序來說一定也是合法的,否則在讀取父目錄的時候就已經出錯了。接著我們就來“跟隨 ..”。
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component > handle_dots > follow_dotdot_rcu

點選(此處)摺疊或開啟

  1. static int follow_dotdot_rcu(struct nameidata *nd)
  2. {
  3.     set_root_rcu(nd);

  ...

    首先設定 nd 的根目錄(nd.root),還記得我們在哪裡設定過這個成員麼?沒錯,在 path_init 裡,如果是絕對路徑的話就會把這個 nd.root 設定成當前程序的根目錄(其實還可以在 do_file_open_root 裡預設這個值,所以為了和系統根目錄區分,我們稱 nd.root 為預設根目錄),但如果是相對路徑的話,就沒有對 nd.root 進行初始化。為啥要分兩步走呢?還是因為效率問題,任何一個目錄都是一種資源,根目錄也不例外,要獲取某種資源必定會有一定的系統開銷(在這裡就是順序鎖),況且很有可能辛辛苦苦獲得了這個根目錄資源卻根本就用不上,造成無端的浪費,所以 Kernel 本著能不用就不用的原則不到萬不得已絕不輕易佔用系統資源。現在的情況是路徑中出現了“..”,就說明需要向上走一層,也就有可能會訪問根目錄,所以現在正是獲取根目錄的時候。拿到根目錄後就進入一個小小的迴圈,有人問了:不就是往上走一層麼,為啥是個迴圈呢?請往下看:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component > handle_dots > follow_dotdot_rcu

點選(此處)摺疊或開啟

  ...

  1.     while (1) {
  2.         if (nd->path.dentry == nd->root.dentry &&
  3.          nd->path.mnt == nd->root.mnt) {
  4.             break;
  5.         }
  6.         if (nd->path.dentry != nd->path.mnt->mnt_root) {
  7.             struct dentry *old = nd->path.dentry;
  8.             struct dentry *parent = old->d_parent;
  9.             unsigned seq;

  10.             seq = read_seqcount_begin(&parent->d_seq);
  11.             if (read_seqcount_retry(&old->d_seq, nd->seq))
  12.                 goto failed;
  13.             nd->path.dentry = parent;
  14.             nd->seq = seq;
  15.             break;
  16.         }
  17.         if (!follow_up_rcu(&nd->path))
  18.             break;
  19.         nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
  20.     }

  ...

        通過觀察這個迴圈體我們發現只有 follow_up_rcu 返回非 0 的時候才會進入迴圈,其餘幾種情況都會通過 break 直接跳出迴圈。那麼這幾種情況都是啥意思呢?我們一個一個來看:
        首先,如果當前路徑就是預設根目錄的話(1141)就什麼也不做直接跳出迴圈(都已經到根目錄了不退出還等啥呢,大家可以在根目錄試試這個命令“cd ../../”,看看有什麼效果);其次,當前路徑不是預設根目錄,但也不是當前檔案系統的根目錄(1145),那麼向上走一層也是很簡單的事,直接將父目錄項拿過來就是了(1153);到最後,當前路徑一定是某個檔案系統的根目錄,往上走有可能就會走到另一個檔案系統裡去了。
        看到這裡可能有人要問了,啥叫檔案系統的根目錄,檔案系統和檔案系統又有啥關係?彆著急,我們祭出本次旅途的第二張導遊圖,再配合我的講解相信大家很快就會明白的。
【mount 結構圖】
        

    這是一個關於 mount(掛載)的故事。在 Kernel 世界裡,掛載是一項很了不起的特性,它可以將不同型別的檔案系統組合成一個有機的整體,從使用者角度來看不同的檔案系統並沒有什麼區別,那麼 Kernel 是怎麼做到呢?首先,Kernel 會為每個檔案系統準備一個 mount 結構,然後再把這個結構加入到 vfs 這顆大樹上就好了。這麼一個小小的 mount 結構就這麼神奇?請看圖,一個 mount 中有三個很重要的成員,他們分別指向父 mount 結構(6)、本檔案系統自己的根目錄(7)和本檔案系統的掛載點(8),前兩個很好理解,那麼掛載點是什麼呢?簡單地說掛載點就是父級檔案系統的某個目錄,一旦將某個檔案系統掛載到某個目錄上,這個目錄就成了該檔案系統的根目錄了。並且該目錄的標誌位 DCACHE_MOUNTED 將被置位,這將表明這個目錄已經是一個掛載點了,如果要訪問這個目錄的話就要順著 mount 結構訪問另一個檔案系統了,原來的內容將變得不可訪問。
    現在我們從圖的左邊講起,帶你一窺 mount 的風采。一個程序有一個叫 root 的 path 結構,它就是本程序的根目錄(大多數情況下它就是系統根目錄),root 中兩個成員分別指向某個檔案系統的 mount 結構(其實是指向 mount.mnt 但這樣理解沒問題)(1)和該檔案系統的根目錄(2),這個檔案系統就是所謂根檔案系統(在圖中就是 rootfs)。由於它是根檔案系統,所以它的父 mount 結構就是它自己(4)它的掛載點就是它自己的根目錄(5)。但是 rootfs 只是一個臨時的根檔案系統,在 Kernel 的啟動過程中載入完 rootfs 之後會緊接著解壓縮 initramfs 到 rootfs 中,這裡麵包括了驅動以及載入真正的根檔案系統的工具,Kernel 通過載入這些驅動、使用這些工具實現了掛載真正的根檔案系統。之後 rootfs 將推出歷史舞臺,但作為檔案系統的總根 rootfs 並不會被解除安裝(注)。圖中 fs1 就是所謂的真正的根檔案系統,Kernel 把它掛載到了 rootfs 的根目錄上(8),並且將它的父 mount 結構指向了 rootfs(6)。這時訪問根目錄的話就會直接訪問到 fs1 的根目錄,而 rootfs 就好像不存在了一樣。
    再看 fs1,他有一個子目錄“mnt/”,以及“mnt/”的子目錄“a”,此時路徑“/mnt/a/”是可訪問的。但現在我們還有另一個檔案系統 fs2,我們把它掛載到“/mnt/”上會發生什麼呢?首先 fs2 的父 mount 將指向 fs1(9),然後 fs2 的掛載點將指向 “/mnt/”(10),同時“mnt/”的 DCACHE_MOUNTED 將被置位。此時路徑“/mnt/a/”就不可訪問了,取而代之的是“/mnt/b/”。本著不怕麻煩的精神我們再折騰一下,把 fs3 也掛載到“/mnt/”上,這時和掛載 fs2 一樣父 mount 將指向 fs2(11),但是掛載點應該指向哪裡呢?答案是 fs2 的根目錄(12)。這時“/mnt/b/”也消失了,我們只能看見“/mnt/c”了。這樣整個結構就形成了一個掛載的序列,最後掛載的在序列末尾,Kernel 可以很容易的通過這個序列找到最初的掛載點和最終的檔案系統。
    在順序查詢的情景下,當遇到一個目錄時 Kernel 會判斷這個目錄是不是掛載點(檢查 DCACHE_MOUNTED 標誌位),如果是就要找到掛載到這個目錄的檔案系統,繼而找到該檔案系統的根目錄,然後在判斷這個根目錄是不是掛載點,如果是那就再往下找直到某個檔案系統的根目錄不再是掛載點。
    反向查詢也和順序查詢類似,我們結合程式碼來看:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component > handle_dots > follow_dotdot_rcu > follow_up_rcu

點選(此處)摺疊或開啟

  1. static int follow_up_rcu(struct path *path)
  2. {
  3.     struct mount *mnt = real_mount(path->mnt);
  4.     struct mount *parent;
  5.     struct dentry *mountpoint;

  6.     parent = mnt->mnt_parent;
  7.     if (&parent->mnt == path->mnt)
  8.         return 0;
  9.     mountpoint = mnt->mnt_mountpoint;
  10.     path->dentry = mountpoint;
  11.     path->mnt = &parent->mnt;
  12.     return 1;
  13. }
    首先檢查當前的檔案系統是不是根檔案系統(891),如果是就直接返回 0 並結束迴圈。如果不是的話就要像上走一層走到父 mount 代表的檔案系統中去,這個向上走的過程也很簡單,直接取得掛載點就可以了(894)當然 mount 也需要跟新一下(895)。但僅僅這樣做是不夠的,因為很有可能現在的這個目錄也是該檔案系統的根目錄,這就需要返回 1,啟動迴圈再來一次。
    當跳出這個 while(1) 迴圈時我們已經站在某個目錄上了,一般來說這個目錄就是我們想要的目標,而不會是一個掛載點,但也有例外。請看 while(1) 迴圈中第一個 if 和 follow_up_rcu 中的那個 if,想必大家已經發現了,當遇到(預設)根目錄的時候會直接退出迴圈,而這時我們的位置就相當於站在圖中 rootfs 的根目錄上,這顯然不是我們想要的,我們想要站在 fs1 的根目錄上。這就需要接下來的迴圈,再順著 mount 結構往下走。
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > link_path_walk > walk_component > handle_dots > follow_dotdot_rcu

點選(此處)摺疊或開啟

  ...

  1.     while (d_mountpoint(nd->path.dentry)) {
  2.         struct mount *mounted;
  3.         mounted = __lookup_mnt(nd->path.mnt, nd->path.dentry);
  4.         if (!mounted)
  5.             break;
  6.         nd->path.mnt = &mounted->mnt;
  7.         nd->path.dentry = mounted->mnt.mnt_root;
  8.         nd->seq = read_seqcount_begin(&nd->path.dentry->d_seq);
  9.         if (!read_seqretry(&mount_lock, nd->m_seq))
  10.             goto failed;
  11.     }
  12.     nd->inode = nd->path.dentry->d_inode;
  13.     return 0;

  ...

    d_mountpoint() 就是檢查標誌位 DCACHE_MOUNTED(1161),然後在某個散列表中查詢屬於這個掛載點的 mount 結構,如果找到了(如果某個目錄既是掛載點但又沒有任何檔案系統掛載在上面那就說明這個目錄可能擁有自動掛載的屬性),就往下走一層,走到掛載檔案系統的根目錄上(1167),然後再回到 1161 行再判斷、查詢、向下走,周而復始直到某個非掛載點。
        從 follow_dotdot_rcu 返回後,對“.”和“..”的處理也完成了,程式將直接返回 link_path_walk 進入對下一個子路徑的處理。
        休息一下,我們馬上回來。

注:摘自《深度探索 Linux 作業系統》王柏生

轉自:http://blog.chinaunix.net/uid-20522771-id-4419678.html