Linux-Kernel-Notes

Thinking in linux kernel

View on GitHub

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 是协议另一端,内核不能直接相信其结果。关键检查包括:

校验失败属于协议错误,不能当作普通 ENOENT 缓存。

4. 正查找、负查找和缓存时间

成功 lookup 会实例化或复用 inode,并增加 daemon 侧需要记录的 lookup 引用。ENOENT 可以形成负 dentry;负结果缓存多久由负 entry timeout 决定。

两个超时不要混淆:

entry 有效而 attr 过期时,不必重新 LOOKUP 名称,只需刷新属性。反之,entry 过期可能需要确认名称是否仍指向原对象。

5. inode 实例化与属性更新

FUSE 根据 node ID 和 generation 找到或创建 VFS inode,并把 fuse_attr 转换为 VFS 字段。更新 size 时尤其敏感:

因此“解析响应然后赋值 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

chmodchownutimensattruncate 最终可能进入 fuse_setattr()。代码先把 VFS iattr 翻译为 fuse_setattr_in 的 valid 位与字段,再发送 FUSE_SETATTR

truncate 的排序必须处理:

  1. 权限和写入状态检查;
  2. 与本地页缓存、writeback 和 DIO 同步;
  3. daemon 修改后端大小;
  4. 内核采用响应属性并截断缓存;
  5. 错误时保留可解释的一致状态。

如果 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 等语义。

fuse_unlink() 向 daemon 发送父 node ID 与名称。成功后,VFS dentry 从命名空间消失,目录和目标 inode 的缓存状态被更新。

但 unlink 不等于对象立即销毁:

10. rename 的原子性与失效

fuse_rename2() 根据 flags 使用 FUSE_RENAME2,必要时兼容旧 FUSE_RENAME。请求同时涉及旧父目录、旧名称、新父目录和新名称。

成功后需要失效或更新:

daemon 必须把 rename 当成后端原子操作实现;仅靠内核 dentry 锁无法使远端的两步操作变原子。

11. FORGET 在哪里接上

当 VFS 回收 dentry/inode 引用时,FUSE 累积并发送 FORGETBATCH_FORGET。该消息通常无需响应,因此走专门队列。daemon 收到后只减少 lookup count,不能关闭仍活跃的 fh

定位 inode 泄漏时,应同时统计:

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