linux系統呼叫open七日遊(一)
阿新 • • 發佈:2018-11-08
友情提示:您需要一個 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()
現在來看 open 的主函式 do_sys_open。
【fs/open.c】sys_open()->do_sys_open()
build_open_flags 就是對標誌位進行檢查,然後包裝成 struct open_flags 結構以供接下來的函式使用。因為這些標誌位大多涉及到對最終目標檔案的操作,所以這個函式也等到我們用到這些標誌位的時候再回過頭來看。
接下來就是 getname ,這個函式定義在 fs/namei.c,主體是 getname_flags,我們撿重點的分析,無關緊要的程式碼以 ... 略過:
【fs/namei.c】 sys_open()->do_sys_open()->getname()->getname_flags()
回到 do_sys_open,現在需要為新開啟的檔案分配空閒檔案描述符。get_unused_fd_flags 主要作用就是在當前程序的 files 結構中找到空閒的檔案描述符,並初始化該描述符對應的 file 結構。
一切準備就緒,就進入 do_filp_open 了。
【fs/namei.c】sys_open->do_sys_open->do_filp_open
path_openat 主要作用是首先為 struct file 申請記憶體空間,設定遍歷路徑的初始狀態,然後遍歷路徑並找到最終目標的父節點,最後根據目標的型別和標誌位完成 open 操作,最終返回一個新的 file 結構。我們分段來看:
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat
【include/linux/namei.h】
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat->path_init
【include/linux/namei.h】
下面接著來看 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
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat->link_path_walk
我們將以 Linux 系統呼叫 open 為主線,參觀遊覽 Kernel 的檔案系統,一窺 Kernel 檔案系統精妙的設計和嚴謹的實現。因受篇幅限制,我們此次觀光只涉足 Kernel 的虛擬檔案系統(vfs),對於具體的檔案系統就不深入進去了。
各位,準備好了嗎?我們已經迫不及待要開始這次奇幻之旅了!
“前往 Kernel 的旅客請注意,您所乘坐的 OPEN1024 航班已經開始登機......”
好了,我們的 OPEN1024 航班已經通過 Linux 系統呼叫穿越了中斷門來到了核心空間,並且通過系統呼叫表(sys_call_table)到達了系統呼叫 open 的主程式 sys_open,這就開始我們的旅程吧。
【fs/open.c 】 sys_open()
點選(此處)摺疊或開啟
- SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t,
- {
- if (force_o_largefile())
- flags |= O_LARGEFILE;
- return do_sys_open(AT_FDCWD, filename, flags, mode);
- }
- long sys_open(const char __user *filename, int flags, umode_t mode)
現在來看 open 的主函式 do_sys_open。
【fs/open.c】sys_open()->do_sys_open()
點選(此處)摺疊或開啟
- long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
- {
- struct open_flags op;
- int fd = build_open_flags(flags, mode, &op);
- struct filename *tmp;
- if (fd)
- return fd;
- tmp = getname(filename);
- if (IS_ERR(tmp))
- return PTR_ERR(tmp);
- fd = get_unused_fd_flags(flags);
- if (fd >= 0) {
- struct file *f = do_filp_open(dfd, tmp, &op);
- if (IS_ERR(f)) {
- put_unused_fd(fd);
- fd = PTR_ERR(f);
- } else {
- fsnotify_open(f);
- fd_install(fd, f);
- }
- }
- putname(tmp);
- return fd;
- }
build_open_flags 就是對標誌位進行檢查,然後包裝成 struct open_flags 結構以供接下來的函式使用。因為這些標誌位大多涉及到對最終目標檔案的操作,所以這個函式也等到我們用到這些標誌位的時候再回過頭來看。
接下來就是 getname ,這個函式定義在 fs/namei.c,主體是 getname_flags,我們撿重點的分析,無關緊要的程式碼以 ... 略過:
【fs/namei.c】 sys_open()->do_sys_open()->getname()->getname_flags()
點選(此處)摺疊或開啟
- static struct filename *
- getname_flags(const char __user *filename, int flags, int *empty)
- {
- struct filename *result, *err;
- result = __getname();
- kname = (char *)result + sizeof(*result);
- result->name = kname;
- result->separate = false;
- max = EMBEDDED_NAME_MAX;
- recopy:
- len = strncpy_from_user(kname, filename, max);
...
- if (len == EMBEDDED_NAME_MAX && max == EMBEDDED_NAME_MAX) {
- kname = (char *)result;
- result = kzalloc(sizeof(*result), GFP_KERNEL);
...
- result->name = kname;
- result->separate = true;
- max = PATH_MAX;
- goto recopy;
- }
- }
回到 do_sys_open,現在需要為新開啟的檔案分配空閒檔案描述符。get_unused_fd_flags 主要作用就是在當前程序的 files 結構中找到空閒的檔案描述符,並初始化該描述符對應的 file 結構。
一切準備就緒,就進入 do_filp_open 了。
【fs/namei.c】sys_open->do_sys_open->do_filp_open
點選(此處)摺疊或開啟
- struct file *do_filp_open(int dfd, struct filename *pathname,
- const struct open_flags *op)
- {
- struct nameidata nd;
- int flags = op->lookup_flags;
- struct file *filp;
- filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
- if (unlikely(filp == ERR_PTR(-ECHILD)))
- filp = path_openat(dfd, pathname, &nd, op, flags);
- if (unlikely(filp == ERR_PTR(-ESTALE)))
- filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_REVAL);
- return filp;
- }
path_openat 主要作用是首先為 struct file 申請記憶體空間,設定遍歷路徑的初始狀態,然後遍歷路徑並找到最終目標的父節點,最後根據目標的型別和標誌位完成 open 操作,最終返回一個新的 file 結構。我們分段來看:
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat
點選(此處)摺疊或開啟
- static struct file *path_openat(int dfd, struct filename *pathname,
- struct nameidata *nd, const struct open_flags *op, int flags)
- {
...
- file = get_empty_filp();
- if (IS_ERR(file))
- return file;
- file->f_flags = op->open_flag;
...
- error = path_init(dfd, pathname->name, flags | LOOKUP_PARENT, nd, &base);
- if (unlikely(error))
- goto out;
...
首先需要分配一個 file 結構,成功的話 get_empty_filp 會返回一個指向該結構的指標,在這個函式裡會對許可權、最大檔案數進行檢查。我們忽略有關 tempfile 的處理,直接來看 path_init。path_init 是對真正遍歷路徑環境的初始化,主要就是設定變數 nd。這個 nd 是 do_filp_open 裡定義的區域性變數,是一個臨時性的資料結構,用來儲存遍歷路徑的中間結果,其結構體定義如下:【include/linux/namei.h】
點選(此處)摺疊或開啟
- struct nameidata {
- struct path path;
- struct qstr last;
- struct path root;
- struct inode *inode; /* path.dentry.d_inode */
- unsigned int flags;
- unsigned seq, m_seq;
- int last_type;
- unsigned depth;
- char *saved_names[MAX_NESTED_LINKS + 1];
- };
【fs/namei.c】 sys_open->do_sys_open->do_filp_open->path_openat->path_init
點選(此處)摺疊或開啟
- static int path_init(int dfd, const char *name, unsigned int flags,
- struct nameidata *nd, struct file **fp)
- {
- int retval = 0;
- nd->last_type = LAST_ROOT; /* if there are only slashes... */
- nd->flags = flags | LOOKUP_JUMPED;
- nd->depth = 0;
- if (flags & LOOKUP_ROOT) {
- }
- nd->root.mnt = NULL;
- nd->m_seq = read_seqbegin(&mount_lock);
- if (*name=='/') {
- set_root(nd);
...
- nd->path = nd->root;
- } else if (dfd == AT_FDCWD) {
...
- get_fs_pwd(current->fs, &nd->path);
...
- } else {
...
- }
- nd->inode = nd->path.dentry->d_inode;
- return 0;
- }
【include/linux/namei.h】
點選(此處)摺疊或開啟
- /*
- * Type of the last component on LOOKUP_PARENT
- */
- enum {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
點選(此處)摺疊或開啟
...- current->total_link_count = 0;
- error = link_path_walk(pathname->name, nd);
- if (unlikely(error))
- 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
點選(此處)摺疊或開啟
- static int link_path_walk(const char *name, struct nameidata *nd)
- {
...
- while (*name=='/')
- name++;
- if (!*name)
- 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
點選(此處)摺疊或開啟
...
- for(;;) {
...
- err = may_lookup(nd);
...
- type = LAST_NORM;
- if (name[0] == '.') switch (len) {
- case 2:
- if (name[1] == '.') {
- type = LAST_DOTDOT;
- nd->flags |= LOOKUP_JUMPED;
- }
- break;
- case 1:
- type = LAST_DOT;
- }
- if (likely(type == LAST_NORM)) {
- struct dentry *parent = nd->path.dentry;
- nd->flags &= ~LOOKUP_JUMPED;
- if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
- err = parent->d_op->d_hash(parent, &this);
- if (err < 0)
- break;
- }
- }
- nd->last = this;
- nd->last_type = type;
- if (!name[len])
- return 0;
- /*
- * If it wasn't NUL, we know it was '/'. Skip that
- * slash, and continue until no more slashes.
- */
- do {
- len++;
- } while (unlikely(name[len] == '/'));
- if (!name[len])
- return 0;
- name += len;
...
首先是例行安全檢查(1750),然後就看子路徑名是否是“.”或“..”並做好標記(1759-1768)。如果不是“.”或“..”那麼這就是一個普通的路徑名,此時還要看看這個當前目錄項是否需要重新計算一下雜湊值(1772)。現在可以先把子路徑名更新一下(1779),如果此時已經到達了最終目標,那麼“路徑行走”的任務就完成了(1782 和 1791)。如果路徑還沒到頭,那麼現在就一定是一個“/”,再次略過連續的“/”(1790)並讓 name 指向下一個子路徑(1794),為下一次迴圈做好了準備。到現在為止我們第一天的行程已經結束了,好好休息準備明天的旅程吧。
轉自:http://blog.chinaunix.net/uid-20522771-id-4419666.html