走马观花: 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 > 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 就可以睡眠了,那我们也休息一下,明天再接着去游览有趣的符号链接。
目录
相关文章
|
2月前
|
算法 Linux C++
【Linux系统编程】解析获取和设置文件信息与权限的Linux系统调用
【Linux系统编程】解析获取和设置文件信息与权限的Linux系统调用
29 0
|
2月前
|
存储 Linux 程序员
Linux中的主要系统调用
【2月更文挑战第16天】
|
3月前
|
Linux API 数据安全/隐私保护
Linux下的系统编程——系统调用(五)
Linux下的系统编程——系统调用(五)
45 0
Linux下的系统编程——系统调用(五)
|
7月前
|
Linux
Linux中Too many open files 问题分析和解决
Linux中Too many open files 问题分析和解决
|
6月前
|
存储 Linux 调度
【看表情包学Linux】系统下的文件操作 | 文件系统接口 | 系统调用与封装 | open,write,close 接口 | 系统传递标记位 O_RDWR,O_RDONLY,O_WRONLY...
【看表情包学Linux】系统下的文件操作 | 文件系统接口 | 系统调用与封装 | open,write,close 接口 | 系统传递标记位 O_RDWR,O_RDONLY,O_WRONLY...
41 1
|
24天前
|
Linux 开发者
Linux文件编程(open read write close函数)
通过这些函数,开发者可以在Linux环境下进行文件的读取、写入和管理。 买CN2云服务器,免备案服务器,高防服务器,就选蓝易云。百度搜索:蓝易云
88 4
|
2月前
|
存储 监控 Linux
Linux 使用getrusage系统调用获取cpu信息:一个C++实例分析
Linux 使用getrusage系统调用获取cpu信息:一个C++实例分析
50 0
|
2月前
|
存储 缓存 Linux
Linux 系统调用深思:从原理到实战
Linux 系统调用深思:从原理到实战
41 1
|
2月前
|
缓存 Linux 编译器
C/C++ 函数调用以及Linux中系统调用 开销介绍:介绍C/C函数调用以及Linux中系统调用的开销情况
C/C++ 函数调用以及Linux中系统调用 开销介绍:介绍C/C函数调用以及Linux中系统调用的开销情况
17 0
|
6月前
|
存储 缓存 Linux
【看表情包学Linux】冯诺依曼架构 | 理解操作系统 | 基于 Pintos 实现新的用户级程序的系统调用
【看表情包学Linux】冯诺依曼架构 | 理解操作系统 | 基于 Pintos 实现新的用户级程序的系统调用
67 1