走马观花: Linux 系统调用 open 七日游(二)-阿里云开发者社区

开发者社区> 开发与运维> 正文

走马观花: Linux 系统调用 open 七日游(二)

简介:     接着昨日的旅程,我们应该开始处理具体的子路径了: 【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

点击(此处)折叠或打开

  ...

  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 操作系统》王柏生

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章