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
图解
先看这张图,再读正文
一句话:xattr 就是挂在文件上的标签——文件内容照常存,标签另外挂,互不干扰。
它在 EROFS 里有三类用途,重要性依次是:
| 用途 | 一句话 | 谁在读它 |
|---|---|---|
| ① 传统:安全与权限 | SELinux 标签 / POSIX ACL / capabilities / user.* |
内核 LSM、VFS、getfattr(1)、Android 系统服务 |
| ② EROFS 特有:ishare 指纹(最关键) | mkfs 算好内容指纹存进 xattr,用来判断”哪些文件内容相同、可共享页缓存” | erofs_xattr_fill_inode_fingerprint() |
| ③ prefix:长前缀编号化 | trusted.overlay. 这类前缀存成表,条目只存 1 字节编号 |
erofs_getxattr(base_index, infix) |
⚠️ 最容易低估的是第 ② 条:ishare 的指纹就存在 xattr 里。
镜像里没有 ishare xattrs 时,内核会打印
on-disk ishare xattrs not found. Turning off inode_share. 并整体关闭该特性——
没有 xattr,ishare 根本活不了。详见 11 专题。
底部那条链讲的是”怎么存”:
mkfs 按出现次数在 inline(独享、局部性好)与 shared(共用、去重)之间二选一,是典型的空间 vs 局部性权衡;
内核侧两种都要会读,靠 erofs_xattr_iter_inline() / _shared() 两个遍历函数 + 上层一个回调完成。
一、特性缘由:为什么 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 路径(erofs_listxattr(),xattr.c):
用户态 listxattr(path) / getfattr -d
↓ 系统调用
VFS
↓ inode->i_op->listxattr
erofs_listxattr(dentry, buffer, size) 【xattr.c】
│
├─① erofs_init_inode_xattrs(inode) 【xattr.c】
│ └ 读 ibody header → 填 vi->xattr_isize /
│ xattr_shared_count / xattr_shared_xattrs[]
│
├─② 填好 it:sb / buf / dentry / buffer /
│ buffer_size / buffer_ofs = 0
│
├─③ erofs_xattr_iter_inline(&it, inode, false) 【xattr.c】★驱动者
│ ├ erofs_init_metabuf(&it->buf, ...)
│ ├ it->pos = erofs_iloc(inode) + inode_isize + xattr_header_sz
│ └ while (remaining) {
│ it->kaddr = erofs_bread(&it->buf, it->pos, true)
│ erofs_listxattr_foreach(it) ← ★回调(每个 entry 一次)
│ ├ erofs_xattr_prefix(base_index, it->dentry)
│ └ erofs_xattr_copy_to_buffer(it, ...) 【xattr.c】
│ }
│
├─④ erofs_xattr_iter_shared(&it, inode, false) 【xattr.c】★驱动者
│ └ while (i < vi->xattr_shared_count) {
│ it->pos = erofs_pos(sb, sbi->xattr_blkaddr)
│ + vi->xattr_shared_xattrs[i++] * sizeof(__le32)
│ it->kaddr = erofs_bread(&it->buf, it->pos, true)
│ erofs_listxattr_foreach(it) ← ★同一个回调
│ }
│
├─⑤ erofs_put_metabuf(&it.buf) 释放元数据游标
└─⑥ return ret ? ret : it.buffer_ofs
getxattr 路径(erofs_getxattr(),xattr.c)结构完全一样,差别只有三处:
| listxattr | getxattr | |
|---|---|---|
| 入口 | inode->i_op->listxattr |
handler->get → erofs_xattr_generic_get() → erofs_getxattr() |
it 多填的字段 |
— | it.index(命名空间编号)、it.name = QSTR(name) |
| 回调 | erofs_listxattr_foreach() |
erofs_getxattr_foreach() |
另外 getxattr 在 ① 之后多做一步快速排除:
/* 有 xattr_filter 时,先用哈希位图判断"这个名字肯定不存在" */
hashbit = xxh32(name, strlen(name), EROFS_XATTR_FILTER_SEED + index);
hashbit &= EROFS_XATTR_FILTER_BITS - 1;
if (vi->xattr_name_filter & (1U << hashbit))
return -ENODATA; /* 连元数据都不用读了 */
记住这条主线:erofs_init_inode_xattrs() 先准备好”有哪些 xattr”,
然后 erofs_xattr_iter_inline() / _shared() 两个驱动者挨个取 entry,
每取一个回调一次 *_foreach() —— 上层只写这一个回调,
两种存储方式的差异被完全封装在驱动者里(就是理念 2 说的”迭代器 + 回调”)。
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;
erofs_off_t pos;
void *kaddr;
char *buffer;
int buffer_size, buffer_ofs;
/* getxattr */
int index, infix_len;
struct qstr name;
/* listxattr */
struct dentry *dentry;
};
| 字段 | 用途 |
|---|---|
sb |
所属超级块 |
buf |
元数据游标(02 专题的 erofs_buf),遍历时临时映射页 |
pos |
当前在元数据里的偏移 |
kaddr |
当前映射页的内核虚拟地址 |
buffer / buffer_size / buffer_ofs |
输出缓冲区、容量、当前写入位置 |
index / infix_len / name |
getxattr 专用:要找的命名空间编号、长前缀的中缀长度、名字 |
dentry |
listxattr 专用:用于取前缀(erofs_xattr_prefix(base_index, it->dentry)) |
要点:
buf是erofs_buf—— 遍历元数据时要临时映射页, 用完必须erofs_put_metabuf()(02 专题强调过的契约)。 看erofs_getxattr()末尾:ret = erofs_xattr_iter_inline(&it, inode, true); if (ret == -ENODATA) ret = erofs_xattr_iter_shared(&it, inode, true); erofs_put_metabuf(&it.buf); return ret ? ret : it.buffer_ofs;buffer_ofs记录写到了哪——xattr 可能很多,需要累积;最终它也是返回值- inode 不在结构体里,而是作为参数传给两个遍历函数:
static int erofs_xattr_iter_inline(struct erofs_xattr_iter *it, struct inode *inode, bool getxattr); static int erofs_xattr_iter_shared(struct erofs_xattr_iter *it, struct inode *inode, bool getxattr);第三个参数
getxattr决定回调走 get 还是 list(erofs_getxattr_foreach/erofs_listxattr_foreach) xattr_id已消失:shared 条目编号现在由erofs_init_inode_xattrs()读进vi->xattr_shared_xattrs[], 遍历时直接用;xattr_id只是那个函数里的一个局部变量(见xattr.c)
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 支持会影响哪个高级特性?
自测答案
点击展开
-
ishare 的文件指纹存在 xattr 里。没有 xattr 支持,
erofs_xattr_fill_inode_fingerprint()取不到指纹, page cache sharing 就无法工作。 -
inline:只被一个 inode 用 → 紧跟 inode,局部性好,但相同内容会存多份。 shared:被多个 inode 共用 → 集中在
xattr_blkaddr,去重省空间, 但多一次跳转。 -
mkfs 决定(不是内核)。mkfs 统计出现次数: 一次 → inline;多次 → 提升为 shared。
-
buf是struct erofs_buf(元数据游标)。 用完必须调用erofs_put_metabuf()释放——这是 02 专题强调过的契约。 -
共用
erofs_xattr_iter遍历器与erofs_xattr_iter_inline/shared(), 只把回调换掉:list 用erofs_listxattr_foreach(), get 用erofs_getxattr_foreach()。 -
把
trusted.overlay.这类重复出现的长前缀存成表, 条目里只存编号,省空间(容器场景前缀重复率极高)。 -
/tmp是 tmpfs,不支持user.*xattr(errno 95,EOPNOTSUPP)。 应改用 ext4,例如/sdd。 -
不能。它只有
-S -e -s --ls --cat --nid --path,没有 xattr 选项, 只能显示Xattr size(大小)。要看内容必须挂载。 -
三条: ① 宿主机
erofsfuse挂载 +getfattr(不需内核支持); ② QEMU 内核挂载 + 静态编译的listxattr; ③ 宿主机dump.erofs只能看大小(不算真正读出内容)。 -
ishare / page cache sharing(11 专题)——指纹依赖 xattr, 没有指纹就无法判断哪些文件内容相同。