2.2 路径名与元数据操作
| 中文 | English | 目录 |
第二章:FUSE 代码实现分析 · 第 2/6 篇
本文以 fs/fuse/dir.c 为主线,分析 pathname lookup、inode 实例化、属性刷新、create/unlink/rename,以及 VFS 状态与 daemon 状态如何保持一致。
1. 从 VFS 操作表开始
普通目录 inode 的 inode_operations 把 VFS 动作映射到 FUSE 入口,典型成员包括:
.lookup -> fuse_lookup
.create -> fuse_create
.unlink -> fuse_unlink
.rename -> fuse_rename2
.getattr -> fuse_getattr
.setattr -> fuse_setattr
不同 inode 类型会使用不同操作表,但属性相关逻辑有大量复用。阅读入口表比全文搜索某个 opcode 更容易看清 VFS 契约。
2. LOOKUP 调用链
path walk
-> fuse_lookup(dir, dentry, flags)
-> fuse_lookup_name(sb, parent_nodeid, name, ...)
-> fuse_lookup_init(args, parent_nodeid, name, out)
-> fuse_simple_request()
-> daemon: FUSE_LOOKUP
-> fuse_lookup_name() 校验响应
-> fuse_iget() / 属性更新
-> d_splice_alias() 或负 dentry
fuse_lookup_init() 把父节点 node ID 和名称组成协议参数。响应 fuse_entry_out 同时携带 node identity、entry lease 和 attribute lease。
3. 响应必须经过哪些校验
daemon 是协议另一端,内核不能直接相信其结果。关键检查包括:
nodeid不能非法,根节点等特殊 ID 要符合语义;- inode 类型与已存在对象不能冲突;
- 属性中的 inode number、mode、size 等要能安全转换;
- 同一 identity 的 generation 和类型保持稳定;
- 返回长度符合 opcode 和协商版本;
- 不允许把目录硬接到不合法的别名关系中。
校验失败属于协议错误,不能当作普通 ENOENT 缓存。
4. 正查找、负查找和缓存时间
成功 lookup 会实例化或复用 inode,并增加 daemon 侧需要记录的 lookup 引用。ENOENT 可以形成负 dentry;负结果缓存多久由负 entry timeout 决定。
两个超时不要混淆:
- entry timeout:名字到 inode 的关系还能否复用;
- attr timeout:该 inode 的属性还能否复用。
entry 有效而 attr 过期时,不必重新 LOOKUP 名称,只需刷新属性。反之,entry 过期可能需要确认名称是否仍指向原对象。
5. inode 实例化与属性更新
FUSE 根据 node ID 和 generation 找到或创建 VFS inode,并把 fuse_attr 转换为 VFS 字段。更新 size 时尤其敏感:
- 缩小文件要与页缓存截断协调;
- 本地有未完成写入时不能随意用旧的 daemon size 覆盖;
- mmap、writeback 和 direct I/O 可能并发观察大小;
- 属性版本或失效标记用于避免旧响应覆盖新状态。
因此“解析响应然后赋值 i_size”远远不够。
6. GETATTR
当 VFS 请求 stat 信息或属性 lease 过期时,fuse_getattr() 决定能否复用缓存;需要刷新时发送 FUSE_GETATTR。若文件已经打开,请求可携带 fh,让 daemon 按打开对象而非仅按 node ID 查询。
典型链路:
vfs_getattr
-> fuse_getattr
-> 缓存仍有效:generic_fillattr
-> 否则 FUSE_GETATTR
-> fuse_change_attributes()
-> generic_fillattr
7. SETATTR 与 truncate
chmod、chown、utimensat 和 truncate 最终可能进入 fuse_setattr()。代码先把 VFS iattr 翻译为 fuse_setattr_in 的 valid 位与字段,再发送 FUSE_SETATTR。
truncate 的排序必须处理:
- 权限和写入状态检查;
- 与本地页缓存、writeback 和 DIO 同步;
- daemon 修改后端大小;
- 内核采用响应属性并截断缓存;
- 错误时保留可解释的一致状态。
如果 daemon 返回的 size 与请求矛盾,内核不能默默接受并制造页缓存越界别名。
8. create/open 合并
fuse_create() 可经 fuse_create_open() 使用 FUSE_CREATE 一次获得新 entry 和 open handle,减少“创建后再 OPEN”的一轮往返。响应包含两部分:新节点的 fuse_entry_out 与打开结果 fuse_open_out。
错误恢复也因此更复杂:entry 已创建但本地 file 初始化失败时,需要正确释放 daemon handle,并避免 lookup 引用泄漏。
如果 daemon 或协议不支持原子 create-open,代码需要采用兼容路径,但不能破坏 O_EXCL 等语义。
9. unlink 的双重生命周期
fuse_unlink() 向 daemon 发送父 node ID 与名称。成功后,VFS dentry 从命名空间消失,目录和目标 inode 的缓存状态被更新。
但 unlink 不等于对象立即销毁:
- 其他硬链接可能仍存在;
- 打开的
fh仍可读写; - mmap 和脏页仍可能存在;
- lookup 引用稍后才通过
FORGET下降; - daemon 的后端对象必须等其自身引用条件满足后回收。
10. rename 的原子性与失效
fuse_rename2() 根据 flags 使用 FUSE_RENAME2,必要时兼容旧 FUSE_RENAME。请求同时涉及旧父目录、旧名称、新父目录和新名称。
成功后需要失效或更新:
- 两个父目录的属性与目录缓存;
- 源 dentry 和目标 dentry;
- 被覆盖目标的 inode 状态;
- 跨目录移动时的父关系;
RENAME_EXCHANGE、NOREPLACE、WHITEOUT等 flag 的特殊语义。
daemon 必须把 rename 当成后端原子操作实现;仅靠内核 dentry 锁无法使远端的两步操作变原子。
11. FORGET 在哪里接上
当 VFS 回收 dentry/inode 引用时,FUSE 累积并发送 FORGET 或 BATCH_FORGET。该消息通常无需响应,因此走专门队列。daemon 收到后只减少 lookup count,不能关闭仍活跃的 fh。
定位 inode 泄漏时,应同时统计:
- 成功
LOOKUP/CREATE返回的 lookup 增量; FORGET的nlookup减量;OPEN/RELEASEhandle;- 后端自身引用。
12. 调试建议
路径问题最好记录一条结构化时间线:parent node ID、name、opcode、unique、response node ID/generation、entry timeout、attr timeout、最终 dentry/inode。只打印路径字符串会丢失 rename 和 alias 后的真实身份。
13. 本文小结
dir.c 把 VFS 名字操作转换为协议消息,又把响应转成 dentry/inode 状态。LOOKUP 同时建立身份、lookup 引用和两个缓存 lease;create、unlink、rename 则必须跨内核与 daemon 维护原子性及失效顺序。
| 上一篇:源码地图、挂载与初始化 | 下一篇:open、read、write 与 writeback |