2.1 源码地图、挂载与初始化
| 中文 | English | 目录 |
第二章:FUSE 代码实现分析 · 第 1/6 篇
本文以当前内核树中的 fs/fuse/ 和 include/uapi/linux/fuse.h 为基线,从模块注册一路跟到 FUSE_INIT 完成。读完后,应能找到任意 FUSE 功能的源码入口,并理解“挂载完成”和“连接完成协商”为什么是两个阶段。
1. 源码目录地图
| 文件 | 核心职责 |
|---|---|
fs/fuse/inode.c |
文件系统注册、挂载、连接初始化、inode 创建与属性更新 |
fs/fuse/dir.c |
lookup、目录项、创建删除、rename、属性和权限操作 |
fs/fuse/file.c |
open/release、缓存 I/O、direct I/O、writeback、锁和 fsync |
fs/fuse/dev.c |
/dev/fuse 请求队列、daemon read/write、完成与中断 |
fs/fuse/req.c |
与具体 channel 解耦的同步、后台和 notify-reply 发送接口 |
fs/fuse/dax.c |
virtio-fs DAX window、iomap、fault、映射回收 |
fs/fuse/control.c |
fusectl 中的 waiting、abort 和后台队列参数 |
fs/fuse/ioctl.c |
ioctl 转发与重试协议 |
fs/fuse/xattr.c |
扩展属性操作 |
fs/fuse/readdir.c |
readdir 和 readdirplus |
fs/fuse/passthrough.c |
backing file passthrough |
fs/fuse/virtio_fs.c |
virtio-fs transport 与 DAX 设备连接 |
fs/fuse/fuse_i.h |
子系统内部核心结构和跨文件接口 |
fs/fuse/fuse_dev_i.h |
channel、request、输入队列和处理队列 |
include/uapi/linux/fuse.h |
用户态可见协议、opcode、flags 和消息结构 |
阅读时先从 VFS 操作表找到入口,再沿 fuse_args 追到 req.c 和 channel;不要从 dev.c 的复制循环反向猜测上层语义。
2. 文件系统类型注册
inode.c 中的模块初始化注册 FUSE 文件系统类型。普通 FUSE 的 file_system_type 提供 .init_fs_context = fuse_init_fs_context;不同配置下还可能注册 fuseblk 等类型。
模块初始化
-> register_filesystem(&fuse_fs_type)
-> 用户执行 mount
-> fuse_init_fs_context()
-> 参数解析
-> fuse_get_tree()
-> get_tree_nodev()/get_tree_bdev()
-> fuse_fill_super()
-> fuse_fill_super_common()
-> fuse_send_init()
新的 mount API 使用 fs_context:解析结果先存入 FUSE 私有上下文,真正构造 super_block 时再消费。这样参数解析、树创建和 superblock 初始化有清楚的生命周期边界。
3. 挂载参数中的关键对象
经典 /dev/fuse 挂载通常需要一个已经打开的 FUSE 设备文件描述符。挂载逻辑会把这个端点与新连接关联,同时处理 root mode、user/group、默认权限、最大读写尺寸和安全选项。
需要区分:
struct fuse_conn:协议连接级状态和协商能力,可被挂载对象引用;struct fuse_mount:一次 FUSE 挂载与连接的结合点;struct super_block:VFS 挂载实例;struct fuse_fs_context:挂载构造期间的临时参数;- channel/transport:请求实际流向 daemon 的端点。
virtio-fs 会复用大量公共初始化,但 transport 和连接建立方式不同,因此不要把 /dev/fuse 文件描述符当成 FUSE 架构的必需概念。
4. fuse_fill_super_common() 做什么
公共 superblock 初始化大致完成以下工作:
- 填写
super_block的操作表、时间粒度和最大文件大小等字段; - 建立或连接
fuse_conn、fuse_mount; - 初始化 root inode 和 root dentry;
- 设置 BDI、缓存和回写相关能力;
- 根据挂载模式设置权限和导出等策略;
- 安排连接销毁时需要的引用关系。
此时 VFS 对象框架已经存在,但 daemon 支持哪些可选协议能力仍未确定。
5. FUSE_INIT 是连接能力协商
fuse_send_init() 构造 FUSE_INIT 请求。请求与响应由专门的初始化参数对象承载,响应完成函数验证版本和限制,然后把协商结果写入 fuse_conn。
协商内容包括但不限于:
- 协议 major/minor;
max_write、max_readahead、max_pages和对齐;max_background与拥塞阈值;- writeback cache、异步读、并行目录操作;
- POSIX locks、flock、ACL、安全上下文;
- DAX、passthrough、io_uring transport 等版本相关能力;
- 时间戳粒度和扩展初始化格式。
初始化不是“把所有本端支持位取并集”,而是取双方可用能力并验证相关字段。daemon 返回的长度还必须与其宣称的协议版本相容。
6. 为什么初始化请求是特殊的
普通请求依赖已经协商好的限制,而 FUSE_INIT 本身发生在限制生效之前。代码因此需要兼容旧版响应、默认值和降级路径。常见错误是:
- daemon 宣称较新 minor,却返回较短结构;
max_write与 transport 能承载的页数不一致;- 宣告某 feature flag,却没有实现相应 opcode 或错误路径;
- 把 daemon 不支持的位继续保留在
fuse_conn中; - 初始化失败后仍允许普通请求进入队列。
7. 初始化完成前后的状态
可以把连接生命周期简化为:
分配连接
-> 建立 transport
-> 创建 VFS 根对象
-> 发送 FUSE_INIT
-> 协商成功:connection initialized
-> 正常请求
-> disconnect/abort/unmount
-> 引用清零后释放
等待初始化的路径必须在成功、失败和 daemon 消失时都能被唤醒。后续请求读取的 feature 字段,应在初始化发布之后保持稳定,避免操作期间改变协议契约。
8. Root inode 的特殊性
FUSE 根节点使用协议规定的 root node ID。它不是通过普通 pathname LOOKUP 首次发现的,但仍要作为 VFS inode/dentry 存在。之后对根目录下名称的查找,才以 root node ID 作为 parent 发送 LOOKUP。
root 的属性和权限策略需要有合理初值,否则在 INIT 和首次 GETATTR 之间就可能产生异常访问结果。
9. 从挂载问题定位源码
建议按症状选择入口:
- 参数拒绝:
fuse_init_fs_context()及参数表; - superblock/root 创建失败:
fuse_get_tree()、fuse_fill_super*(); - daemon 能读到 INIT 但 mount 卡住:
fuse_send_init()、请求完成路径; - feature 未生效:INIT 输入 flags、响应解析、
fuse_conn字段; - 最大 I/O 异常:
max_write、max_pages、transport buffer 限制; - virtio-fs DAX 未启用:virtio feature、DAX 设备发现和 INIT DAX flag 三者一起检查。
10. 本文小结
FUSE 挂载先通过 VFS mount API 构造 superblock、root 和连接,再用 FUSE_INIT 确立协议版本与能力。fuse_conn、fuse_mount、super_block 和 transport 各自拥有不同生命周期。后续所有代码分析都应先确认所讨论的能力是否已经在 INIT 中协商。