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

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

友情提示:您需要一個 kernel 3.15.6,下載地址: https://www.kernel.org/pub/linux/kernel/v3.0/linux-3.15.6.tar.xz
    我們將以 Linux 系統呼叫 open 為主線,參觀遊覽 Kernel 的檔案系統,一窺 Kernel 檔案系統精妙的設計和嚴謹的實現。因受篇幅限制,我們此次觀光只涉足 Kernel 的虛擬檔案系統(vfs),對於具體的檔案系統就不深入進去了。
    各位,準備好了嗎?我們已經迫不及待要開始這次奇幻之旅了!
    “前往 Kernel 的旅客請注意,您所乘坐的 OPEN1024 航班已經開始登機......”


    好了,我們的 OPEN1024 航班已經通過 Linux 系統呼叫穿越了中斷門來到了核心空間,並且通過系統呼叫表(sys_call_table)到達了系統呼叫 open 的主程式 sys_open,這就開始我們的旅程吧。
fs/open.c sys_open()

點選(此處)摺疊或開啟

  1. SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t,
     mode)
  2. {
  3.     if (force_o_largefile())
  4.         flags |= O_LARGEFILE;

  5.     return do_sys_open(AT_FDCWD, filename, flags, mode);
  6. }
    SYSCALL_DEFINE3 是用來定義系統呼叫的巨集,展開後類似於這樣:

  1. long sys_open(const char __user *filename, int flags, umode_t mode)
    形參 filename 實際上就是路徑名;flags 表示開啟模式,諸如只讀、新建等等;mode 代表新建檔案的許可權,所以僅僅在建立新檔案時(flags 為 O_CREAT 或 O_TMPFILE)才使用,具體還有哪些標誌位請參考 Linux man 手冊( http://man7.org/linux/man-pages/man2/open.2.html )。接下來,除了 flags 會在 64 位 Kernel 的情況下強制加上 O_LARGEFILE 標誌位,其餘的引數都原封不動的傳遞給 open 的主函式 do_sys_open。唯一需要注意的是 AT_FDCWD,其定義在 include/uapi/linux/fcntl.h,是一個特殊值(-100),該值表明當 filename 為相對路徑的情況下將當前程序的工作目錄設定為起始路徑。相對而言,你可以在另一個系統呼叫 openat 中為這個起始路徑指定一個目錄,此時 AT_FDCWD 就會被該目錄的描述符所替代。
    現在來看 open 的主函式 do_sys_open。
fs/open.csys_open()->do_sys_open()

點選(此處)摺疊或開啟

  1. long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
  2. {
  3.     struct open_flags op;
  4.     int fd = build_open_flags(flags, mode, &op);
  5.     struct filename *tmp;

  6.     if (fd)
  7.         return fd;

  8.     tmp = getname(filename);
  9.     if (IS_ERR(tmp))
  10.         return PTR_ERR(tmp);

  11.     fd = get_unused_fd_flags(flags);
  12.     if (fd >= 0) {
  13.         struct file *= do_filp_open(dfd, tmp, &op);
  14.         if (IS_ERR(f)) {
  15.             put_unused_fd(fd);
  16.             fd = PTR_ERR(f);
  17.         } else {
  18.             fsnotify_open(f);
  19.             fd_install(fd, f);
  20.         }
  21.     }
  22.     putname(tmp);
  23.     return fd;
  24. }
    首先檢查幷包裝傳遞進來的標誌位(962),隨後將使用者空間的路徑名複製到核心空間(968),在順利取得空閒的檔案表述符的情況下呼叫 do_filp_open 完成對路徑的搜尋和檔案的開啟(974),如果一切順利,就為這個檔案描述符安裝檔案(980),然後大功告成並將檔案描述符返回使用者空間。在此之前還不忘使用 fsnotify 機制來喚醒檔案系統中的監控程序(979)。
    build_open_flags 就是對標誌位進行檢查,然後包裝成 struct open_flags 結構以供接下來的函式使用。因為這些標誌位大多涉及到對最終目標檔案的操作,所以這個函式也等到我們用到這些標誌位的時候再回過頭來看。
    接下來就是 getname ,這個函式定義在 fs/namei.c,主體是 getname_flags,我們撿重點的分析,無關緊要的程式碼以 ... 略過:
fs/namei.c sys_open()->do_sys_open()->getname()->getname_flags()

點選(此處)摺疊或開啟

  1. static struct filename *
  2. getname_flags(const char __user *filename, int flags, int *empty)
  3. {
  4.     struct filename *result, *err;
  ...
  1.     result = __getname();
  ...
  1.     kname = (char *)result + sizeof(*result);
  2.     result->name = kname;
  3.     result->separate = false;
  4.     max = EMBEDDED_NAME_MAX;

  5. recopy:
  6.     len = strncpy_from_user(kname, filename, max);

  ...

  1.     if (len == EMBEDDED_NAME_MAX && max == EMBEDDED_NAME_MAX) {
  2.         kname = (char *)result;

  3.         result = kzalloc(sizeof(*result), GFP_KERNEL);

  ...

  1.         result->name = kname;
  2.         result->separate = true;
  3.         max = PATH_MAX;
  4.         goto recopy;
  5.     }
  ...
  1. }
    首先通過第 144 行 __getname  在核心緩衝區專用佇列裡申請一塊記憶體用來放置路徑名,其實這塊記憶體就是一個 4KB 的記憶體頁。這塊記憶體頁是這樣分配的,在開始的一小塊空間放置結構體 struct filename,之後的空間放置字串。152 行初始化字串指標 kname,使其指向這個字串的首地址,相當於 kname = (char *)((struct filename *)result + 1)。然後就是拷貝字串(158),返回值 len 代表了已經拷貝的字串長度。如果這個字串已經填滿了記憶體頁剩餘空間(170 ),就說明該字串的長度已經大於 4KB - sizeof(struct filename) 了,這時就需要將結構體 struct filename 從這個記憶體頁中分離(180)並單獨分配空間(173),然後用整個記憶體頁儲存該字串(171)。
    回到 do_sys_open,現在需要為新開啟的檔案分配空閒檔案描述符。get_unused_fd_flags 主要作用就是在當前程序的 files 結構中找到空閒的檔案描述符,並初始化該描述符對應的 file 結構。
    一切準備就緒,就進入 do_filp_open 了。
【fs/namei.c】sys_open->do_sys_open->do_filp_open

點選(此處)摺疊或開啟

  1. struct file *do_filp_open(int dfd, struct filename *pathname,
  2.         const struct open_flags *op)
  3. {
  4.     struct nameidata nd;
  5.     int flags = op->lookup_flags;
  6.     struct file *filp;

  7.     filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
  8.     if (unlikely(filp == ERR_PTR(-ECHILD)))
  9.         filp = path_openat(dfd, pathname, &nd, op, flags);
  10.     if (unlikely(filp == ERR_PTR(-ESTALE)))
  11.         filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
  12.     return filp;
  13. }
    主角是 path_openat,在這裡 Kernel 向我們展示了“路徑行走(path walk)”的兩種策略:rcu-walk(3242)和 ref-walk(3244)。在 rcu-walk 期間將會禁止搶佔,也決不能出現程序阻塞,所以其效率很高;ref-walk 會在 rcu-walk 失敗、程序需要隨眠或者需要取得某結構的引用計數(reference count)的情況下切換進來,很明顯它的效率大大低於 rcu-walk。最後 REVAL(3246)其實也是 ref-walk,在以後我們會看到,該模式是在已經完成了路徑查詢,開啟具體檔案時,如果該檔案已經過期(stale)才啟動的,所以 REVAL 是給具體檔案系統自己去解釋的。其實 REVAL 幾乎不會用到,在核心的檔案系統中只有 nfs 用到了這個模式。
    path_openat 主要作用是首先為 struct file 申請記憶體空間,設定遍歷路徑的初始狀態,然後遍歷路徑並找到最終目標的父節點,最後根據目標的型別和標誌位完成 open 操作,最終返回一個新的 file 結構。我們分段來看:
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat

點選(此處)摺疊或開啟

  1. static struct file *path_openat(int dfd, struct filename *pathname,
  2.         struct nameidata *nd, const struct open_flags *op, int flags)
  3. {

  ...

  1.     file = get_empty_filp();
  2.     if (IS_ERR(file))
  3.         return file;

  4.     file->f_flags = op->open_flag;

  ...

  1.     error = path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);
  2.     if (unlikely(error))
  3.         goto out;

  ...

    首先需要分配一個 file 結構,成功的話 get_empty_filp 會返回一個指向該結構的指標,在這個函式裡會對許可權、最大檔案數進行檢查。我們忽略有關 tempfile 的處理,直接來看 path_init。path_init 是對真正遍歷路徑環境的初始化,主要就是設定變數 nd。這個 nd 是 do_filp_open 裡定義的區域性變數,是一個臨時性的資料結構,用來儲存遍歷路徑的中間結果,其結構體定義如下:
【include/linux/namei.h】

點選(此處)摺疊或開啟

  1. struct nameidata {
  2.     struct path    path;
  3.     struct qstr    last;
  4.     struct path    root;
  5.     struct inode    *inode; /* path.dentry.d_inode */
  6.     unsigned int    flags;
  7.     unsigned    seq, m_seq;
  8.     int        last_type;
  9.     unsigned    depth;
  10.     char *saved_names[MAX_NESTED_LINKS + 1];
  11. };
    其中,path 儲存當前搜尋到的路徑;last 儲存當前子路徑名及其雜湊值;root 用來儲存根目錄的資訊;inode 指向當前找到的目錄項的 inode 結構;flags 是一些和查詢(lookup)相關的標誌位;seq 是相關目錄項的順序鎖序號; m_seq 是相關檔案系統(其實是 mount)的順序鎖序號; last_type 表示當前節點型別;depth 用來記錄在解析符號連結過程中的遞迴深度;saved_names 用來記錄相應遞迴深度的符號連結的路徑。我們結合 path_init  的程式碼來看這些成員的初始化。
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat->path_init

點選(此處)摺疊或開啟

  1. static int path_init(int dfd, const char *name, unsigned int flags,
  2.          struct nameidata *nd, struct file **fp)
  3. {
  4.     int retval = 0;

  5.     nd->last_type = LAST_ROOT; /* if there are only slashes... */
  6.     nd->flags = flags | LOOKUP_JUMPED;
  7.     nd->depth = 0;
  8.     if (flags & LOOKUP_ROOT) {
  ...
  1.     }

  2.     nd->root.mnt = NULL;

  3.     nd->m_seq = read_seqbegin(&mount_lock);
  4.     if (*name=='/') {
  ...
  1.             set_root(nd);

  ...

  1.         nd->path = nd->root;
  2.     } else if (dfd == AT_FDCWD) {

  ...

  1.             get_fs_pwd(current->fs, &nd->path);

  ...

  1.     } else {

  ...

  1.     }

  2.     nd->inode = nd->path.dentry->d_inode;
  3.     return 0;
  4. }
    首先將 last_type 設定成 LAST_ROOT,意思就是在路徑名中只有“/”。為方便敘述,我們把一個路徑名分成三部分:起點(根目錄或工作目錄)、子路徑(以“/”分隔的一系列子字串)和最終目標(最後一個子路徑),Kernel 會一個子路徑一個子路徑的遍歷整個路徑。所以 last_type 表示的是當前子路徑(不是 dentry 或 inode)的型別。last_type 一共有五種型別:
【include/linux/namei.h】

點選(此處)摺疊或開啟

  1. /*
  2.  * Type of the last component on LOOKUP_PARENT
  3.  */
  4. enum {LAST_NORM, LAST_ROOT, LAST_DOT, LAST_DOTDOT, LAST_BIND};
     LAST_NORM 就是普通的路徑名;LAST_ROOT 是 “/”;LAST_DOT  和 LAST_DOTDOT  分別代表了 “.” 和 “..”;LAST_BIND 就是符號連結。
    下面接著來看 path_init,LOOKUP_ROOT 可以提供一個路徑作為根路徑,主要用於兩個系統呼叫 open_by_handle_at 和 sysctl,我們就不關注了。然後是根據路徑名設定起始位置,如果路徑是絕對路徑(以“/”開頭)的話,就把起始路徑指向程序的根目錄(1849);如果路徑是相對路徑,並且 dfd 是一個特殊值(AT_FDCWD),那就說明起始路徑需要指向當前工作目錄,也就是 pwd(1856);如果給了一個有效的 dfd,那就需要吧起始路徑指向這個給定的目錄(1871)。
        path_init  返回之後 nd 中的 path 就已經設定為起始路徑了,現在可以開始遍歷路徑了。在此之前我們先探討一下 Kernel 的檔案系統,特別是 vfs 的組織結構。vfs 是具體檔案系統(如 ext4、nfs、fat)和 Kernel 之間的橋樑,它將各個檔案系統抽象出來並提供一個統一的機制來組織和管理各個檔案系統,但具體的實現策略則由各個檔案系統來實現,這很好的遮蔽的各個檔案系統的差異,也非常容易擴充套件,這就是 Linux 著名格言“提供機制而不是策略”的具體實踐。vfs 中有兩個個很重要的資料結構 dentry 和 inode,dentry 就是“目錄項”儲存著諸如檔名、路徑等資訊;inode 是索引節點,儲存具體檔案的資料,比如許可權、修改日期、裝置號(如果是裝置檔案的話)等等。檔案系統中的所有的檔案(目錄也是一種特殊的檔案)都必有一個 inode 與之對應,而每個 inode 也至少有一個 dentry 與之對應(也有可能有多個,比如硬連結)。結合下圖我們可以更清晰的理解這個架構:
【dentry-inode 結構圖】
        
    首先有這樣一個檔案:/home/user1/file1,它的目錄項中 d_parent 指標指向它所在目錄的目錄項 /home/user1(1),而這個目錄項中有一個雙向連結串列 d_subdirs,裡面連結著該目錄的子目錄項(2),所以 /home/user1/file1 目錄項裡的 d_u 也加入到了這個連結串列(3),這樣一個檔案上下關係就建立起來了。同樣,/home/user1 的 d_parent 將指向它的父目錄 /home,並且將自己的 d_u 連結到 /home 的 d_subdirs。file1 的目錄項中有一個 d_inode 指標,指向一個 inode 結構(4),這個就是該檔案的索引節點了,並且 file1 目錄項裡的 d_alias 也加入到了 inode 的連結串列 i_dentry 中(5),這樣 dentry 和 inode 的關係也建立起來了。前面講過,如果一個檔案的硬連線不止一個的話就會有多個 dentry 與 inode 相關聯,請看圖中 /home/user2/file2,它和 file1 互為硬連結。和 file1 一樣,file2 也把自己的 d_inode 指向這個 inode 結構(6)並且把 d_alias 加入到了 inode 的連結串列 i_dentry 裡(7)。這樣無論是通過 /home/user1/file1 還是 /home/user2/file2,訪問的都是同一個檔案。還有,目錄也是允許硬連結的,只不過不允許普通使用者建立目錄的硬連結。
    但是 Kernel 並不直接使用這樣的結構來進行路徑的遍歷,為了提高效率 Kernel 使用雜湊陣列來組織這些 dentry 和 inode,這已經超出我們的討論範圍了,所以知道有這麼個東西就好了。現在我們可以回到 path_openat 接著我們的旅行了。
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat

點選(此處)摺疊或開啟

  ...
  1.     current->total_link_count = 0;
  2.     error = link_path_walk(pathname->name, nd);
  3.     if (unlikely(error))
  4.         goto out;

  ...

    total_link_count 是用來記錄符號連結的深度,每穿越一次符號連結這個值就加一,最大允許 40 層符號連結。接下來 link_path_walk 會帶領我們走向目標,並在到達最終目標所在目錄的時候停下來(最終目標需要交給另一個函式 do_last 單獨處理)。下面我們就來看看這個函式是怎樣一步一步接近目標的。
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk

點選(此處)摺疊或開啟

  1. static int link_path_walk(const char *name, struct nameidata *nd)
  2. {

  ...

  1.     while (*name=='/')
  2.         name++;
  3.     if (!*name)
  4.         return 0;

  ...

    首先略過連續的“/”(可以試試這個命令“ls /////dev/”,看看有什麼效果),如果此時路徑就結束了那就相當於整個路徑只有一個“/”(1741),還記得在 init_path() 裡 nd->last_type 的初始值是什麼麼?沒錯這就是 LAST_ROOT。那麼如果路徑沒有結束呢,那就說明我們至少擁有了一個真正的子路徑,這就需要進入 1745 行這個大大的迴圈體來一步一步的走下去,所以連函式名都叫做“路徑行走”嘛。
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk

點選(此處)摺疊或開啟

  ...

  1.     for(;;) {

  ...

  1.         err = may_lookup(nd);

  ...

  1.         type = LAST_NORM;
  2.         if (name[0] == '.') switch (len) {
  3.             case 2:
  4.                 if (name[1] == '.') {
  5.                     type = LAST_DOTDOT;
  6.                     nd->flags |= LOOKUP_JUMPED;
  7.                 }
  8.                 break;
  9.             case 1:
  10.                 type = LAST_DOT;
  11.         }
  12.         if (likely(type == LAST_NORM)) {
  13.             struct dentry *parent = nd->path.dentry;
  14.             nd->flags &= ~LOOKUP_JUMPED;
  15.             if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
  16.                 err = parent->d_op->d_hash(parent, &this);
  17.                 if (err < 0)
  18.                     break;
  19.             }
  20.         }

  21.         nd->last = this;
  22.         nd->last_type = type;

  23.         if (!name[len])
  24.             return 0;
  25.         /*
  26.          * If it wasn't NUL, we know it was '/'. Skip that
  27.          * slash, and continue until no more slashes.
  28.          */
  29.         do {
  30.             len++;
  31.         } while (unlikely(name[len] == '/'));
  32.         if (!name[len])
  33.             return 0;

  34.         name += len;

  ...

        首先是例行安全檢查(1750),然後就看子路徑名是否是“.”或“..”並做好標記(1759-1768)。如果不是“.”或“..”那麼這就是一個普通的路徑名,此時還要看看這個當前目錄項是否需要重新計算一下雜湊值(1772)。現在可以先把子路徑名更新一下(1779),如果此時已經到達了最終目標,那麼“路徑行走”的任務就完成了(1782 和 1791)。如果路徑還沒到頭,那麼現在就一定是一個“/”,再次略過連續的“/”(1790)並讓 name 指向下一個子路徑(1794),為下一次迴圈做好了準備。

        到現在為止我們第一天的行程已經結束了,好好休息準備明天的旅程吧。

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