12 · 特性专题:xattr(扩展属性)
对应配置:
CONFIG_EROFS_FS_XATTR(及其子项_POSIX_ACL/_SECURITY) 主源码:
xattr.c、xattr.h相关:internal.h(xattr_isize、xattr_blkaddr等字段)本文回答:xattr 是什么、EROFS 为什么需要它 → 两种存储方式怎么权衡 → 内核怎么遍历与查找 → 关键结构体与函数 → 用户态如何读到。
本专题目标(读完你应该能做到什么)
- 说清 xattr 是什么、EROFS 为什么特别依赖它(不止是”扩展属性”那么简单)
- 区分 inline 与 shared 两种存储方式,并说清各自的取舍
- 看懂
erofs_xattr_iter这个”遍历上下文”的作用 - 解释
xattr_filter/xattr_prefix_start这类新机制在解决什么 - 知道 ishare 的指纹就存在 xattr 里(与 11 专题的联系)
- 能在虚拟机里实际读出 EROFS 镜像的 xattr
图解
先看这张图,再读正文
一、特性缘由:为什么 EROFS 需要 xattr
1.1 xattr 本身:给文件附加”键值对”
xattr(扩展属性)就是挂在文件上的额外信息:
security.selinux = "system_u:object_r:..."
user.mime_type = "text/plain"
传统用途:
| 用途 | 例子 |
|---|---|
| SELinux 安全标签 | security.selinux |
| POSIX ACL | system.posix_acl_access |
| capabilities | security.capability |
| 用户自定义 | user.* |
对只读文件系统一样重要——Android 就大量依赖 security.selinux。
1.2 但 EROFS 还有个特殊用途:指纹
这是 EROFS 里 xattr 最关键、也最容易被忽略的用途:
ishare(page cache sharing)的文件指纹存在 xattr 里。
见 11 专题:erofs_xattr_fill_inode_fingerprint() 从 xattr 读出指纹,
用来判断”哪些文件内容相同、可以共享页缓存”。
⇒ 没有 xattr 支持,ishare 根本无法工作。
(实测现象:挂载一个没有 ishare xattrs 的镜像并指定 inode_share 时,
内核会打印 on-disk ishare xattrs not found. Turning off inode_share.
——就是这条依赖的直接体现。)
1.3 还有一个用途:xattr prefix(前缀表)
较新的 EROFS 引入了 xattr prefix 机制:把常见的前缀(如 trusted.overlay.)
存成一张表,条目里只存编号,从而省空间。
相关字段见 erofs_sb_info:xattr_prefix_start、xattr_prefix_count、
xattr_prefixes、ishare_xattr_prefix_id。
二、设计理念
理念 1:两种存储,按”是否值得共享”选择
EROFS 把 xattr 存成两种形式:
| inline | shared | |
|---|---|---|
| 位置 | 紧跟在 inode 后面 | 集中在 xattr_blkaddr 指向的元数据块 |
| 适用 | 只被一个 inode 用 | 被多个 inode 共用 |
| 优点 | 局部性好,读 inode 顺带拿到 | 去重——相同 xattr 只存一份 |
| 代价 | 多份相同 xattr 会浪费空间 | 多一次跳转 |
判断依据:mkfs 时统计。若某组 xattr 只出现一次 → inline; 若多次出现 → 提升为 shared。
⇒ 这是典型的空间 vs 局部性权衡。
理念 2:用”迭代器 + 回调”遍历
xattr 的两种存储差异很大,如果每次操作都写两套代码会很啰嗦。
EROFS 的做法:定义 struct erofs_xattr_iter 作为遍历上下文,
配两个”遍历函数”:
erofs_xattr_iter_inline()—— 遍历 inline 部分erofs_xattr_iter_shared()—— 遍历 shared 部分
上层(list / get)只写一个回调,由遍历函数负责”怎么挨个取出来”。
理念 3:prefix 换编号,进一步压缩
trusted.overlay. 这类长前缀在容器场景里成千上万次出现。
存成表后,每个条目只存 1 字节编号——省下的空间相当可观。
三、实现架构
3.1 磁盘布局
inode(on-disk)
├── 定长部分
├── 变长部分(若有)
├── xattr 区(inline) ← xattr_isize 指定大小
│ └── 多个 entry:name_len / value_size / 后缀名 / 值
└── ...
元数据区 xattr_blkaddr
└── shared xattr 条目(按哈希/编号索引)
└── 多个 inode 指向同一条
3.2 内核读取路径
用户态 listxattr(path)
↓ 系统调用
VFS
↓ inode->i_op->listxattr
erofs_listxattr() 【xattr.c】
↓
erofs_init_inode_xattrs() 读 inode 的 xattr 区大小/位置
↓
erofs_listxattr_foreach(it) 遍历回调
↓
erofs_xattr_iter_inline(it) ──► 遍历 inline 区
erofs_xattr_iter_shared(it) ──► 遍历 shared 区
↓
erofs_xattr_copy_to_buffer() 拷到用户缓冲区
getxattr 路径同理,只是回调换成 erofs_getxattr_foreach()。
3.3 共享 xattr 的”提升”
mkfs 侧的行为(不是内核):统计每个 xattr 集合出现次数, 多次出现的写入 shared 区,inode 里只留一个引用编号。
内核侧只负责两种都会读。
四、关键结构体
4.1 struct erofs_xattr_iter(xattr.c)
遍历上下文——贯穿所有 xattr 操作:
struct erofs_xattr_iter {
struct super_block *sb;
struct erofs_buf buf; /* 元数据游标(02 专题的 erofs_buf)*/
struct inode *inode;
char *buffer; /* 输出缓冲区 */
int buffer_size, buffer_ofs; /* 容量与当前写入位置 */
unsigned int xattr_id; /* shared 时的编号 */
...
};
要点:
buf是erofs_buf—— 遍历元数据时要临时映射页, 用完必须erofs_put_metabuf()(02 专题强调过的契约)buffer_ofs记录写到了哪——xattr 可能很多,需要累积
4.2 erofs_inode 中的 xattr 字段(internal.h)
unsigned int xattr_isize; /* inline xattr 区大小 */
unsigned int xattr_name_filter; /* 名字过滤位图 */
unsigned int xattr_shared_count; /* shared xattr 条目数 */
unsigned int *xattr_shared_xattrs; /* shared 条目编号数组 */
4.3 erofs_sb_info 中的 xattr 字段(internal.h)
u32 xattr_blkaddr; /* shared 区起始块 */
u32 xattr_prefix_start; /* prefix 表起始 */
u8 xattr_prefix_count; /* prefix 数量 */
u8 ishare_xattr_prefix_id; /* ishare 指纹用的 prefix id */
struct erofs_xattr_prefix_item *xattr_prefixes; /* prefix 表 */
unsigned int xattr_filter_reserved;
4.4 struct xattr_handler(内核通用)
EROFS 为不同命名空间注册不同 handler:
| handler | 命名空间 |
|---|---|
erofs_xattr_user_handler |
user.* |
erofs_xattr_trusted_handler |
trusted.* |
erofs_xattr_security_handler |
security.* |
| (POSIX ACL) | system.posix_acl_* |
每个 handler 决定”这个命名空间的 xattr 要不要显示/能不能读”
(例如 user.* 在非特权场景可隐藏)。
五、主要函数
5.1 遍历与查找
| 函数 | 作用 |
|---|---|
erofs_init_inode_xattrs() |
初始化:算出 inode 的 xattr 区在哪、多大 |
erofs_xattr_iter_inline() |
遍历 inline 区的条目 |
erofs_xattr_iter_shared() |
遍历 shared 区的条目 |
erofs_listxattr_foreach() |
“列出全部”用的回调 |
erofs_getxattr_foreach() |
“取某一个”用的回调 |
erofs_xattr_copy_to_buffer() |
把结果拷进用户缓冲区 |
设计要点:list 与 get 共用遍历器,只换回调——
这就是理念 2 的落地。
5.2 对 VFS 的接口
| 函数 | 作用 |
|---|---|
erofs_listxattr() |
列出某文件的全部 xattr 名 |
erofs_getxattr() |
取某个 xattr 的值 |
它们注册到 inode_operations / xattr_handler,由 VFS 回调。
5.3 prefix 机制
| 函数 | 作用 |
|---|---|
erofs_xattr_prefixes_init() |
挂载时读入 prefix 表 |
erofs_xattr_prefixes_cleanup() |
卸载时释放 |
六、来龙去脉:一次 getfattr 的完整过程
① mkfs 时:
├ 收集每个文件的 xattr
├ 只出现一次 → inline(写进 inode 后面)
└ 出现多次 → shared(写进 xattr_blkaddr 区,inode 记编号)
│
② 挂载:
└ erofs_xattr_prefixes_init() 读入 prefix 表
│
③ 用户态执行 getfattr -d /mnt/f.txt
│
④ 系统调用 listxattr / getxattr
│
⑤ VFS → erofs_listxattr() / erofs_getxattr()
│
⑥ erofs_init_inode_xattrs()
├ 读 inode 的 xattr_isize / xattr_shared_count
└ 算出 inline 区与 shared 编号数组的位置(用 erofs_buf 映射元数据)
│
⑦ 遍历:
├ erofs_xattr_iter_inline() → 挨个取 inline 条目
└ erofs_xattr_iter_shared() → 按编号去 shared 区取
│
⑧ 每个条目交给回调(foreach)
└ erofs_xattr_copy_to_buffer() 写进用户缓冲区
│
⑨ erofs_put_metabuf() 释放元数据映射
│
⑩ 返回用户态 → getfattr 打印
user.test1="hello-xattr"
user.test2="second-value"
七、动手验证
下面的方法都实测过(详见
erofs-analysis/10-EROFS-xattr-用户态读取验证.md)。
验证 1:宿主机侧确认镜像里有 xattr(不用挂载)
/opt/erofs-utils/bin/dump.erofs --path=/f.txt /tmp/erofs-lab/xattr.erofs
看输出的 Xattr size::
Xattr size: 104→ 有 xattrXattr size: 0→ 没有
对照:用 mkfs.erofs -x -1 禁用 xattr 造的镜像,Xattr size: 0。
⚠️
dump.erofs只能看大小,列不出 key=value。 想看内容必须挂载(验证 2、3)。
验证 2:宿主机用 erofsfuse(不需要内核支持)
mkdir -p /mnt/xattr
/opt/erofs-utils/bin/erofsfuse /tmp/erofs-lab/xattr.erofs /mnt/xattr
getfattr -R -d /mnt/xattr
umount /mnt/xattr
验证 3:QEMU 虚拟机里用内核挂载
# VM 内
mount -t erofs /host/xattr.erofs /mnt/x
/host/listxattr /mnt/x/f.txt # user.test1 / user.test2
/host/listxattr /mnt/x/g.txt # user.foo
⚠️ VM 里是 busybox 最小系统,没有
getfattr。 需要宿主机静态编译一个listxattr丢进共享目录 (gcc -static,源码见img-stable/test/tools/listxattr.c)。
验证 4:造一个带 xattr 的镜像
# 注意:必须在 ext4 上造,/tmp 是 tmpfs 不支持 user.* xattr!
mkdir -p /sdd/xattrtest/fs
echo "hello" > /sdd/xattrtest/fs/f.txt
setfattr -n user.test1 -v "hello-xattr" /sdd/xattrtest/fs/f.txt
/opt/erofs-utils/bin/mkfs.erofs /sdd/xattrtest/xattr.erofs /sdd/xattrtest/fs
八、常见误解(重要)
误解 1:dump.erofs 能列出 xattr 内容
不能。它只有 -S -e -s --ls --cat --nid --path 这些选项,
没有 xattr 选项,只能显示 Xattr size(大小)。
要看内容必须挂载(erofsfuse 或内核挂载)。
误解 2:xattr 只是一种”附加信息”,可有可无
对 EROFS 不是——ishare 的指纹就存在 xattr 里。 没有 xattr 支持,page cache sharing 直接失效。
误解 3:inline 一定比 shared 好
不一定。inline 局部性好,但相同 xattr 会被复制多份; shared 去重省空间,但多一次跳转。 mkfs 会按出现次数自动选择。
误解 4:在 /tmp 里造带 xattr 的文件没问题
有问题。/tmp 是 tmpfs,不支持 user.* xattr,
setfattr 会报 Operation not supported(errno 95)。
要用 ext4(如 /sdd)。
误解 5:VM 里能用 getfattr
不能(busybox 最小系统没带)。要么宿主机静态编译一个工具,
要么用 erofsfuse 在宿主机直接读。
九、与其他特性的关系
| 特性 | 关系 |
|---|---|
| ishare / page cache sharing(11 专题) | 强依赖——指纹存在 xattr 里 |
| 多设备 | shared xattr 区也走 erofs_map_dev(),可跨设备 |
| metabox | shared xattr 可放在 metabox 里(erofs_sb_has_shared_ea_in_metabox()) |
| POSIX ACL / security | 都是 xattr 的特殊命名空间,靠 handler 区分 |
| on-disk 格式(01 专题) | xattr_isize 是 inode 变长部分的关键字段 |
自测检查点
- xattr 在 EROFS 里除了传统用途,还有什么关键作用?
- inline 与 shared 两种存储各自的适用场景与代价?
- 谁来决定某个 xattr 用 inline 还是 shared?
erofs_xattr_iter里的buf是什么?用完要做什么?- list 与 get 两个操作如何复用遍历逻辑?
- prefix 机制解决什么问题?
- 为什么在
/tmp里造不了user.*xattr?该去哪造? dump.erofs能看到 xattr 的内容吗?为什么?- 想实际读出 EROFS 镜像的 xattr,有哪几条路?
- 没有 xattr 支持会影响哪个高级特性?