坚持高质量原创,拒绝内容堆砌,喜欢的话点击上方星标,更新第一时间收到提醒,谢谢关注! 前两节我们已经把 VFS 的四大核心对象全部拆解完毕:super_block 描述文件系统,inode 描述文件元数据,dentry 缓存路径名到 inode 的映射,file 描述一次打开的会话。 前面第三第四章在讲驱动和系统调用的时候其实都有所涉及open read流程。 但是受限于知识体系不完整所以都只能点到为止,在我们完成了VFS架构的讲解之后,整套流程的体系已经完整。 所以这一节我们就来:用一个完整的 open("/dev/gpio", O_RDWR) + read(fd, buf, 10) 流程。 看看从用户态的一行 C 代码开始,内核是如何一步一步经过 VFS,最终调用到我们驱动中的 .open 和 .read 方法的。 在各个关键流程的位置我也都为大家配了流程图。 这个过程中,我们重点关注两件事: 路径解析:内核如何把 "/dev/gpio" 这个字符串,变成一个 inode 操作表绑定:VFS 如何把我们驱动注册的 file_operations 绑定到 struct file 上,让后续的 read/write 能找到正确的驱动方法 开始前的一图流,数据结构关系图: open 函数调用流程: 1. open 全流程 1.1 系统调用入口:do_sys_openat2 用户态调用 open("/dev/gpio", O_RDWR) 后,经过我们在第四章分析过的系统调用机制(glibc → svc → 异常向量 → sys_call_table),最终进入内核的 do_sys_openat2()。 我们直接看它的源码,定义在 fs/open.c: static int do_sys_openat2(int dfd, const char __user *filename, struct open_how *how){ /* ... 参数校验省略 ... */ // 将用户态路径字符串拷贝到内核空间 tmp = getname(filename); // ① 分配空闲 fd fd = get_unused_fd_flags(how->flags); // ② 路径解析 + 创建 file struct file *f = do_filp_open(dfd, tmp, &op); // ③ 将 file 安装到进程 fd 表 fd_install(fd, f); putname(tmp); return fd;}三步逻辑非常清晰: ① get_unused_fd_flags() —— 在进程的文件描述符表中找到第一个空闲槽位,返回其索引值作为 fd。这就是为什么 fd 通常是从 3 开始递增的小整数(0/1/2 被 stdin/stdout/stder 占了)。 ② do_filp_open() —— 这是 open 的核心引擎:解析路径、创建 struct file、绑定操作表、调用驱动的 .open。我们接下来重点分析。 ③ fd_install(fd, f) —— 将创建好的 struct file 指针存入进程 fd 表的对应槽位。完成之后,用户态拿到的 fd 整数就可以通过内核的 fdget() 找到对应的 struct file 了。 1.2 do_filp_open → path_openat:open 的核心引擎 do_sys_openat2 的第②步调用了 do_filp_open()。 这个函数是 open 流程的核心引擎入口: struct file *do_filp_open(int dfd, struct filename *pathname, const struct open_flags *op){ struct nameidata nd; int flags = op->lookup_flags; struct file *filp; set_nameidata(&nd, dfd, pathname, NULL); // 第一次:RCU 模式(快速路径) filp = path_openat(&nd, op, flags | LOOKUP_RCU); if (unlikely(filp == ERR_PTR(-ECHILD))) // 第二次:普通模式 filp = path_openat(&nd, op, flags); if (unlikely(filp == ERR_PTR(-ESTALE))) // 第三次:强制重新验证 filp = path_openat(&nd, op, flags | LOOKUP_REVAL); restore_nameidata(); return filp;}可以看到,do_filp_open 本身的逻辑很薄,核心工作全部委托给了 path_openat()。但它做了一件重要的事:三级降级重试。 内核会优先用 RCU 模式(无锁快速路径)调用 path_openat,绝大多数情况下一次就能成功。只有在遇到并发修改或缓存过期的情况下,才会降级到加锁模式甚至强制重新验证模式重试。 这种 "先走快路径,失败了再走慢路径" 的设计在内核中非常常见。 了解了 do_filp_open 的重试策略后,我们进入它真正干活的地方——path_openat()。 这是 VFS open 操作的主函数: static struct file *path_openat(struct nameidata *nd, const struct open_flags *op, unsigned flags){ // ① 分配空的 struct file file = alloc_empty_file(op->open_flag, current_cred()); // ② 初始化路径解析起点 constchar *s = path_init(nd, flags); // ③ 逐段解析路径 while (!(error = link_path_walk(s, nd)) && (s = open_last_lookups(nd, file, op)) != NULL) ; if (!error) // ④ 打开文件 error = do_open(nd, file, op); terminate_walk(nd); return file;}path_openat 是整个 open 流程的主线,下面四步完成后就是一次完整的"路径 → 文件"转换: ① alloc_empty_file() —— 从 slab 缓存中分配一个 struct file,初始化引用计数、自旋锁等基本字段。此时 f_op、f_inode、f_path 等关键字段都还是空的,要等路径解析完成后才填充。 ② path_init() —— 初始化路径解析的起点。对于绝对路径 "/dev/gpio",起点就是当前进程的根目录 dentry。 ③ link_path_walk() —— 逐段解析路径字符串中除了最后一个分量以外的所有中间路径。对于 "/dev/gpio",link_path_walk 负责解析 "dev" 这一段。这是路径解析的核心,下面会详细展开。 ④ do_open() —— 路径解析完成后,打开目标文件:绑定 f_op、调用 .open 方法。这部分也会展开来讲。 1.3 link_path_walk:路径解析的核心 link_path_walk() 是整个 open 流程中最关键的函数,定义在 fs/namei.c。 它的职责是:把路径字符串逐段拆解,每一段都通过 dentry 缓存或文件系统的 lookup 方法找到对应的 inode。 static int link_path_walk(const char *name, struct nameidata *nd){ /* 跳过路径开头的斜杠 */ while (*name == '/') name++; /* 逐段解析路径 */ for(;;) { // 权限检查:当前目录是否允许搜索 err = may_lookup(idmap, nd); // 计算当前分量的哈希值 name = hash_name(nd, name, &lastword); switch(lastword) { case LAST_WORD_IS_DOTDOT: /* ".." 回到父目录 */break; case LAST_WORD_IS_DOT: /* "." 当前目录 */ break; default: // 普通路径分量 nd->last_type = LAST_NORM; } // 到达路径末尾,解析完成 if (!*name) return0; /* 跳过分隔符斜杠,解析当前分量 */ // 解析当前分量 link = walk_component(nd, WALK_MORE); if (link) { /* 遇到符号链接,跟随 */ } if (!d_can_lookup(nd->path.dentry)) // 中间分量不是目录,报错 return -ENOTDIR; } }}这个函数的结构是一个 for(;;) 无限循环,每一轮处理路径中的一个分量。以 "/dev/gpio" 为例: 第一轮循环:处理 "dev" hash_name() 从字符串中提取 "dev" 这个分量,计算哈希值存入 nd->last lastword 不是 . 也不是 ..,走 LAST_NORM 分支 name 指向 "dev" 后面的 "/gpio",不为空,跳过斜杠后 name 指向 "gpio" 调用 walk_component(nd, WALK_MORE) 来真正解析 "dev" 这个分量 第二轮循环:处理 "gpio" hash_name() 提取 "gpio",计算哈希值 depth == 0,设置 nd->dir_mode,返回 0 注意 "gpio" 作为最后一个分量,不在 link_path_walk 中解析,而是留给后面的 open_last_lookups() 处理 所以 link_path_walk 只解析中间分量("dev"),最后一个分量("gpio")由 open_last_lookups 单独处理。 因为最后一个分量的处理逻辑和中间分量不同(可能需要创建文件、处理 O_CREAT 等标志)。 1.4 walk_component:dentry 缓存的快慢路径 link_path_walk 中每解析一个中间分量,都会调用 walk_component(): static const char *walk_component(struct nameidata *nd, int flags){ // "." 和 ".." 特殊处理 if (unlikely(nd->last_type != LAST_NORM)) return handle_dots(nd, nd->last_type); // 快速路径:查 dentry 缓存 dentry = lookup_fast(nd); if (unlikely(!dentry)) // 慢速路径 dentry = lookup_slow(&nd->last, nd->path.dentry, nd->flags); // 进入找到的 dentry return step_into(nd, flags, dentry);}1.4.1 lookup_fast:快速路径 我们在上一讲中讲过 dentry 可以通过缓存的方式来提升性能,路径解析性能的关键核心就是:快速路径 lookup_fast。 lookup_fast 内部实现其实挺复杂的,为了在高并发下依然保持高性能,用到了 RCU 无锁读、序列号校验等机制。 但抛开这些并发保护的细节,它的原理并不复杂,就是通过 "父目录 dentry 地址 + 当前分量名称" 算一个哈希值,然后到内核全局的 dentry 哈希表里去找。 如果找到了,说明之前有人访问过这个路径,内核已经把对应的 dentry 缓存在内存里了,这种情况下直接通过 dentry->d_inode 就能拿到 inode,整个过程只需要几次内存访问,完全不需要访问磁盘硬件。 对于我们的例子 "/dev/gpio" 来说,系统启动后 /dev 目录几乎一定会被访问过,所以 "dev" 这个分量的 dentry 大概率已经在缓存中了。所以日常使用中路径解析通常非常快,因为绝大多数路径分量都能在缓存中命中。 1.4.2 lookup_slow:慢速路径 如果 lookup_fast 没找到(缓存未命中),就只能走慢速路径了。 这时候内核没有捷径可走,必须去问具体的文件系统:"你的这个目录下面,有没有叫这个名字的文件?"具体来说,就是调用 inode->i_op->lookup()——这就是我们在 5.2 节讲过的 inode_operations->lookup 方法。 对于 ext4 这样的磁盘文件系统,这意味着要读取磁盘上的目录数据块,逐条比对文件名,找到对应的 inode 号再加载 inode。 所以慢的根本原因就是可能触发磁盘 I/O。对于 devtmpfs 这样的纯内存文件系统,虽然不需要磁盘 I/O,但依然比哈希表直接命中要慢。 不过慢速路径查找完成后,新创建的 dentry 会自动加入缓存。这样下次再有进程访问同样的路径时,就可以直接走快速路径了。 1.4.3 step_into:进入下一层 不管走的是快速还是慢速路径,最终都会拿到目标 dentry。 接下来 step_into() 负责把 nameidata 中的"当前位置"更新到这个新 dentry 上。 到这里,让我们再回到 path_openat 的全局流程。在之前的 1.2 节的代码中,path_openat 中路径解析相关的调用链是这样的: link_path_walk 用 walk_component 逐段解析了中间路径分量("dev") open_last_lookups 解析了最终分量("gpio"),拿到了 /dev/gpio 的 dentry 和 inode 路径解析阶段到此结束。接下来,path_openat 进入最后一步—— do_open(),真正去"打开"这个文件。 1.5 do_dentry_open:打开文件,绑定 f_op 路径解析完成后,内核已经拿到了 /dev/gpio 的 dentry 和 inode。现在 path_openat 调用它的最后一步:do_open()。 这个调用会一路走到 do_open() → vfs_open() → do_dentry_open()。 do_dentry_open() 是真正完成"打开"动作的函数,它的职责是:把路径解析阶段找到的 inode 和 struct file 关联起来,并绑定正确的操作表。 我们看关键代码: static int do_dentry_open(struct file *f, int (*open)(struct inode *, struct file *)){ // 从 dentry 拿到 inode struct inode *inode = f->f_path.dentry->d_inode; // ① 绑定 inode f->f_inode = inode; f->f_mapping = inode->i_mapping; /* ... 权限和模式检查 ... */ // ② 从 inode 获取默认 file_operations f->f_op = fops_get(inode->i_fop); /* ... 安全模块检查 ... */ if (!open) open = f->f_op->open; if (open) // ③ 调用 f_op->open error = open(inode, f); f->f_mode |= FMODE_OPENED;}三个关键步骤: ① f->f_inode = inode —— 将路径解析找到的 inode 绑定到 file 上。从这一刻起,struct file 就知道自己对应的是哪个文件了。 ② f->f_op = fops_get(inode->i_fop) —— 从 inode 获取默认的 file_operations。注意这里用的是 inode->i_fop——这个字段是在设备节点创建时就设好的。 那么问题来了:对于 /dev/gpio 这样的字符设备文件,inode->i_fop 到底指向什么? 这个问题的答案是理解整个 open 流程的关键,我们在 1.6 中详细展开。 ③ open(inode, f) —— 调用 f_op->open 方法。第②步刚把 f_op 设好,这一步就立刻调用它的 .open。对于字符设备来说,这一步实际调用的是 chrdev_open()——这也是下面要重点讲的。 1.6 字符设备的关键桥梁:def_chr_fops 与 chrdev_open 上面留下了两个问题:字符设备的 inode->i_fop 是什么?f_op->open 调用的又是谁?这两个问题的答案串在一起,构成了 VFS 连接驱动的关键桥梁。 1.6.1 设备节点的 inode 从何而来? 当我们在驱动中调用 device_create() 或手动执行 mknod /dev/gpio c 250 0 时,内核会在 /dev 所在的文件系统(通常是 devtmpfs)中创建一个 inode,并调用 init_special_inode() 来初始化它。 这个函数定义在 fs/inode.c: void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev){ inode->i_mode = mode; switch (inode->i_mode & S_IFMT) { case S_IFCHR: // 字符设备:绑定 def_chr_fops inode->i_fop = &def_chr_fops; inode->i_rdev = rdev; break; case S_IFBLK: /* 块设备 */break; case S_IFIFO: /* 管道 */ break; case S_IFSOCK: /* socket */break; }}可以看到,字符设备 inode 的 i_fop 被设置为 def_chr_fops。这不是我们驱动实现的 fops,而是 VFS 为所有字符设备节点提供的一个通用入口。 def_chr_fops 定义在 fs/char_dev.c /* * Dummy default file-operations: the only thing this does * is contain the open that then fills in the correct operations * depending on the special file... */const struct file_operations def_chr_fops = { .open = chrdev_open, .llseek = noop_llseek,};它几乎什么都没实现,唯一有意义的方法就是 .open = chrdev_open。 所以在 do_dentry_open 的第②步中,f->f_op 被设置为 def_chr_fops; 第③步调用 f->f_op->open(inode, f),实际调用的就是 chrdev_open()。 1.6.2 chrdev_open:从 VFS 跳转到驱动 chrdev_open 的使命是:根据 inode 中的设备号,找到我们驱动注册的 cdev,然后用驱动的 file_operations 替换掉 def_chr_fops。 定义在 fs/char_dev.c: static int chrdev_open(struct inode *inode, struct file *filp){ conststruct file_operations *fops; struct cdev *p; p = inode->i_cdev; if (!p) { /* ① 通过设备号在 cdev_map 中查找 cdev */ kobj = kobj_lookup(cdev_map, inode->i_rdev, &idx); new = container_of(kobj, struct cdev, kobj); /* ② 缓存到 inode(DCL 并发保护省略) */ inode->i_cdev = p = new; list_add(&inode->i_devices, &p->list); } // ③ 获取驱动的 file_operations fops = fops_get(p->ops); // ④ 替换 file->f_op replace_fops(filp, fops); if (filp->f_op->open) // ⑤ 调用驱动的 .open ret = filp->f_op->open(inode, filp); return ret;}逐步分析: ① kobj_lookup(cdev_map, inode->i_rdev, &idx) —— inode->i_rdev 保存的是设备号 MKDEV(250, 0),kobj_lookup() 用这个设备号在全局的 cdev_map 中查找。cdev_map 中的映射关系是我们在驱动 init 中通过 cdev_add() 注册进去的。 ② inode->i_cdev = p = new —— 将找到的 cdev 指针缓存到 inode 上。下次再有进程 open 同一个设备文件时,直接从 inode 上就能拿到 cdev,不需要再去 cdev_map 中查找。注意这里还有一个 double-check 的并发保护:先解锁去查 cdev_map,再加锁检查是否有人抢先赋值了——这是典型的 DCL(Double-Checked Locking)模式。 ③ fops_get(p->ops) —— 从 cdev 中获取驱动注册的 file_operations,并增加模块引用计数。 ④ replace_fops(filp, fops) —— 这一行是整个 open 流程的关键转折点。执行之前,file->f_op 指向 def_chr_fops(VFS 通用入口);执行之后,file->f_op 指向我们驱动的 my_gpio_fops。从这一刻起,VFS 和驱动之间的桥梁就建立完成了。 ⑤ filp->f_op->open(inode, filp) —— 调用驱动的 .open 方法。这就是我们在驱动中写的 my_gpio_open() 被调用的时刻,通常在这里做硬件初始化、设置 file->private_data 等工作。 1.6.3 两次 f_op 替换 为了让大家更清楚,我把 file->f_op 在 open 过程中的变化总结一下: 时间点 | file->f_op 指向 | 来源 | | do_dentry_open 第②步 | def_chr_fops | 从 inode->i_fop 拷贝 | | chrdev_open 第④步 | my_gpio_fops (驱动的 fops) | 从 cdev->ops 替换 | open 返回后 | my_gpio_fops | 最终生效,后续 read/write 用这个 |
这个 "先绑定通用入口,再替换为驱动实现" 的两阶段设计,就是 VFS 实现设备文件多态的方式。 对于普通的 ext4 文件,inode->i_fop 直接就是 ext4_file_operations,不需要经过 chrdev_open 这一层转换。但字符设备因为种类繁多(成百上千种驱动),所以多了一层通过设备号查找 cdev 的间接层。 下图展示了 chrdev_open 中从 VFS 跳转到驱动的完整过程,核心是 replace_fops 这一步: 1.7 open 全流程总览 到这里 open 的完整流程就走完了,我们用一张完整的调用链路图把所有步骤串起来,并标注每一步涉及的核心对象: 用户态: fd = open("/dev/gpio", O_RDWR) │ ▼sys_openat → do_sys_openat2 ──────────────────────── fs/open.c│├─ ① get_unused_fd_flags()│ 分配 fd = 3│├─ ② do_filp_open() → path_openat() ─────────────── fs/namei.c│ ││ ├─ alloc_empty_file()│ │ 创建空的 struct file ── 【file 诞生】│ ││ ├─ path_init()│ │ 确定起点:进程 root dentry ── 【dentry】│ ││ ├─ link_path_walk("dev/gpio") ──────────────── fs/namei.c│ │ ││ │ ├─ 分量 "dev": walk_component() ───────── fs/namei.c│ │ │ ├─ lookup_fast() → __d_lookup_rcu()│ │ │ │ 在 dentry 哈希表中查找 ── 【dentry 缓存】│ │ │ │ 命中 → dentry("dev") → inode(/dev)│ │ │ └─ step_into()│ │ │ 跨越挂载点 → devtmpfs ── 【super_block】│ │ ││ │ └─ 分量 "gpio": 留给 open_last_lookups 处理│ ││ ├─ open_last_lookups()│ │ 解析最终分量 "gpio"│ │ → dentry("gpio") → inode(/dev/gpio) ── 【inode】│ │ i_mode = S_IFCHR│ │ i_rdev = MKDEV(250, 0)│ │ i_fop = &def_chr_fops│ ││ └─ do_open() → vfs_open() → do_dentry_open() ── fs/open.c│ ││ ├─ f->f_inode = inode│ ├─ f->f_op = inode->i_fop (= def_chr_fops)│ ││ └─ f->f_op->open() ═══ chrdev_open() ──── fs/char_dev.c│ ││ ├─ kobj_lookup(cdev_map, inode->i_rdev)│ │ → 找到驱动注册的 struct cdev ── 【cdev_map】│ ││ ├─ inode->i_cdev = cdev (缓存)│ ││ ├─ replace_fops(filp, cdev->ops)│ │ file->f_op = my_gpio_fops 关键替换!│ ││ └─ my_gpio_fops->open(inode, file)│ → 驱动的 .open 被调用│└─ ③ fd_install(fd=3, file) 将 file 安装到进程 fd 表 │ ▼ return fd = 3 → 用户态拿到文件描述符我们前文讲的 VFS 结构体在 open 中的角色: super_block:路径解析跨越挂载点时切换文件系统上下文(从 rootfs 切换到 devtmpfs) dentry:路径解析的每一段都依赖 dentry 缓存加速查找,是路径字符串到 inode 的桥梁 inode:路径解析的终点,保存文件类型(i_mode)、设备号(i_rdev)、默认操作表(i_fop),是 VFS 连接驱动的桥梁 file:open 的产物,持有当前会话的所有状态(f_op、f_pos、private_data),是用户后续所有 I/O 操作的入口 2. read 全流程 open 完成后,用户态拿到了 fd = 3,对应的 struct file 中 f_op 已经指向了我们驱动的 my_gpio_fops。 现在用户调用 read(fd, buf, 10)。 2.1 系统调用入口:ksys_read 定义在 fs/read_write.c:704: ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count){ // ① 从进程 fd 表中取出 struct file CLASS(fd_pos, f)(fd); // ② 调用 vfs_read ret = vfs_read(fd_file(f), buf, count, ppos); // ③ 更新文件读写位置 fd_file(f)->f_pos = pos; return ret;}① CLASS(fd_pos, f)(fd) —— 这是内核新版本中使用的 RAII 风格宏,本质上就是 fdget_pos(fd):用 fd 作为索引,在当前进程的 task_struct->files->fdt 数组中取出对应的 struct file *。这是一个 O(1) 的数组索引操作。 ② vfs_read() —— VFS 层的 read 入口。 ③ fd_file(f)->f_pos = pos —— 读取完成后,更新 struct file 中的读写位置。这就是为什么两个进程打开同一个文件,各自的读写位置互不干扰——它们有各自独立的 struct file,各自独立的 f_pos。 2.2 vfs_read:最终调用到驱动 定义在 fs/read_write.c: ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos){ /* ... 权限和安全检查省略 ... */ if (file->f_op->read) // 传统路径 ret = file->f_op->read(file, buf, count, pos); else if (file->f_op->read_iter) // 迭代器路径 ret = new_sync_read(file, buf, count, pos); else ret = -EINVAL; return ret;}vfs_read的核心就是这两行: 如果驱动实现了 .read,直接调用 file->f_op->read()——这就是我们驱动中的 my_gpio_read() 被调用的地方 如果驱动实现的是 .read_iter(新式迭代器接口),则通过 new_sync_read() 包装调用 因为 file->f_op 在 open 时已经被 chrdev_open 替换为 my_gpio_fops,所以这里 file->f_op->read 调用的就是我们驱动中实现的 .read 方法。 read 流程比 open 简单得多,因为 open 已经把所有的"路由"工作都做完了。read 只需要沿着一条直线走:fd → file → f_op → driver.read。 下图展示了 read 的调用链 —— 相比 open 的复杂路径解析,read 只需沿直线走到驱动: 2.3 read 流程中四大对象的参与 file:核心角色,提供 f_op(路由到驱动)和 f_pos(读写位置) inode:驱动可以通过 file->f_inode 访问文件元数据(如设备号),但大多数驱动在 read 中不需要 dentry / super_block:read 流程中不直接参与,它们的任务在 open 阶段就完成了 这也反映了 VFS 的设计哲学:open 做一次昂贵的路径解析和绑定,后续的 read/write 只需要走一条轻量级的直线路径。 复杂度被前置到 open 中一次性消化了。 3. 从全局视角看四大对象的分工 走完了 open 和 read 的全流程,我们可以从全局视角总结每个对象在整个文件访问生命周期中的角色: 对象 | 在 open 中的角色 | 在 read/write 中的角色 | 生命周期 | | super_block | 路径跨越挂载点时提供文件系统上下文 | 不直接参与 | mount → umount | | dentry | 缓存路径解析结果,加速"路径名 → inode"查找 | 不直接参与 | 内存缓存,LRU 回收 | | inode | 提供文件类型、设备号、默认 f_op;字符设备通过 i_rdev 查找 cdev | 驱动可选访问 | 首次访问 → 最后引用释放 | | file | open 的产物:持有 f_op、f_pos、private_data | 核心角色:f_op 路由到驱动,f_pos 维护读写状态 | open → close |
简单来说:**super_block 和 dentry 负责"找到文件",inode 负责"连接驱动",file 负责"使用驱动"**。 前两者完成使命后退居幕后,inode 作为持久的元数据存在,而 file 作为每次打开的会话贯穿整个使用过程。 4. 总结 这一节我们以 open("/dev/gpio") + read(fd, buf, 10) 为例,跟着真实的内核源码(Linux 6.18),把 VFS 四大核心对象的协作关系完整地串联了一遍: open 是一次绑定过程,它把路径字符串翻译成了一个可以直接操作的 struct file;read 则是直线调用,沿着 open 建立好的路由调用到驱动的功能实现。 到这里,VFS 这一章的核心内容就讲完了。我们从 5.1 的"一切皆文件"哲学出发,经过 5.2/5.3 的四大数据结构拆解。 再到这一节的我们具备了完整的知识储备后对全流程串联,应该对 VFS 如何实现统一文件接口、如何连接驱动有了比较完整的理解。 回头看,VFS 的设计是一个经典的分层抽象:用户态只看到 fd,VFS 把 fd 翻译成 file,file 通过 dentry 和 inode 找到底层实现,底层实现通过 operations 表提供多态能力。 这种"接口不变、实现可替换"的设计思想,不仅体现在 VFS 中,也贯穿了 Linux 内核的方方面面。 - END - 如果有什么问题,欢迎添加我的微信讨论。我建了一个小群,后面会陆续加一些我身边认识的行业大佬,都是来自一线大厂P7以上的工程师,任何有关行业、工作、跳槽的问题都可以在群里讨论~~ 自我介绍: 曾就职于AMD,现就职于某大厂自研芯片部门任 BSP 研发专家。 崇尚实用主义,主张从用中学。 系列介绍: 带大家从零进行 Bringup 工作,并深入讲解这个过程中涉及到的内容以及原理实现。 往期推荐: 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |