背景

simple_xattr 是内核里一套轻量的 xattr 实现,给那些用不上完整 xattr 机制的特殊文件系统兜底,tmpfs、kernfs、pidfs、socket 都靠它存放扩展属性。它存的都是零散的小属性(一个安全标签、一个用户自定义键值),每个 inode 上往往只有 1-2 个。

v7.1 合入的 b32c4a213698 把 simple_xattr 从链表改成了基于 rhashtable 的哈希表查找,本意是加快属性查找。但这次改造按 inode 粒度建表:每个 inode 各自持有一份哈希表。

┌─────────────────────────────────┐
│    每个 inode 持有一个哈希表    │
└────────────────┬────────────────┘
                 ▼
┌─────────────────────────────────┐
│ xattr 稀疏,per-inode 仅 1-2 个 │  哈希表自身有固定开销,分摊不到几个 xattr 上
└────────────────┬────────────────┘
                 ▼
┌─────────────────────────────────┐
│      单 xattr 内存占用暴涨      │  实测单 xattr 从约 80 涨到 1296 bytes/file
└─────────────────────────────────┘

哈希表自身有不可忽视的固定开销(桶表、控制结构),这些开销能否被摊薄,取决于一张表里装了多少个 xattr。而 simple_xattr 的典型用法恰恰是 per-inode 只有 1-2 个属性,固定开销分摊不到几个属性上,单个 xattr 的内存占用于是暴涨。作者用 slabinfo 做了对比:创建 10 万个空文件、每个加一个 user.test 属性后,v7.0 的链表实现每个 xattr 仅占约 80 字节,v7.1-rc2 的 per-inode 哈希表暴涨到 1296 字节。

问题

  • per-inode 各持一份哈希表,固定开销无法跨 xattr 摊薄
  • simple_xattr 典型用法是 per-inode 仅 1-2 个属性,恰好最不利
  • 单 xattr 内存占用从约 80 暴涨到 1296 bytes/file
  • 加速查找的收益被这波内存回归彻底盖过

方案

要害出在"per-inode"上。

把建表粒度从 inode 提到 superblock,让整个superblock共享一份哈希表,固定开销就被superblock内所有 xattr 一起摊薄,单 xattr 占用随之降回接近链表水平。

改动前:per-inode 各持一份哈希表
┌────────────────────────────────────┐
│     N 个 inode 各持一个哈希表      │
└─────────────────┬──────────────────┘
                  ▼
┌────────────────────────────────────┐
│ N 份固定开销,xattr 稀疏时单 xattr │
│ 暴涨                               │
└────────────────────────────────────┘

改动后:整个superblock共享一份哈希表
┌─────────────────────────────────────┐
│ superblock内所有 inode 共享一个哈希 │
│ 表                                  │
└──────────────────┬──────────────────┘
                   ▼
┌─────────────────────────────────────┐  单 xattr 降回接近链表水平;inode 另挂链
│      固定开销被全部 xattr 摊薄      │  表负责枚举与回收,cmpxchg 与 RCU 保障跨
└─────────────────────────────────────┘  inode 并发

哈希表一旦跨 inode 共享,原先赖以互斥的 inode 锁就不够用了。以前每个 inode 的表只归自己用,持 inode 锁就能放心做惰性初始化;现在多个 inode 往同一张表里插,inode 锁管不到别人,于是改用 cmpxchg 做无锁的原子插入,靠 cmpxchg_release 配合 READ_ONCE 提供必要的内存屏障。释放也要相应调整:表共享之后,一个 inode 释放属性时,另一个 inode 上正在进行的 RCU 查找可能恰好走到刚释放的元素上,因此 simple_xattrs_free 改走 RCU 宽限期,让元素熬过可能并发的查找再真正回收。

哈希表解决了 get/set 的快速定位,但 listxattr(枚举某 inode 的全部属性)和属性回收还需要一个属于单个 inode 的视角。为此每个 inode 另挂一条链表,listxattr 在 RCU 保护下遍历它。于是查找走共享哈希表、枚举与回收走 per-inode 链表,两条路径各得其所。

代价是 struct simple_xattr 多出三个指针用于链表链接,所以单 xattr 开销略高于 v7.0 的纯链表,但比起 per-inode 哈希表已不值一提。系列里的另两处改动是铺垫:

  • 简化 security.foo 这类属性名的拼装
  • 把 simple_xattr 接口改成传二级指针、让惰性分配在接口内部完成

为哈希表上移到superblock扫清调用方。

收益

作者用 slabinfo 差值计量每文件开销,场景为创建 10 万个空文件、每个加一个 user.test=foo 属性后取值:

配置 建文件开销 (bytes/file) 加 xattr 开销 (bytes/file)
v7.0(链表,无 rhashtable) 993.40 79.99
v7.1-rc2(per-inode rhashtable) 939.73 1296.08
v7.1-rc2 + 本 patch(per-sb rhashtable) 946.84 111.86

加 xattr 的单属性开销从 1296.08 降到 111.86 bytes/file,回到接近 v7.0 的 79.99,基本消除了 v7.1 引入的内存回归;struct simple_xattr 多出三个指针的代价使其略高于 v7.0,但远低于 per-inode 的 1296。建文件开销三者基本持平,本 patch 修正的是 xattr 带来的额外占用。