4. 文件对象、文件系统类型注册与挂载描述符
好,咱们接着往下聊。上一节我们把 dentry 和 inode 掰扯清楚了,这节我重点讲剩下的三个结构体:file、file_system_type 和 mount。这三个家伙,说白了就是「打开的文件长什么样」「文件系统怎么注册进来」「挂载点怎么管理」。
我个人习惯把这三个结构体放在一起讲,因为它们在实际开发中经常要联动。你写一个文件系统驱动,注册、挂载、打开文件,这三步几乎是必走的流程。
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 vfsmount 和 struct 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_parent 和 mnt_child 形成树形结构。遍历挂载树时,千万别搞成死循环——我见过有人递归遍历时没判断 parent 等于自身的情况。
4.4 三者关系与流程图
这三个结构体怎么配合?我画个图你就明白了。
从上图你能看到:file_system_type 是「工厂」,负责生产挂载实例;mount 是「挂载上下文」,记录挂载点和超级块;file 是「打开的文件」,通过路径找到对应的挂载和 inode。
4.5 实际开发中的注意事项
写文件系统驱动时,这三个结构体你都得打交道。我总结几个实战经验:
- 注册时机要早:
register_filesystem()最好在模块 init 函数的最前面调用。我见过有人把它放在中间,结果前面代码出错返回了,文件系统没注册上,后面 mount 直接失败。 - mount 回调要小心:你的 mount 函数要返回根 dentry。如果返回 NULL,内核会报错。我曾经返回了一个没设置 d_op 的 dentry,结果所有文件操作都走默认路径,查了半天。
- file 的 private_data 别乱用:如果你用它存指针,记得在 release 时清理。多进程 fork 后 file 结构体会被共享,private_data 的释放要处理好引用计数。
- 挂载树遍历别递归太深:内核里有
namespace_sem保护挂载树,但你自己的遍历代码要注意锁的粒度。我建议用list_for_each_entry配合mnt_child链表,别自己写递归。
调试技巧:想看当前系统有哪些挂载点?cat /proc/mounts 或者 findmnt。内核里可以用 iterate_mounts() 遍历所有挂载。我调试时经常在 mount 回调里加 printk,打印出挂载路径和超级块信息。
好了,这一节的内容就到这。文件对象、文件系统类型、挂载描述符,这三个结构体是 VFS 层的骨架。你写驱动时,注册、挂载、打开文件,每一步都离不开它们。下一节我们讲超级块和 dentry 的缓存管理,那个更贴近性能优化。
公众号:蓝海资料掘金营,微信deep3321