走马观花: 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 就可以睡眠了,那我们也休息一下,明天再接着去游览有趣的符号链接。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值