4. 文件对象、文件系统类型注册与挂载描述符

好,咱们接着往下聊。上一节我们把 dentry 和 inode 掰扯清楚了,这节我重点讲剩下的三个结构体:filefile_system_typemount。这三个家伙,说白了就是「打开的文件长什么样」「文件系统怎么注册进来」「挂载点怎么管理」。

我个人习惯把这三个结构体放在一起讲,因为它们在实际开发中经常要联动。你写一个文件系统驱动,注册、挂载、打开文件,这三步几乎是必走的流程。

4.1 文件对象(struct file)

先看 file 结构体。它代表一个「打开的文件实例」。注意,不是磁盘上的文件,而是进程打开后的那个句柄。

struct file {
    struct path                 f_path;
    struct inode                *f_inode;
    const struct file_operations *f_op;
    atomic_long_t               f_count;
    unsigned int                f_flags;
    loff_t                      f_pos;
    void                        *private_data;
    // ... 省略部分字段
};

这里我重点说几个字段:

  • f_path:包含 dentry 和 vfsmount,用来定位文件在哪个挂载点上。
  • f_inode:指向 inode。你想想看,每次读写文件都要查 inode,所以这里直接缓存一份。
  • f_op:文件操作函数表。read、write、mmap 都在这里。
  • f_pos:当前读写位置。多线程共享同一个 file 结构体时,这里会出问题——我踩过这个坑。
  • private_data:私有数据指针。很多驱动用它来挂自己的上下文。

核心要点:每个进程的每个打开文件,在内核里都有一个独立的 struct file。两个进程打开同一个文件,会有两个 file 结构体,但共享同一个 inode。

我在项目中遇到过一个问题:某个驱动在 open 时分配了内存,存在 private_data 里,但 release 时忘了释放。结果每次打开都泄漏一点,跑几天系统就 OOM 了。嗯,这里要注意:private_data 的生命周期你得自己管好。

4.2 文件系统类型注册(struct file_system_type)

接下来是 file_system_type。这个结构体用来描述「一类文件系统」,比如 ext4、btrfs、tmpfs 都属于不同的类型。

struct file_system_type {
    const char                  *name;
    int                         fs_flags;
    struct dentry *(*mount)     (struct file_system_type *, int,
                                 const char *, void *);
    void (*kill_sb)             (struct super_block *);
    struct module               *owner;
    struct file_system_type     *next;
    // ...
};

关键点:

  • name:文件系统名字,比如 "ext4"、"tmpfs"。用户空间用 mount -t ext4 就是匹配这个。
  • mount:挂载回调。内核调用它来创建超级块和根 dentry。
  • kill_sb:卸载回调。销毁超级块。
  • owner:模块所有者。如果是模块,这里指向 THIS_MODULE。

注册一个文件系统类型,用 register_filesystem()。我建议你在模块初始化时调用它,卸载时调用 unregister_filesystem()

小技巧fs_flags 可以设置 FS_REQUIRES_DEV 表示需要块设备,或者 FS_USERNS_MOUNT 支持用户命名空间挂载。写虚拟文件系统时一般不需要设备。

我曾经写过一个简单的内存文件系统,注册时忘了设 owner,结果模块卸载后内核还在调用 mount 回调,直接 panic。从那以后我每次注册都会检查 owner 字段。

4.3 挂载描述符(struct mount)

最后是 mount 结构体。注意,内核里有两个名字相似的东西:struct vfsmountstruct mount。前者是公共接口,后者是内部实现。我们一般说挂载描述符指的是 struct mount

struct mount {
    struct hlist_node           mnt_hash;
    struct mount                *mnt_parent;
    struct dentry               *mnt_mountpoint;
    struct vfsmount             mnt;
    struct list_head            mnt_child;
    // ...
};

这里我解释几个关键字段:

  • mnt_parent:指向父挂载点。挂载是有层次的,根挂载点的 parent 指向自己。
  • mnt_mountpoint:挂载点在父文件系统中的 dentry。比如你把 U 盘挂到 /mnt/usb,这个 dentry 就是 /mnt/usb 的目录项。
  • mnt:内嵌的 vfsmount 结构体,包含挂载的根 dentry 和超级块。
  • mnt_child:子挂载链表。用来遍历某个挂载点下的所有子挂载。

注意:挂载描述符之间通过 mnt_parentmnt_child 形成树形结构。遍历挂载树时,千万别搞成死循环——我见过有人递归遍历时没判断 parent 等于自身的情况。

4.4 三者关系与流程图

这三个结构体怎么配合?我画个图你就明白了。

file_system_type name: "myfs" mount() 回调 kill_sb() 回调 struct mount mnt_parent → 父挂载 mnt_mountpoint → dentry mnt.mnt_root → 根dentry mnt.mnt_sb → super_block struct file f_path.dentry → dentry f_path.mnt → vfsmount f_inode → inode f_op → file_operations register_filesystem() open() 时关联 三者协作流程 1. 注册 file_system_type → 2. mount 回调创建 mount 和 super_block 3. open() 时创建 file,通过 dentry 找到 mount,再找到 inode

从上图你能看到:file_system_type 是「工厂」,负责生产挂载实例;mount 是「挂载上下文」,记录挂载点和超级块;file 是「打开的文件」,通过路径找到对应的挂载和 inode。

4.5 实际开发中的注意事项

写文件系统驱动时,这三个结构体你都得打交道。我总结几个实战经验:

  1. 注册时机要早register_filesystem() 最好在模块 init 函数的最前面调用。我见过有人把它放在中间,结果前面代码出错返回了,文件系统没注册上,后面 mount 直接失败。
  2. mount 回调要小心:你的 mount 函数要返回根 dentry。如果返回 NULL,内核会报错。我曾经返回了一个没设置 d_op 的 dentry,结果所有文件操作都走默认路径,查了半天。
  3. file 的 private_data 别乱用:如果你用它存指针,记得在 release 时清理。多进程 fork 后 file 结构体会被共享,private_data 的释放要处理好引用计数。
  4. 挂载树遍历别递归太深:内核里有 namespace_sem 保护挂载树,但你自己的遍历代码要注意锁的粒度。我建议用 list_for_each_entry 配合 mnt_child 链表,别自己写递归。

调试技巧:想看当前系统有哪些挂载点?cat /proc/mounts 或者 findmnt。内核里可以用 iterate_mounts() 遍历所有挂载。我调试时经常在 mount 回调里加 printk,打印出挂载路径和超级块信息。

好了,这一节的内容就到这。文件对象、文件系统类型、挂载描述符,这三个结构体是 VFS 层的骨架。你写驱动时,注册、挂载、打开文件,每一步都离不开它们。下一节我们讲超级块和 dentry 的缓存管理,那个更贴近性能优化。


公众号:蓝海资料掘金营,微信deep3321