FUSE —— 用户态文件系统
前置阅读 vfs-overview(四大对象模型)、vfs-file-operations(
file_operations多态分发 +private_data设计模式)。本文聚焦 FUSE 如何利用 VFS 接口把文件系统实现搬到用户态,以及多挂载点的隔离机制。
零、一句话认知
FUSE(Filesystem in Userspace)不是一种文件系统格式——它是一种"代理架构"。内核中的 fuse.ko 模块不存储任何数据,它只是一个转发器:把 VFS 下发的 read/write/open/mkdir 等操作封装成 FUSE 协议消息,通过 /dev/fuse 发给用户态守护进程,由守护进程处理后再返回结果。用户态程序可以实现任意后端(S3、HTTP、加密、压缩),内核只负责把 VFS 接口和 /dev/fuse 字符设备对接起来。

一、FUSE 的关键内核数据结构
1.1 struct fuse_conn —— 挂载会话的"连接上下文"
每个 FUSE 挂载点在内核中对应一个独立的 fuse_conn,它是整个挂载会话的核心——保存请求队列、inode 缓存、配置参数、通信状态等。访问路径:
用户访问 /mnt/fuse1/some/file
→ VFS 找到该路径对应的 super_block
→ super_block->s_fs_info = fuse_conn ← 这就是本挂载点的会话上下文// fs/fuse/fuse_i.h (简化)
struct fuse_conn {
struct list_head devices; // 连接到本 conn 的 /dev/fuse fd 列表
struct list_head queue; // 待处理的请求队列(pending)
wait_queue_head_t waitq; // 等待队列——用户态守护进程阻塞在这里
struct list_head bg_queue; // 后台请求队列
u64 reqctr; // 请求计数器
struct inode *ctl_inode; // 控制 inode
unsigned int max_pages; // 单次请求最大页面数
// ... 挂载选项、配置、统计 ...
};关键:
fuse_conn不是通过file->private_data访问的,而是通过super_block->s_fs_info。这是区分"这个请求属于哪个挂载点"的核心机制。
1.2 struct fuse_file_info —— 单次打开文件的会话状态
每次 open() 一个 FUSE 文件,内核分配一个 fuse_file_info,挂到 file->private_data:
struct fuse_file_info {
u64 fh; // FUSE 文件句柄(由用户态守护进程分配,替代内核的 fd)
u32 open_flags; // 打开标志(O_RDONLY / O_RDWR 等)
u64 nodeid; // 该文件在 FUSE 守护进程中的 inode 号
// ...
};这里体现了 VFS 的 private_data 模式:同一套 fuse_file_operations(全局唯一),通过 file->private_data 区分不同的打开会话。详见 vfs-file-operations §四。
二、多挂载点隔离:super_block->s_fs_info 是关键
2.1 隔离机制
┌─────────────────────────────────────────────────────┐
│ 挂载点 A: /mnt/fuse1 │
│ super_block_A │
│ → s_fs_info = fuse_conn_A │
│ → 独立的请求队列 │
│ → 独立的 inode 缓存 │
│ → 绑定 /dev/fuse fd_A(用户态守护进程 A 持有) │
├─────────────────────────────────────────────────────┤
│ 挂载点 B: /mnt/fuse2 │
│ super_block_B │
│ → s_fs_info = fuse_conn_B │
│ → 独立的请求队列 │
│ → 独立的 inode 缓存 │
│ → 绑定 /dev/fuse fd_B(用户态守护进程 B 持有) │
└─────────────────────────────────────────────────────┘
所有 FUSE 文件共用一套 fuse_file_operations(f_op 全局唯一)
挂载点之间的隔离全靠 super_block → fuse_conn 的不同实例
2.2 /dev/fuse 绑定规则
一个 /dev/fuse fd 只能绑定一个挂载会话。每新增一个挂载点,必须重新 open("/dev/fuse") 获取新的 fd:
// 挂载点 A
fd_A = open("/dev/fuse", O_RDWR);
mount("/dev/fuse", "/mnt/fuse1", "fuse", flags, fd_A);
// 内核: fd_A → 创建 fuse_conn_A → sb_A->s_fs_info = fuse_conn_A
// 挂载点 B(必须用新 fd!)
fd_B = open("/dev/fuse", O_RDWR);
mount("/dev/fuse", "/mnt/fuse2", "fuse", flags, fd_B);
// 内核: fd_B → 创建 fuse_conn_B → sb_B->s_fs_info = fuse_conn_B如果用同一个 fd 执行两次 mount,内核会报错——因为 fd 已经绑定到第一个
fuse_conn,不能复用给第二个挂载点。
三、VFS 请求在 FUSE 中的完整流转
以 read("/mnt/fuse/file") 为例:
1. 用户态: read(fd, buf, 4096)
│
2. VFS: ksys_read(fd)
│ file = fdget(fd)
│ file->f_op->read_iter(file, ...) ← fuse_file_operations 中的函数
▼
3. FUSE 内核模块: fuse_read_iter()
│ 从 file->private_data 取出 fuse_file_info
│ 从 file->f_inode->i_sb->s_fs_info 取出 fuse_conn
│ 构造 FUSE 请求(FUSE_READ, 带上 nodeid + fh + offset + size)
│ 请求入队 → fuse_conn.queue
│ 唤醒等待在 /dev/fuse 上的守护进程
│ 当前进程阻塞(TASK_INTERRUPTIBLE)
▼
4. 用户态守护进程:
│ read(dev_fuse_fd, buf, sizeof(fuse_in_header) + payload)
│ 解析请求 → 调用对应后端(读 S3 / HTTP / 本地磁盘)
│ 拿到数据后 → 构造 FUSE 响应
▼
5. 守护进程写回:
│ write(dev_fuse_fd, response, sizeof(fuse_out_header) + data)
▼
6. FUSE 内核模块:
│ 响应出队 → 匹配原始请求
│ 拷贝数据到用户态 buf
│ 唤醒阻塞的 VFS read 调用
▼
7. 返回用户态: read() 返回读取的字节数3.1 调度分析:进内核态必然触发调度吗?
进内核态本身不必然触发调度。普通 syscall(如 getpid())执行完直接 iretq 返回,全程不碰调度器。
但 FUSE 场景下必定触发调度——因为内核自己完成不了请求(数据在用户态守护进程手里),内核必须把调用者挂起睡眠、把 CPU 让给守护进程。具体涉及的调度点:
时间线
═══════
syscall 入口
│
调用者进程 ▼
(cat /mnt/fuse/f) VFS → fuse_read_iter()
│
│ ① 请求入队后,调用者主动睡眠
▼ set_current_state(TASK_INTERRUPTIBLE)
调度点 1: schedule() ← 让出 CPU,切到其他进程
║
║ 上下文切换(调用者 → 守护进程)
║
守护进程 ║
(之前阻塞在 ▼
read(/dev/fuse)) 唤醒:wake_up(&fud->waitq)
│
│ 守护进程从 read(/dev/fuse) 返回
│ 拿到 FUSE 请求 → 访问后端存储
│
│ 后端返回数据 → 构造响应
│ write(dev_fuse_fd, response)
│ ↓
│ ② 响应入队,唤醒调用者
│ wake_up(fuse_conn->waitq)
│
│ ③ 守护进程继续循环:
│ read(dev_fuse_fd) ← 无新请求,阻塞
▼
调度点 2: schedule() ← 守护进程睡眠,让出 CPU
║
║ 上下文切换(守护进程 → 调用者 或 其他进程)
║
调用者进程 ║
▼
被唤醒 → 从 schedule() 返回
│ fuse_read_iter() 拷贝数据 → 返回到 VFS
│ ksys_read() → iretq → 回到用户态最少触发 2 次 schedule():
| 调度点 | 谁触发 | 原因 |
|---|---|---|
① schedule() | 调用者(cat 进程) | TASK_INTERRUPTIBLE 睡眠,等待守护进程响应 |
② schedule() | FUSE 守护进程 | read(/dev/fuse) 无新请求,阻塞等待 |
最少 2 次 context switch:
- 调用者 → 守护进程(调度点 ① 之后)
- 守护进程 → 调用者(调度点 ② 之后,或中间经过其他进程)
对比:普通磁盘 I/O 也阻塞也调度,但区别在于 FUSE 的等待链多了一个用户态环节——普通 read(ext4) 是:调用者睡眠 → 磁盘中断唤醒 → 返回。FUSE read 是:调用者睡眠 → 守护进程被唤醒(调度)→ 守护进程访问后端(可能再次睡眠/调度)→ 守护进程 write /dev/fuse → 调用者被唤醒(再次调度)。每个简单的
read()在 FUSE 上至少产生 2 次调度 + 2 次上下文切换,这是 FUSE 性能开销的重要来源之一。
3.2 守护进程侧:事件循环也依赖调度
// libfuse 事件循环(简化)
while (1) {
struct fuse_buf buf;
ssize_t res = read(dev_fuse_fd, buf, bufsize);
// ↑ 无请求时,守护进程在这里 TASK_INTERRUPTIBLE 睡眠
// 内核调度器会把它切走,CPU 给别人用
// 有新请求到来 → wake_up → 守护进程变为 TASK_RUNNING
// → 调度器下次选中时 → read() 返回 → 开始处理
fuse_session_process_buf(se, &buf); // 解析请求、调 handler
// ... handler 完成后 write(dev_fuse_fd, response)
}守护进程大部分时间都在 read(/dev/fuse) 上阻塞睡眠——这是设计意图:没有请求时不消耗 CPU。有请求时才被内核唤醒。
一句话总结:FUSE 的每一次 I/O 都伴随 2+ 次调度——这不是 bug,是 FUSE 把文件系统搬到用户态的必然代价。普通文件系统在内核态一条龙完成,FUSE 必须在用户态和内核态之间来回踢皮球,每次踢球都意味着一次 context switch。
四、两种实现方案
方案 1:多进程多挂载点(标准方案,libfuse 默认)
每个挂载点一个独立进程 + 独立 /dev/fuse fd:
进程A 进程B
│ │
├─ open("/dev/fuse") → fd_A ├─ open("/dev/fuse") → fd_B
├─ mount → /mnt/fuse1 ├─ mount → /mnt/fuse2
├─ fuse_loop(fd_A) ├─ fuse_loop(fd_B)
│ while(1) { │ while(1) {
│ read(fd_A, req); │ read(fd_B, req);
│ process(req); │ process(req);
│ write(fd_A, resp); │ write(fd_B, resp);
│ } │ }- 优点:简单、天然隔离、崩溃互不影响、所有开源 FUSE 工具默认方式(s3fs、mergerfs、zipfs、sshfs)
- 缺点:多进程,资源开销略高
方案 2:单进程多会话(高级用法,需 libfuse3)
单个进程管理多个挂载点,内部维护多个 fuse_session:
// libfuse3 单进程多挂载点简化示例
struct fuse_session *se1 = fuse_session_new(&ops1, &se1_data);
struct fuse_session *se2 = fuse_session_new(&ops2, &se2_data);
fuse_session_mount(se1, "/mnt/fuse1");
fuse_session_mount(se2, "/mnt/fuse2");
// 同一个事件循环处理多个 session
// 每个 session 对应内核中独立的 fuse_conn
fuse_loop_mt(se1); // 或使用 fuse_session_loop_mt 同时驱动多个- 适用场景:需要统一管理多个挂载目录、不想启动大量进程的服务
- 限制:libfuse3 才完善支持;多个 session 共用进程内存空间,需注意全局变量隔离(每个 session 绑定自己的配置数据,不要用全局变量保存挂载点相关配置)
五、FUSE 中的三层隔离设计
FUSE 完美体现了 VFS 的分层设计思想:
| 层级 | 存储位置 | 粒度 | 作用 |
|---|---|---|---|
① file_operations | 全局变量 | 类型级别 | 所有 FUSE 文件共用同一套函数代码 |
② file->private_data | struct file | 单次打开 | 区分同一挂载点内不同 open() 的会话(偏移量、打开标志) |
③ super_block->s_fs_info | struct super_block | 每个挂载点 | 区分不同挂载点本身(inode 编号空间、缓存、请求队列全部隔离) |

