『7x24小时有问必答』
坚持高质量原创,拒绝内容堆砌,喜欢的话点击上方星标,更新第一时间收到提醒,谢谢关注!

前两节我们已经把 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_opf_inodef_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",计算哈希值
name  指向字符串末尾  \0,走  OK  标签
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_opf_posprivate_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 研发专家。
崇尚实用主义,主张从用中学。

系列介绍:
从零开始的BSP之路,BSP系列的开章,站在芯片原厂工程师视角,
带大家从零进行 Bringup 工作,并深入讲解这个过程中涉及到的内容以及原理实现。

BSP工程师的内核基本功,旨在为驱动及BSP工程师讲解
工作中会用到的内核知识及其底层原理。

往期推荐:

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码