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

简介:     回到 path_openat:【fs/namei.c】sys_open > do_sys_open > do_filp_open > path_openat 点击(此处)折叠或打开   .
    回到 path_openat:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat

点击(此处)折叠或打开

  ...

  1.     error = do_last(nd, &path, file, op, &opened, pathname);
  2.     while (unlikely(error > 0)) { /* trailing symlink */
  3.         struct path link = path;
  4.         void *cookie;
  5.         if (!(nd->flags & LOOKUP_FOLLOW)) {
  6.             path_put_conditional(&path, nd);
  7.             path_put(&nd->path);
  8.             error = -ELOOP;
  9.             break;
  10.         }
  11.         error = may_follow_link(&link, nd);
  12.         if (unlikely(error))
  13.             break;
  14.         nd->flags |= LOOKUP_PARENT;
  15.         nd->flags &= ~(LOOKUP_OPEN|LOOKUP_CREATE|LOOKUP_EXCL);
  16.         error = follow_link(&link, nd, &cookie);
  17.         if (unlikely(error))
  18.             break;
  19.         error = do_last(nd, &path, file, op, &opened, pathname);
  20.         put_link(nd, &link, cookie);
  21.     }

  ...

    其实到现在为止我们的旅程才进行了一半,还记得吗,我们现在只是顺着路径走到了最终目标所在的目录,对最终目标还没有进行任何处理,甚至连这个最终目标到底存不存在我们都不知道。这一切都会交给 do_last 来完成,如果一切顺利它会填充 file 结构并返回 0;如果返回值大于 0(其实还是 1,为什么是“还是”?回头看看 walk_component 你就明白了),那就表明这个最终目标是一个符号链接且需要跟随这个符号链接,那么就要进入 3194 行这个循环来顺着符号链接找到真正目标(还有一种情况是虽然最终目标是一个符号链接但我们只想得到这个符号链接本身,也不会进入这个循环)。符号链接的处理我们应该很熟悉了,只不过因为是最终目标所以增加了一些对标志位的判断和处理,大家理解这段代码应该不会有什么问题。
    接下来我们就要进入 do_last 了,首先快速浏览一遍这个函数,它有 228 行,而且遍地是 if,到处是 goto,谁第一次看到它都会发懵,根本就无从下手。但是只要我们抓住本质,一切事物都有规律可循,比如这个 do_last,可以说它是既复杂又简单。说它复杂是因为这里面进行了大量的标志位的检查,这些标志位是用户传进来指示 Kernel 需要打开什么样的文件、打开这些文件的模式以及怎么打开这些文件的,这些标志位总共有二十几个而且还相互影响相互制约,盘根错节的能不复杂吗?说它简单,那是和 link_path_walk 相比,在逻辑上和结构上相对简单,虽然这里也会遇到诸如跟随“..”、跟随符号链接、跟随挂载点,但这都是我们熟悉的老朋友了,没有什么新奇的东西。
    既然 do_last 的复杂之处是在于对各个标志位的判断,那么我们至少需要了解设置这些标志位的目的,这可以参考 Linux 最权威的文档——man 手册有关 open 的内容( http://man7.org/linux/man-pages/man2/open.2.html)。我们把这个手册当成指路牌,它可以引导我们理解 do_last 里对这些标志位的操作。其实用户设置的标志位并不是原封不动的传递进来,大家应该还记得在 do_sys_open 里会调用 build_open_flags 对这些标志位进行预处理和分装,所以我们可能还需要参考这个函数。
    事情是这样的话我们的 do_last 之行就不能像之前一样沿着代码一步一步边走边看了,如果那样的话我们很快就会迷失在 flag 的海洋之中,而且也会使我们本来愉快的旅行变得枯燥乏味。现在让我们用一种全新的方式游览 do_last 吧,我称之为“情景模式”。在情景模式中我们假设了几个 open 应用的场景,然后在我们想像的场景之中看看都有哪些标志位起到了怎样的作用。
【场景一】open(pathname, O_RDONLY)
    使用只读方式打开一个文件,这应该是最简单的 open 应用了。我们先来看看 build_open_flags 是怎么包装 O_RDONLY 的:
【fs/open.c】 sys_open > do_sys_open > build_open_flags

点击(此处)折叠或打开

  1. static inline int build_open_flags(int flags, umode_t mode, struct open_flags *op)
  2. {

  ...

  1.         op->mode = 0;

  ...

  1.         acc_mode = MAY_OPEN | ACC_MODE(flags);

  ...

  1.     op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN;

  ...

  1. }
    经过我们的简化,build_open_flags 和 O_RDONLY 相关的就剩这么三行了。我记得第一天说过,mode 代表文件权限只有在新建文件的时候才会用到,咱们当前的情景显然不是创建文件,所以先将 mode 置零(847)。接下来设置访问模式 acc_mode 这个东西主要用来进行权限检查,把宏 ACC_MODE 展开后这一行其实就是这样:acc_mode = MAY_OPEN | MAY_READ,其意思也很明白,那就是对于目标文件我们至少需要打开和读取的权限。最后是判断标志位中 O_PATH 有没有被设置,如果没有就将 intent 加上 LOOKUP_OPEN,intent 用来标记我们对最终目标想要做什么操作(intent 字面意思就是“意图”),所以在以后我们会看到这里暂存的 op->intent 会在 do_last 里重出江湖。
    build_open_flags 之后我们直接进入 do_last:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > do_last

点击(此处)折叠或打开

  1. static int do_last(struct nameidata *nd, struct path *path,
  2.          struct file *file, const struct open_flags *op,
  3.          int *opened, struct filename *name)
  4. {

  ...

  1.     nd->flags &= ~LOOKUP_PARENT;
  2.     nd->flags |= op->intent;

  ...

  1.     if (!(open_flag & O_CREAT)) {

  ...

  1.         error = lookup_fast(nd, path, &inode);
  2.         if (likely(!error))
  3.             goto finish_lookup;

  ...

  1.     }

  2. retry_lookup:

  ...

    LOOKUP_PARENT 实在 patn_init 的时候设置的,当时我们的目标是找到最终文件的父目录,但现在我们要找的就是最终文件,所以需要将这个标志位清除(2888),紧接着 intent 重现江湖(2889)。很明显我们当前的情况不是创建文件,所以会进入 2898 这个 if,这里有个熟面孔 lookup_fast,看到老朋友就是高兴。还记得 lookup_fast 执行结构有几种情况么?如果返回 0,则表示成功找到;返回 1,表示内存中没有,需要 lookup_real;返回小于 0,则表示需要从当前 rcu-walk 转到 ref-walk。那现在我们先看看返回 1 的情况:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > do_last

点击(此处)折叠或打开

  ...

  1. retry_lookup:

  ...

  1.     mutex_lock(&dir->d_inode->i_mutex);
  2.     error = lookup_open(nd, path, file, op, got_write, opened);
  3.     mutex_unlock(&dir->d_inode->i_mutex);

  ...

  1.     error = follow_managed(path, nd->flags);

  ...

  1.     inode = path->dentry->d_inode;
    lookup_fast 返回 1 的话程序会直接走到标号 retry_lookup 处。现在的程序已经肯定不在 rcu-walk 模式里了(为什么?),所以可以使用各种有可能引起进程阻塞的锁来占有相应的资源了(2941)。接下来是一个新朋友 lookup_open,说是新朋友其实是新瓶装旧酒,因为它和 lookup_slow 很像,都是先使用 lookup_dcache 在内存中找,如果不行就启动 lookup_real 在具体文件系统里面去找,当它成功返回时会将 path 指向找到的目标。接下来是 follow_managed 它也算是老朋友吧,之前我们简单介绍过的。再往下走,我们来到了 finish_lookup:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > do_last

点击(此处)折叠或打开

  ...

  1. finish_lookup:

  ...

  1.     if ((nd->flags & LOOKUP_RCU) || nd->path.mnt != path->mnt) {
  2.         path_to_nameidata(path, nd);
  3.     } else {
  4.         save_parent.dentry = nd->path.dentry;
  5.         save_parent.mnt = mntget(path->mnt);
  6.         nd->path.dentry = path->dentry;

  7.     }
  8.     nd->inode = inode;
    这里是 lookup_fast 返回 1 和返回 0 的交汇点。这时就需要更新 nd 了,但这个交汇点有两个来源也就是说现在有可能还在 rcu-walk 模式当中,所以还需要分情况处理一下(3014)。请注意这个 if 的第二个条件“nd->path.mnt != path->mnt”,什么情况下会出现这两个 mnt 不相等呢?还记得 nd 的脾气吗,当遇到挂载点的时候 nd 会原地踏步,只有 path 才大无畏的向前走。既然两个 mnt 不一样了,那么更新 nd 前也许要放弃原先占有的结构,这就是 path_to_nameidata 所做。接下来就要彻底告别 rcu-walk 了:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > do_last

点击(此处)折叠或打开

  ...

  1. finish_open:
  2.     error = complete_walk(nd);

  ...

    我们到 complete_walk 里面看看 rcu-walk 告别仪式:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > do_last > complete_walk

点击(此处)折叠或打开

  1. static int complete_walk(struct nameidata *nd)
  2. {
  3.     struct dentry *dentry = nd->path.dentry;
  4.     int status;

  5.     if (nd->flags & LOOKUP_RCU) {
  6.         nd->flags &= ~LOOKUP_RCU;

  ...

  1.         rcu_read_unlock();
  2.     }

  3.     if (likely(!(nd->flags & LOOKUP_JUMPED)))
  4.         return 0;

  ...

  1. }
    告别 rcu-walk 其实很简单,最主要的就是 605 和 624 这两行,rcu_read_unlock 之后就会重新启动进程抢占,本进程就有可能被切换出去,这样的话当前 CPU 就会经过 quiescent state,当所有 CPU 都经过了自己的 quiescent state 之后,在 rcu-walk 期间的变更才会被更新。接着如果当前不是 LOOKUP_JUMPED,就会直接返回。这个 LOOKUP_JUMPED 在哪里会被设置呢?其实我们之前遇到了很多次设置 LOOKUP_JUMPED 的地方,比方说遇到“..”的时候、遇到挂载点的时候、符号链接是绝对路径的时候,它们的共同点就是有可能会跨越(jump)文件系统。如果的确跨越了文件系统也很简单,检查一下当前 dentry 需不需要验证( DCACHE_OP_WEAK_REVALIDATE),大部分情况是不需要,所以这段代码咱们也省略了,有兴趣的同学请查阅 Kernel 源代码。
    接下来终于要真正“打开”这个文件了:
【fs/namei.c】 sys_open > do_sys_open > do_filp_open > path_openat > do_last

点击(此处)折叠或打开

  ...

  1. finish_open_created:
  2.     error = may_open(&nd->path, acc_mode, open_flag);
  3.     if (error)
  4.         goto out;
  5.     file->f_path.mnt = nd->path.mnt; 
  6.     error = finish_open(file, nd->path.dentry, NULL, opened);
  7.     if (error) {
  8.         if (error == -EOPENSTALE)
  9.             goto stale_open;
  10.         goto out;
  11.     }
  12. opened:

  ...

  1. out:
  2.     if (got_write)
  3.         mnt_drop_write(nd->path.mnt);
  4.     path_put(&save_parent);
  5.     terminate_walk(nd);
  6.     return error;

  ...

  1. }
    may_open 就是权限和标志位的检查,咱们就懒得进去看了。finish_open 会真正打开这个我们期待已久的目标文件,里面主要是利用该文件系统自己的 file_operations.open 来填充 file 结构。如果一切顺利 finish_open 返回 0,然后释放占用的资源之后就大功告成可以返回了。
    这样,我们的情景一就结束了,休息一下,咱们再看看稍微复杂一点的情景。
目录
相关文章
|
17天前
|
网络协议 Linux 调度
深入探索Linux操作系统的心脏:内核与系统调用####
本文旨在揭开Linux操作系统中最为核心的部分——内核与系统调用的神秘面纱,通过生动形象的语言和比喻,让读者仿佛踏上了一段奇妙的旅程,从宏观到微观,逐步深入了解这两个关键组件如何协同工作,支撑起整个操作系统的运行。不同于传统的技术解析,本文将以故事化的方式,带领读者领略Linux内核的精妙设计与系统调用的魅力所在,即便是对技术细节不甚了解的读者也能轻松享受这次知识之旅。 ####
|
13天前
|
缓存 算法 安全
深入理解Linux操作系统的心脏:内核与系统调用####
【10月更文挑战第20天】 本文将带你探索Linux操作系统的核心——其强大的内核和高效的系统调用机制。通过深入浅出的解释,我们将揭示这些技术是如何协同工作以支撑起整个系统的运行,同时也会触及一些常见的误解和背后的哲学思想。无论你是开发者、系统管理员还是普通用户,了解这些基础知识都将有助于你更好地利用Linux的强大功能。 ####
24 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
32 1
|
3月前
|
项目管理 敏捷开发 开发框架
敏捷与瀑布的对决:解析Xamarin项目管理中如何运用敏捷方法提升开发效率并应对市场变化
【8月更文挑战第31天】在数字化时代,项目管理对软件开发至关重要,尤其是在跨平台框架 Xamarin 中。本文《Xamarin 项目管理:敏捷方法的应用》通过对比传统瀑布方法与敏捷方法,揭示敏捷在 Xamarin 项目中的优势。瀑布方法按线性顺序推进,适用于需求固定的小型项目;而敏捷方法如 Scrum 则强调迭代和增量开发,更适合需求多变、竞争激烈的环境。通过详细分析两种方法在 Xamarin 项目中的实际应用,本文展示了敏捷方法如何提高灵活性、适应性和开发效率,使其成为 Xamarin 项目成功的利器。
49 1
|
3月前
|
Linux
揭秘Linux心脏:那些让你的编程事半功倍的主要系统调用
【8月更文挑战第31天】Linux中的系统调用是操作系统提供给应用程序的接口,用于请求内核服务,如文件操作、进程控制等。本文列举了22种主要系统调用,包括fork()、exec()、exit()、wait()、open()、close()、read()、write()等,并通过示例代码展示了如何使用fork()创建新进程及使用open()、write()、close()操作文件。这些系统调用是Linux中最基本的接口,帮助应用程序与内核交互。
43 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系统调用案例分析