六、容易踩坑的关键点
- 一个
/dev/fusefd 只能绑定一个挂载点 —— 复用 fd 执行第二次 mount 会失败 - 单进程多挂载点时,避免全局变量 —— 所有挂载点的回调共用同一份代码,全局变量会导致会话混淆;每个挂载点的配置必须绑定到
fuse_session的私有数据上 - 挂载命名空间 —— 如果需要部分进程可见,可结合 Linux mount namespace 实现容器级别的挂载点隔离
- libfuse2 的多会话局限 —— 老版本不支持多 session,容易出现信号、全局变量冲突
七、延伸阅读
- vfs-overview — VFS 四大对象模型、多态分发机制
- vfs-file-operations —
file_operations逐字段拆解 +f_opvsprivate_data设计模式 - vfs-and-socket — socket 如何利用 VFS 的
private_data桥接网络协议栈 - files-struct — 进程 fd 表 →
struct file的完整链路 - read-write-process —
read()从 syscall 到 DMA 的 10 层调用链
总结
FUSE 是 VFS 多态分发的典范应用——内核通过 super_block->s_fs_info 隔离挂载点、通过 file->private_data 隔离打开会话、通过全局 fuse_file_operations 复用函数代码;用户态守护进程只需 read/write 一个 /dev/fuse fd 就能实现完整文件系统的语义。