1. 边界、分层与端到端架构
| [中文] | English | 教程目录 |
本文目标
本文先回答三个问题:
- FUSE 把哪些工作留在内核,哪些工作交给用户态?
- 一次系统调用为什么可能对应零个、一个或多个 FUSE 请求?
/dev/fuse、FUSE over io_uring 和 virtio-fs 在整个架构中分别处于哪一层?
1. FUSE 不是“用户态 VFS”
应用通过标准系统调用访问 FUSE 文件系统。进入内核后,VFS 仍然负责通用文件系统语义:
- 路径遍历以及 dentry/inode/file 对象的生命周期;
- fd table、mount namespace 和 superblock;
- 通用权限入口、LSM hook、文件锁和 mmap 生命周期;
- 页缓存、readahead、writeback 和通用 I/O helper;
- 对各文件系统 operation table 的调用。
FUSE daemon 实现的是具体文件系统策略和后端访问:
- 名称到对象的解析;
- 对象元数据和目录内容;
- 文件数据存取;
- 后端特有的权限、配额、快照、网络或对象存储逻辑;
- 缓存有效期和主动失效策略。
因此,FUSE 的准确定位是:Linux 内核中的 FUSE client 把 VFS 对具体文件系统提出的问题编码成协议请求,用户态 daemon 返回答案;内核再用答案维护 VFS 对象。
2. 分层架构
Application
open / stat / read / write / mmap / rename
|
v
Linux VFS
namei, dcache, inode cache, page cache, file_operations
|
v
FUSE kernel client
inode/dir/file operations, cache policy, request construction
|
v
Transport
/dev/fuse read/write | FUSE over io_uring | virtio-fs virtqueue
|
v
Userspace or host daemon
libfuse/virtiofsd/custom server
|
v
Backend
local files, network service, object store, generated namespace
层次之间有两个方向的数据流:
- 请求/应答: 内核发起
LOOKUP、GETATTR、READ、WRITE等操作,daemon 以相同 unique 返回结果。 - 主动通知: daemon 通过 notification 要求内核失效 dentry、inode 或数据,或者唤醒 poll。
3. 系统调用与协议请求不是一一对应
以 open("/mnt/a/b", O_RDONLY) 为例:
VFS 查找 /mnt/a
cache miss -> FUSE_LOOKUP("a")
VFS 查找 /mnt/a/b
cache miss -> FUSE_LOOKUP("b")
VFS 打开 inode
-> FUSE_OPEN
第二次打开时,若两个 dentry 和 inode 属性仍有效,路径查找可以完全命中内核缓存,只剩 FUSE_OPEN;若 daemon 声明 FUSE_NO_OPEN_SUPPORT,连 OPEN 也可能由内核省略。
相反,一次 4 MiB read(2) 可能因为 max_read、max_pages、页缓存缺页边界或 direct-I/O 拆分而生成多个 FUSE_READ。所以分析性能时应分别观察:
- syscall 数;
- VFS cache miss 数;
- FUSE opcode 数;
- daemon 后端操作数。
这四个数字通常并不相同。
4. 元数据路径和数据路径
4.1 元数据路径
元数据操作通常以小请求为主,但会放大延迟:
path walk -> LOOKUP -> ENTRY reply -> dentry/inode cache
stat -> GETATTR -> ATTR reply -> attribute cache
create -> CREATE -> ENTRY + OPEN reply
unlink -> UNLINK -> dentry/inode invalidation
readdir -> READDIR or READDIRPLUS
READDIRPLUS 能在返回目录项时附带属性和 nodeid,减少后续 LOOKUP,但也增加单次应答大小和引用记账。
4.2 数据路径
FUSE 提供多种数据路径:
- cached I/O: 使用 guest/kernel 页缓存,缺页时发送 READ,脏页由 writeback 发送 WRITE;
- direct I/O: 绕过普通页缓存,把用户
iov_iter拆成 FUSE READ/WRITE; - passthrough: 协商后让 I/O 进入 backing file;
- virtio-fs DAX: guest 页表映射 DAX window,数据访问不经普通 FUSE READ/WRITE。
这些路径共享 inode 和文件生命周期,却有不同的一致性与锁要求。它们不是可以在任意时刻无条件互换的性能开关。
5. 传输层是可替换的
5.1 /dev/fuse
传统 daemon 打开 /dev/fuse:
- daemon 的
read()从内核 pending queue 取得请求; - daemon 的
write()提交应答; - poll/fasync 用于通知请求可读;
- 请求数据通过 copy helpers 在地址空间之间移动。
5.2 FUSE over io_uring
当前内核可以协商 FUSE_OVER_IO_URING,用 io_uring command 提供请求槽和完成路径。它改变传输机制和调度成本,但不改变 LOOKUP、READ 等 FUSE opcode 的文件系统语义。
5.3 virtio-fs
virtio-fs 在 guest 中复用 FUSE client 和协议对象,以 virtqueue 把请求送到 host 上的 virtiofsd。DAX 又增加共享内存 window 和 SETUPMAPPING/REMOVEMAPPING。因此分析 virtio-fs 时应分开:
- FUSE 层:对象、opcode、缓存和 inode/file 生命周期;
- virtio 层:virtqueue、通知和共享内存;
- vhost-user/virtiofsd 层:host 进程和后端文件。
6. 性能成本模型
一次未缓存请求的延迟大致由以下部分组成:
VFS/FUSE client CPU
+ 请求排队
+ 唤醒与调度 daemon
+ 参数/数据复制或映射
+ daemon 调度与协议解析
+ 后端访问
+ 应答排队与复制
+ 唤醒原线程
优化不能只盯住 daemon 的后端函数。常见手段对应不同成本:
| 手段 | 主要减少什么 | 代价 |
|---|---|---|
| 增大 entry/attr timeout | 元数据往返 | 后端外部变化更晚可见 |
| readahead | READ 次数和等待 | 可能读入无用数据 |
| writeback cache | 同步 WRITE 等待 | 一致性和错误延迟更复杂 |
| 增大请求 | 每字节协议开销 | 内存占用和尾延迟可能上升 |
| 增加并发 | daemon/后端利用率 | 排队、锁竞争和内存压力 |
| passthrough/DAX | 数据面往返和拷贝 | 模式切换及一致性更严格 |
7. 设计边界检查表
设计一个 FUSE 文件系统前,应明确:
- 后端是否可能被 daemon 之外的进程修改?
- 名称、属性和数据分别允许多长的陈旧窗口?
- 哪些操作必须有严格顺序,哪些可以并行?
- daemon 卡死或退出时,应用应等待、超时还是快速失败?
- 使用 cached、direct、passthrough 或 DAX 中的哪些路径?
- user namespace、
allow_other和后端凭据如何映射? - 如何观测 unique、opcode、队列深度和后端延迟?
小结
FUSE 的核心不是字符设备,而是一条横跨 VFS、内核 client、可替换传输和 daemon 的对象与请求协议。下一篇将建立这些对象之间的精确映射。