走马观花: 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 就可以睡眠了,那我们也休息一下,明天再接着去游览有趣的符号链接。
目录
相关文章
|
26天前
|
网络协议 Linux 调度
深入探索Linux操作系统的心脏:内核与系统调用####
本文旨在揭开Linux操作系统中最为核心的部分——内核与系统调用的神秘面纱,通过生动形象的语言和比喻,让读者仿佛踏上了一段奇妙的旅程,从宏观到微观,逐步深入了解这两个关键组件如何协同工作,支撑起整个操作系统的运行。不同于传统的技术解析,本文将以故事化的方式,带领读者领略Linux内核的精妙设计与系统调用的魅力所在,即便是对技术细节不甚了解的读者也能轻松享受这次知识之旅。 ####
|
23天前
|
缓存 算法 安全
深入理解Linux操作系统的心脏:内核与系统调用####
【10月更文挑战第20天】 本文将带你探索Linux操作系统的核心——其强大的内核和高效的系统调用机制。通过深入浅出的解释,我们将揭示这些技术是如何协同工作以支撑起整个系统的运行,同时也会触及一些常见的误解和背后的哲学思想。无论你是开发者、系统管理员还是普通用户,了解这些基础知识都将有助于你更好地利用Linux的强大功能。 ####
35 1
|
2月前
|
Docker 容器
14 response from daemon: open \\.\pipe\docker_engine_linux: The system cannot find the file speci
14 response from daemon: open \\.\pipe\docker_engine_linux: The system cannot find the file speci
35 1
|
3月前
|
项目管理 敏捷开发 开发框架
敏捷与瀑布的对决:解析Xamarin项目管理中如何运用敏捷方法提升开发效率并应对市场变化
【8月更文挑战第31天】在数字化时代,项目管理对软件开发至关重要,尤其是在跨平台框架 Xamarin 中。本文《Xamarin 项目管理:敏捷方法的应用》通过对比传统瀑布方法与敏捷方法,揭示敏捷在 Xamarin 项目中的优势。瀑布方法按线性顺序推进,适用于需求固定的小型项目;而敏捷方法如 Scrum 则强调迭代和增量开发,更适合需求多变、竞争激烈的环境。通过详细分析两种方法在 Xamarin 项目中的实际应用,本文展示了敏捷方法如何提高灵活性、适应性和开发效率,使其成为 Xamarin 项目成功的利器。
53 1
|
3月前
|
Linux
揭秘Linux心脏:那些让你的编程事半功倍的主要系统调用
【8月更文挑战第31天】Linux中的系统调用是操作系统提供给应用程序的接口,用于请求内核服务,如文件操作、进程控制等。本文列举了22种主要系统调用,包括fork()、exec()、exit()、wait()、open()、close()、read()、write()等,并通过示例代码展示了如何使用fork()创建新进程及使用open()、write()、close()操作文件。这些系统调用是Linux中最基本的接口,帮助应用程序与内核交互。
47 1
|
3月前
|
C语言
Linux0.11 系统调用进程创建与执行(九)(下)
Linux0.11 系统调用进程创建与执行(九)
33 1
|
3月前
|
存储 Linux 索引
Linux0.11 系统调用进程创建与执行(九)(上)
Linux0.11 系统调用进程创建与执行(九)
75 1
|
3月前
|
安全 Linux 程序员
在Linux中,系统调用是什么?
在Linux中,系统调用是什么?
|
3月前
|
安全 Linux 程序员
在Linux中,什么是系统调用?举例说明其作用是什么?
在Linux中,什么是系统调用?举例说明其作用是什么?
|
3月前
|
存储 Linux API
Linux源码阅读笔记08-进程调度API系统调用案例分析
Linux源码阅读笔记08-进程调度API系统调用案例分析