阶段 6:高级特性
本章的特性都”挂在主干上”,没有一个独立成体系。 按挂载点分类记忆,比按字母顺序背清单有效得多。
📌 先看这里:本章是概览,深度内容在特性专题(09–18)
本章用「① 解决什么问题 → ② 磁盘上怎么表示 → ③ 内核怎么用 → ④ 现状与坑」 四段式覆盖 9 个特性,每个约百行——目的是让你先建立全景。
若要深入理解某个特性(关键结构体、主要函数、完整来龙去脉), 请跳转到对应的特性专题——那里每份都是独立的深度文档(389–631 行):
| 本章小节 | 深度专题 |
|---|---|
| 6.1 xattr(扩展属性) | 12-特性-xattr扩展属性 |
| 6.2 多设备与 device table | 13-特性-多设备与device-table |
| 6.3 fileio 文件后端 | 09-特性-文件后端 |
| 6.4 FSDAX | 10-特性-FSDAX |
| 6.5 48-bit 地址 | 16-特性-48bit地址 |
| 6.6 fragment 与 ztailpacking | 14-特性-fragment与ztailpacking |
| 6.7 rolling-hash 去重 | 15-特性-rolling-hash去重 |
| 6.8 metabox | 18-特性-metabox元数据压缩 |
| 6.9 ishare(page cache share) | 11-特性-page-cache-sharing |
| —(本章未涉及)硬件解压加速 | 17-特性-硬件解压加速 |
三对最容易混淆的概念:
| 混淆对 | 区分要点 |
|---|---|
| fragment vs tail-packing(→14) | fragment = 多文件零头集中到 packed inode;tail-packing = 单文件尾部内联进自己的 inode |
| dedupe vs fragment(→15) | dedupe 消除跨文件相同块;fragment 处理压缩后填不满 pcluster 的零头 |
| DAX vs DIO(→10) | DIO 仍走块设备层;DAX 完全不走,直接映射持久内存 |
本阶段目标
读完这一章,你应该能够:
- 说出本章每个特性解决什么问题、挂在哪个模块上
- 解释 xattr 的 inline / shared 两种形式
- 说清多设备寻址的完整链路,并解释为什么”镜像分层(layering)”不是内核概念
- 区分 fileio 与块设备后端,知道 fscache 后端已被移除
- 说清 FSDAX 在 EROFS 里的作用与”设备粒度”的真相
- 区分 fragment 与 rolling-hash 去重(最容易混淆的一对)
- 解释 metabox 与 ishare 各自的设计动机
前置要求
- 完成阶段 0~5(本章多处引用前面的知识)
6.0 全景
按挂载点分类:
| 类别 | 特性 | 挂在哪个模块上 |
|---|---|---|
| 挂 inode | xattr、metabox | inode 解析 |
| 挂地址解析 | 多设备、48-bit | 映射函数 |
| 挂数据来源 | fileio、FSDAX | 设备/IO 层 |
| 挂压缩路径 | fragment、ztailpacking、去重 | zmap / zdata |
| 挂缓存复用 | ishare | page cache |
下面每个特性按四段式讲:① 解决什么问题 → ② 磁盘上怎么表示 → ③ 内核怎么用 → ④ 现状与坑。
6.1 xattr(扩展属性)
① 解决什么问题
文件的额外元数据(如 SELinux 标签 security.selinux、capabilities)。
Linux 的 xattr 是标准接口,文件系统必须支持。
② 磁盘上怎么表示
两种形式:
| 形式 | 位置 | 适用 |
|---|---|---|
| inline | 紧跟在 inode 结构之后 | 少量、仅本文件用的 xattr |
| shared | 集中在共享 xattr 区域(xattr_blkaddr) |
多个文件相同的 xattr |
shared 的价值:容器镜像里成千上万个文件常有相同的 SELinux 标签, 存一份、大家引用,能省下大量空间。
③ 内核怎么用
入口 erofs_init_inode_xattrs()(xattr.c):
vi->xattr_shared_count = ih->h_shared_count; /* xattr.c */
if ((u32)vi->xattr_shared_count * sizeof(__le32) > ...)
... /* 边界检查 */
vi->xattr_shared_xattrs = kmalloc_objs(uint, vi->xattr_shared_count);
...
for (i = 0; i < vi->xattr_shared_count; ++i) {
...
}
内联 xattr 从 inode 后面读;shared 的先读出”哪些 shared xattr”的索引数组, 再按索引去共享区取。
④ 现状与坑
⚠️ xattr 路径是 erofs_buf 释放义务断掉过的地方——这是本项目核查发现的历史 bug。
阶段 2 讲过,erofs_put_metabuf() 的调用全靠人工维系。
xattr 的遍历过程中,两个迭代函数之间曾漏掉 put。
这就是阶段 2 知识的实战检验:
读 xattr 代码时,看到erofs_bread/erofs_read_metabuf就去找对应的put, 尤其是跨函数的调用链——最容易漏。
6.2 多设备与 device table
① 解决什么问题
一个 EROFS 镜像的数据可以分布在多个设备/文件上。 典型场景:容器镜像的分层存储,每层是一个独立的 blob 文件。
② 磁盘上怎么表示
superblock 里有 extra_devices(额外设备数)和 devt_slotoff(设备表位置)。
每个设备一个槽位(erofs_fs.h):
struct erofs_deviceslot {
...
/* tag[64] / blocks / uniaddr */
};
字段含义:
tag:设备标识(用于匹配)blocks:该设备提供多少块uniaddr:该设备在统一地址空间里的起始块号
③ 内核怎么用
挂载时 erofs_scan_devices()(super.c)扫描设备表,
把设备信息存进 idr 树(devs->tree),按设备号索引。
读数据时,映射函数产出 m_deviceid,再由 erofs_map_dev()(data.c)定位设备:
if (map->m_deviceid) {
dif = idr_find(&devs->tree, map->m_deviceid - 1); /* data.c */
...
if (devs->flatdev) {
map->m_pa += erofs_pos(sb, dif->uniaddr);
return 0; /* 扁平模式:还是主设备,只加偏移 */
}
erofs_fill_from_devinfo(map, sb, dif); /* 换设备 */
}
三种情况:
| 情况 | 处理 |
|---|---|
m_deviceid == 0 |
主设备 dif0 |
m_deviceid != 0,flatdev 模式 |
仍用主设备,地址加 uniaddr |
m_deviceid != 0,正常模式 |
换成对应设备的 bdev |
④ 现状与坑 —— ⚠️ “layer” 不是内核概念
这是本节最重要的认知。
T4 那条命令看起来很直观:
mkfs.erofs meta.erofs layer0.erofs layer1.erofs ...
但内核里根本没有 “layer” 这个抽象。
实证:
grep -rin layer fs/erofs/
# → 零命中
内核看到的是一个扁平的设备数组。它不知道:
- 哪个设备是”第几层”
- 层与层之间的依赖关系
“层 N ↔ 设备 N” 的对应关系是 mkfs 的 rebuild 模式建立的
(erofs-utils/mkfs/main.c,每个输入镜像 ++rebuild_src_count)。
严格说:EROFS 具备的是”多设备 / 外部 blob“这一能力, layering 是 mkfs 在它之上构建出的用法。
引用时若说”内核支持镜像分层”,会被内核开发者当场纠正。
另一个易混点:多 layer ≠ --blobdev
| 多输入镜像(rebuild) | --blobdev=X |
|
|---|---|---|
| 设备数 | = 输入镜像数 | 只建 1 个额外设备 |
| device_id | 依次为 1, 2, 3… | 恒为 1 |
6.3 fileio 文件后端
① 解决什么问题
镜像不一定在块设备上——它可能只是一个普通文件(容器镜像里的一个 layer)。
传统做法要先 losetup 成块设备,麻烦且需要特权。
② 磁盘上怎么表示
镜像本身格式不变,只是数据来源从块设备换成了文件。
③ 内核怎么用
/* internal.h */
static inline bool erofs_is_fileio_mode(struct erofs_sb_info *sbi)
{
return IS_ENABLED(CONFIG_EROFS_FS_BACKED_BY_FILE) && sbi->dif0.file;
}
fileio 模式下,buf->mapping 指向后备文件的 f_mapping
(阶段 2 的 erofs_init_metabuf,data.c),
即借用后备文件系统的 page cache,避免双重缓存。
配置:CONFIG_EROFS_FS_BACKED_BY_FILE(默认 y)。
④ 现状与坑
EROFS 早期基于 fscache 的后端已在 7.2 内核被整体移除。
移除理由:fscache 引入 netfs 硬依赖、不够灵活, 已被 “file-backed mounts + fanotify pre-content hooks” 替代。
⚠️ 注意:T2/T3/T4 三份演讲都还在宣传 EROFS over fscache。 演讲材料与当前源码已经脱节——读材料时要注意时间点。
6.4 FSDAX
① 解决什么问题
让虚拟机/容器直接访问宿主机的内存,绕过 guest 的 page cache, 实现”主机与客户机共享镜像缓存”。
② 磁盘上怎么表示
不涉及磁盘格式,是运行时能力。
③ 内核怎么用
每个设备可以有一个 dax_dev(阶段 2 的 sbi->dif0.dax_dev):
/* super.c */
dif->dax_dev = fs_dax_get_by_bdev(file_bdev(file), ...);
...
if (!dif->dax_dev && test_opt(&sbi->opt, DAX_ALWAYS)) { /* super.c */
...
}
挂载选项:-o dax=always / dax=never。
启用后 iomap 会把 iomap->dax_dev 填好(阶段 3 的 data.c)。
④ 现状与坑
⚠️ T4 讲的 “layer-granularity FSDAX” 其实是设备粒度的。
内核侧是 per-device 的 dax_dev(每个设备一个),
没有专门的 layer 代码。
所谓”层粒度”,本质上就是”每个 layer 恰好对应一个设备”带来的效果。
这与 6.2 节的认知一致:内核只认设备,不认层。
6.5 48-bit 地址
① 解决什么问题
普通 EROFS 用 32 位块地址,最大 2^32 × 4096 = 16 TB。 更大容量的设备需要更多位。
② 磁盘上怎么表示
EROFS_FEATURE_INCOMPAT_48BIT(superblock 的 incompat 位)。
启用后,块地址变成 48 位(与 chunk 地址的低 48 位呼应,见阶段 3)。
③ 内核怎么用
super.c 解析 48 位地址。
注意它会打印警告:
/* super.c */
erofs_info(sb, "EXPERIMENTAL 48-bit layout support in use. Use at your own risk!");
④ 现状与坑
🔶 实验性。源码自己就写着 “EXPERIMENTAL … Use at your own risk!”。
演讲里从未把它当作稳定特性宣传——这是官方的诚实之处。
6.6 fragment 与 ztailpacking
① 解决什么问题
尾部浪费:每个文件压缩后,尾部往往剩一小段不足一个压缩单元的数据。 如果各自存一个块,浪费巨大。
两个方案:
- fragment:把所有文件的尾部压缩数据合并放进一个共享的 packed inode
- ztailpacking:把尾部数据内联在 inode 后面(连 packed inode 都不用)
② 磁盘上怎么表示
| feature 位 | 数据位置 | |
|---|---|---|
| fragment | INCOMPAT_FRAGMENTS(erofs_fs.h) |
packed inode(packed_nid 指向) |
| ztailpacking | INCOMPAT_ZTAILPACKING(erofs_fs.h) |
inode 之后(z_idata_size 记长度) |
inode 里的标志与字段:
/* zmap.c */
bool fragment = vi->z_advise & Z_EROFS_ADVISE_FRAGMENT_PCLUSTER; /* erofs_fs.h */
bool ztailpacking = vi->z_idata_size; /* internal.h */
③ 内核怎么用
在 z_erofs_map_blocks_fo() 里(阶段 4 讲过):
/* zmap.c */
} else if (fragment && m.lcn == vi->z_tailextent_headlcn) {
map->m_flags = EROFS_MAP_FRAGMENT;
} else {
map->m_pa = erofs_pos(sb, m.pblk); /* 常规压缩数据 */
...
}
EROFS_MAP_FRAGMENT:数据在 packed inode 里,要单独读- ztailpacking:数据在 inode 后,走
EROFS_MAP_META+IOMAP_INLINE
fragmentoff 的 64 位组装(阶段 4 提过,这里补全):
/* zmap.c —— FINDTAIL 时 */
if (fragment && vi->datalayout == EROFS_INODE_COMPRESSED_FULL)
vi->z_fragmentoff |= (u64)m.pblk << 32;
低 32 位来自 h_fragmentoff(header),高 32 位来自尾部索引项被复用的 pblk 槽位。
这是有意的位打包,不是 bug。
④ 现状与坑
fragment 与 ztailpacking 互斥(zmap.c 保证),
不会同时启用。
打包安全:尾部 pcluster 被判为 EROFS_MAP_FRAGMENT,
不会落到 erofs_pos(sb, m.pblk) 那条路径,
所以被复用的 pblk 永远不会被当成块号解释。
6.7 rolling-hash 去重(最容易与 fragment 混淆)
① 解决什么问题
不同文件(或同一文件的不同位置)可能含有完全相同的内容。 检测到重复后,只存一份,多处引用。
② 磁盘上怎么表示
索引项上的标志位 Z_EROFS_LI_PARTIAL_REF(erofs_fs.h)。
③ 内核怎么用
/* zmap.c —— 解析索引项时 */
m->partialref = !!(advise & Z_EROFS_LI_PARTIAL_REF);
...
/* zmap.c —— 填进 map 标志 */
if (m.partialref)
map->m_flags |= EROFS_MAP_PARTIAL_REF;
EROFS_MAP_PARTIAL_REF 使 EROFS_MAP_FULL() 为假,
于是该 pcluster 恒保持 partial=true,
解压时按”只解压前缀部分”处理。
④ 现状与坑 —— ⚠️ 与 fragment 的区分
两者是完全独立的机制:
| fragment | rolling-hash 去重 | |
|---|---|---|
| 目的 | 合并各文件的尾部数据 | 找出重复内容 |
| 位置 | packed inode | 索引项标志位 |
| mkfs 选项 | -Efragdedupe |
-Ededupe |
不同材料里叫法不同,但指同一机制(下列均已回查原文核对):
| 材料 | 叫法(原文) |
|---|---|
内核提交 5c2a64252c5d(2022,Gao Xiang) |
“variable-length global compressed data deduplication with rolling hash” |
| OSS China 2023 | “variant-CDC data deduplication” / “per-extent CDC dedupe” |
| FOSDEM 2023 | “EROFS compressed data deduplication“(未给出算法名) |
| OSS NA 2024 | “Rolling-hash compressed data deduplication” |
⚠️ 一个容易混淆的点:FOSDEM 2023 里出现的 “rolling” 指的是
rolling decompression(阶段 4 讲的滚动解压),
与去重的 rolling hash 不是一回事,别混为一谈。
(同一个词在不同上下文里可能指完全不同的机制 —— 这是读材料时常踩的坑。)
“variant-CDC” 里的 CDC = Content-Defined Chunking
即用 rolling hash 做内容定义的变长切分 ——
与内核提交里的 “variable-length … rolling hash” 是同一机制,
只是不同场合叫法不同。
⚠️ 本项目发现:
EROFS_FEATURE_INCOMPAT_DEDUPE(erofs_fs.h) 内核从不查询。关于查证方式的说明:
erofs_sb_has_dedupe()在源码里grep 不到完整的函数名,
因为它不是手写的函数,而是由internal.h里的宏拼接生成的:#define EROFS_FEATURE_FUNCS(name, compat, feature) \ static inline bool erofs_sb_has_##name(struct erofs_sb_info *sbi) \ { \ return sbi->feature_##compat & EROFS_FEATURE_##feature; \ } ... EROFS_FEATURE_FUNCS(dedupe, incompat, INCOMPAT_DEDUPE) /* 确实注册了 */⇒ 宏注册了 dedupe,这个函数存在;但
fs/erofs/里没有任何调用点。 对照其他特性(调用次数):
函数 调用次数 erofs_sb_has_metabox()4 erofs_sb_has_48bit()3 erofs_sb_has_fragments()1 erofs_sb_has_device_table()1 erofs_sb_has_dedupe()0 所以内核实际上从不查询该 feature 位。
运行时判据是 per-extent 的标志位(
Z_EROFS_LI_PARTIAL_REF等), 不是 superblock 的 feature 位。即:镜像不设该位、而 extent 带 partial 标志时,内核仍会走去重路径。
是否构成问题取决于 partial-ref 路径自身的校验强度 —— 尚未评估。
6.8 metabox
① 解决什么问题
增量构建:往已有镜像里追加文件时, 不想重排整个元数据区(代价太高)。
② 磁盘上怎么表示
superblock 的 metabox_nid 指向一个特殊的 inode,
metabox 的元数据放在它里面。
nid 的最高位(EROFS_DIRENT_NID_METABOX_BIT)作为标记:
置位表示该 inode 的元数据在 metabox 里。
③ 内核怎么用
/* super.c */
... metabox_nid ...
sbi->metabox_nid = le64_to_cpu(dsb->metabox_nid);
if (sbi->metabox_nid & BIT_ULL(EROFS_DIRENT_NID_METABOX_BIT))
...
/* super.c —— 挂载时把 metabox inode 读进来 */
inode = erofs_iget(sb, sbi->metabox_nid);
两个连带影响(前面阶段提过,这里串起来):
erofs_iloc()退化:metabox 里的 inode 位置 =nid << islotbits(不加meta_blkaddr,internal.h)erofs_init_metabuf()换 mapping:用metabox_inode->i_mapping(data.c)
④ 现状与坑
metabox 引入了第二个 mapping,
这正是阶段 2 讲的 P0-3(跨 mapping 复用隐患)成为可能的原因。
因为 metabox 的 mapping 偏移从 0 起算,
与块设备 mapping 的低页号区间天然重叠,
所以页号碰撞不是理论问题。
6.9 ishare(page cache share)
① 解决什么问题
同一台机器上跑几十个容器,每个容器的镜像里有大量相同的文件。 如果各自缓存一份,内存浪费巨大。
目标:内容相同的文件,在内存里只缓存一份。
② 磁盘上怎么表示
不涉及磁盘格式变化(是运行时优化)。
镜像里可以标记哪些 xattr 用作”指纹”(ishare_xattr_prefix_id)。
③ 内核怎么用
思路很巧妙——复用 VFS 已有的 inode cache:
/* ishare.c */
si = iget5_locked(erofs_ishare_mnt->mnt_sb,
...,
erofs_ishare_iget5_eq, /* 比较指纹 */
erofs_ishare_iget5_set, /* 设置指纹 */
&fp);
做法:
- 建一个伪文件系统(
erofs_ishare_mnt,ishare.c)作为指纹 inode 的宿主 - 用文件的”指纹”(某些 xattr 的哈希)作为 key
- 通过
iget5_locked()在伪 fs 里查找/创建 inode - 指纹相同 → 命中同一个 inode → 共享同一份 page cache
入口 erofs_ishare_fill_inode()(ishare.c)。
④ 现状与坑
🔶 仍是 experimental(Kconfig EROFS_FS_PAGE_CACHE_SHARE)。
设计上有个值得注意的点:它不复用 fscache,
而是自己搭了个伪 fs + iget5_locked 的方案。
原因与 6.3 节讲的 fscache 被移除一致——fscache 太重、引入 netfs 硬依赖。
这也解释了为什么 EROFS 的”共享缓存”方案出现过两次不同的实现 (fscache 版 → ishare 版)。
6.10 特性之间的互斥与叠加(叠加矩阵)
本章前面 6.1–6.9 逐个讲了九种特性。它们彼此能不能一起用?
答案取决于问的是哪一层——很多”互斥”其实是把文件级、镜像级、挂载级混在一起问了。
先分清三个层级
| 层级 | 问的是 | 判定依据 |
|---|---|---|
| 文件级 | 同一个 inode 的数据怎么放 | datalayout,五选一,fs/erofs/erofs_fs.h |
| 镜像级 | superblock 里置了哪些特性位 | feature_incompat / feature_compat |
| 挂载级 | mount 时传了什么选项 | sbi->opt,如 dax=always、inode_share |
⇒ 同一文件级互斥的东西,镜像里不同文件仍可以各用一种; 镜像级的位大多可自由叠加;挂载级才有真正的硬互斥。
第 1 层:同一文件只能五选一
| 值 | 布局 | 说明 |
|---|---|---|
| 0 | FLAT_PLAIN |
非压缩 |
| 1 | COMPRESSED_FULL |
压缩 |
| 2 | FLAT_INLINE |
非压缩 + 尾部 inline |
| 3 | COMPRESSED_COMPACT |
压缩 compact |
| 4 | CHUNK_BASED |
chunk-based(多设备 / 稀疏文件) |
⇒ 同一个文件不能既压缩又 chunk-based。但一个镜像里可以同时存在压缩文件、 chunk-based 文件和非压缩文件,这是常态。
为什么”五选一”不等于”整镜像只能一种”
关键:datalayout 是 per-inode 的。它是 i_format 里的 3 个位
(fs/erofs/erofs_fs.h 的 EROFS_I_DATALAYOUT_MASK,值 0x07),
每个 inode 各填各的值,互不影响。内核读的时候也按 inode 分发:
| 判据 / 分发点 | 出处 | 行为 |
|---|---|---|
erofs_inode_is_data_compressed() |
erofs_fs.h |
只认 COMPRESSED_FULL / COMPACT,CHUNK_BASED 不算”压缩” |
erofs_get_aops() |
internal.h |
压缩 → z_erofs_aops;否则 → erofs_aops / erofs_fileio_aops |
erofs_map_blocks() |
data.c |
CHUNK_BASED → erofs_map_chunks();压缩文件不走这里,走 zmap.c 的 z_erofs_map_blocks_iter() |
chunk 自己的 format 字段只有三个标志(erofs_fs.h 的 EROFS_CHUNK_FORMAT_*):
chunk blkbits、INDEXES、48BIT——没有压缩位,
所以不存在”压缩的 chunk”,chunk 里的数据原样存放。
mkfs 怎么决定每个文件用哪种
erofs-utils 在 lib/inode.c 里逐文件判定(简化):
if (sbi->available_compr_algs && erofs_file_is_compressible(im, inode)) {
/* 压缩 → COMPRESSED_FULL / COMPACT */
} else {
erofs_write_unencoded_file(); /* 内部 if (cfg.c_chunkbits) → CHUNK_BASED */
}
即:开了压缩且文件压得动 → 压缩;否则才落回 chunk-based(前提是你给了 --chunksize)。
源码里没有“chunk 与压缩冲突”的检查,两个选项可以同时给。
实测(一个 256K 可压缩文本 + 一个 256K 随机数):
| mkfs 参数 | text.bin | rand.bin |
|---|---|---|
-zlz4 |
Layout 3(压缩,占 1.56%) | Layout 0(FLAT_PLAIN) |
--chunksize=65536 |
Layout 4(CHUNK_BASED) | Layout 4(CHUNK_BASED) |
-zlz4 --chunksize=65536 |
Layout 3(压缩,1.56%) | Layout 4(CHUNK_BASED) |
第三种构建成功、无报错,镜像里两种 layout 并存——这就是”第 1 层互斥、镜像里混装”的真实样子。
⚠️ 两个容易踩的坑
- 开
-z会”抢走” chunk-based 文件:压得动的文件被压成 COMPRESSED_*, 就不再走 chunk、也不会进--blobdev。想要”所有文件都按 chunk 存进 blob 设备”就别加-z; 要精确控制哪些文件压缩,用--compress-hints。 --blobdev强制配--chunksize(mkfs/main.c报错 “–blobdev must be used together with –chunksize”),后果见 13 专题。
第 2 层:镜像级特性位——可叠加,但有三组”位别名”
⚠️ 最容易误读的一点:下面三组是同一个位,dump 会把两个名字都打印出来, 不代表同时开了两个特性:
INCOMPAT_FRAGMENTS 0x20 == INCOMPAT_DEDUPE 0x20
INCOMPAT_COMPR_CFGS 0x02 == INCOMPAT_BIG_PCLUSTER 0x02
INCOMPAT_DEVICE_TABLE 0x08 == INCOMPAT_COMPR_HEAD2 0x08
⇒ 这就是为什么开了 -Eall-fragments 的镜像,dump 会显示 fragments dedupe。
其余特性位互不冲突,可自由叠加:48-bit + 多设备 + 压缩 + metabox + xattr 前缀 可以同时出现在一个镜像上。
两条依赖关系:
COMPAT_SHARED_EA_IN_METABOX(0x8) 依赖INCOMPAT_METABOX(0x100) —— shared xattr 要放进 metabox,没有 metabox 就没有 shared EA 的容器- 挂载
inode_share时若镜像没有COMPAT_ISHARE_XATTRS(0x20),内核自动关掉 ishare
第 3 层:挂载选项——硬互斥与软降级
硬互斥(直接 -EINVAL,挂载失败):
| 组合 | 报错信息 |
|---|---|
inode_share 但不给 domain_id |
domain_id is needed when inode_share is on |
dax=always + inode_share |
FSDAX is not allowed when inode_share is on |
软降级(自动关闭,只打 info 不失败):
| 条件 | 结果 |
|---|---|
dax=always 但块大小 ≠ PAGE_SIZE |
关 DAX(unsupported blocksize for DAX) |
dax=always 但设备没有 dax_dev |
关 DAX(DAX unsupported by block device) |
inode_share 但镜像没有 ISHARE_XATTRS |
关 ishare(on-disk ishare xattrs not found) |
FSDAX 的白名单(最容易踩的一条)
inode.c 里对 DAX 的判定:
inode->i_flags &= ~S_DAX;
if (test_opt(&sbi->opt, DAX_ALWAYS) && S_ISREG(inode->i_mode) &&
(vi->datalayout == EROFS_INODE_FLAT_PLAIN ||
vi->datalayout == EROFS_INODE_CHUNK_BASED))
inode->i_flags |= S_DAX;
⇒ 只有 flat plain 和 chunk-based 能拿到 DAX。由此得出 FSDAX 的边界:
| 与 FSDAX | 能否共存 | 原因 |
|---|---|---|
| 压缩文件 | ❌ | 数据要解压后才在内存里,无法直接映射持久内存 |
| flat inline 尾部 | ❌ | 尾部在 inode 元数据里,不在块设备上 |
| ishare | ❌ | 硬互斥,-EINVAL |
| fileio 文件后端 | ❌ | 文件后端没有 dax_dev,自动降级 |
| chunk-based + 多设备 | ✅ | 容器场景的典型用法 |
ztailpacking 优先于 fragment
两者都管”文件尾部”,但不是报错,而是优先级。zmap.c 里:
if (fragment && !(flags & EROFS_GET_BLOCKS_FINDTAIL) &&
!vi->z_tailextent_headlcn) { /* 用了 ztailpacking 就不走 fragment */
map->m_la = 0;
map->m_llen = inode->i_size;
map->m_flags = EROFS_MAP_FRAGMENT;
return 0;
}
⇒ 同一文件:尾部要么 inline(ztailpacking),要么进 packed inode(fragment), ztailpacking 优先。但两个特性位可以同时置在镜像上,让不同文件各用一种。
速查矩阵
读法:横竖相交处是两者能否共存在同一个镜像 / 同一次挂载里。
| 压缩 | 多设备 | 48-bit | dedupe/fragment | metabox | ishare | FSDAX | fileio | |
|---|---|---|---|---|---|---|---|---|
| 压缩 | — | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| 多设备 | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 48-bit | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ |
| dedupe/fragment | ✅ | ✅ | ✅ | — | ✅ | ✅ | ❌ | ✅ |
| metabox | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ |
| ishare | ✅ | ✅ | ✅ | ✅ | ✅ | — | ❌ | ✅ |
| FSDAX | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | — | ❌ |
| fileio | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | — |
几点解释:
- ishare 与几乎所有镜像特性都兼容——它是挂载层的页缓存共享机制, 不改动磁盘布局,属于正交叠加
- FSDAX 与 fileio 互斥:fileio 的镜像是普通文件,没有
dax_dev - 48-bit / 多设备与 fileio 可并存:48-bit 还改 rootnid 布局(
super.c),fileio 下额外设备用文件打开(erofs_init_device),两者照常生效;真正与 fileio 互斥的是 FSDAX(fileio 无 dax_dev) - 压缩与 FSDAX 互斥、dedupe/fragment 与 FSDAX 互斥:根因都在 DAX 的白名单 (fragment 只出现在压缩文件上,所以一并受限)
术语速查
| 术语 | 含义 | 出处 |
|---|---|---|
| inline / shared xattr | 内联 / 共享的扩展属性 | 6.1 |
erofs_deviceslot |
磁盘上的设备表槽位 | erofs_fs.h |
| flatdev | 多设备扁平模式(都在主设备上,只加偏移) | data.c |
| fileio | 文件后端(镜像是普通文件) | internal.h |
dax_dev |
每设备的 DAX 设备(FSDAX 用) | super.c |
| 48-bit | 块地址扩展为 48 位(实验性) | super.c |
| fragment | 尾部压缩数据合并进 packed inode | erofs_fs.h |
| ztailpacking | 尾部数据内联在 inode 后 | erofs_fs.h |
Z_EROFS_LI_PARTIAL_REF |
索引项标志:部分引用(去重) | erofs_fs.h |
| metabox | 存放增量构建元数据的区域 | super.c |
| ishare | 用伪 fs + iget5_locked 做 page cache 共享 |
ishare.c |
iget5_locked |
VFS 的”按自定义 key 查找/创建 inode”接口 | ishare.c |
自测检查点
- xattr 有哪两种形式?shared 形式为什么能省空间?
- 读 xattr 代码时要特别留意什么?(提示:阶段 2 的契约)
- 多设备的设备信息存在什么数据结构里?按什么索引?
m_deviceid == 0时用什么设备?flatdev 模式怎么处理?- 为什么说”内核支持镜像分层”这个说法是错的?给出实证方法。
- 多输入镜像(rebuild)与
--blobdev有什么区别? - fileio 后端借用了谁的 page cache?为什么?
- EROFS 早期的 fscache 后端现在还在吗?
- FSDAX 的
dax_dev是全局的还是每设备的?T4 讲的”layer 粒度”真相是什么? - 48-bit 地址特性的现状如何?内核怎么提示用户的?
- fragment 与 ztailpacking 的区别?它们能同时启用吗?
- fragment 与 rolling-hash 去重是同一机制吗?最硬的区分证据是什么?
- 演讲里的 “variant-CDC” 是什么?为什么和 rolling hash 是一回事?
INCOMPAT_DEDUPE这个 feature 位内核会检查吗?有什么影响?- metabox 的引入带来了什么副作用?
- ishare 是怎么复用 VFS inode cache 的?为什么不用 fscache?
自测答案
点击展开答案
1. xattr 的两种形式?
- inline:紧跟在 inode 结构之后,适合仅本文件使用的少量 xattr
- shared:集中在共享 xattr 区域(
xattr_blkaddr),多个文件引用同一份
shared 能省空间,是因为容器镜像里成千上万个文件常有相同的 SELinux 标签, 存一份大家引用即可。
2. 读 xattr 代码要留意什么?
erofs_put_metabuf() 的调用——尤其是跨函数的调用链。
xattr 路径是本项目核查发现”释放义务断掉过”的地方:
两个迭代函数之间曾漏掉 put。
这正是阶段 2 讲的”人工契约”最容易出问题的形态。
3. 设备信息存在哪?
存在 idr 树里(devs->tree),按设备号索引。
挂载时由 erofs_scan_devices()(super.c)扫描设备表填充。
4. m_deviceid == 0?flatdev?
m_deviceid == 0→ 主设备dif0- flatdev 模式 → 仍用主设备,但地址加上
uniaddr(data.c) - 正常模式 → 换成对应设备的
bdev(data.c)
5. 为什么”内核支持镜像分层”是错的?
因为内核里没有 layer 抽象。实证方法:
grep -rin layer fs/erofs/
# → 零命中
内核只看到一个扁平的设备数组(idr 树)。 “层 N ↔ 设备 N” 是 mkfs 的 rebuild 模式建立的。
严格说,EROFS 有的是”多设备/外部 blob”能力, layering 是 mkfs 在其上构建的用法。
6. rebuild vs –blobdev?
| 多输入镜像(rebuild) | --blobdev=X |
|
|---|---|---|
| 设备数 | = 输入镜像数 | 只建 1 个额外设备 |
| device_id | 依次 1, 2, 3… | 恒为 1 |
7. fileio 借谁的 page cache?
借后备文件的 f_mapping(data.c),
即借用后备文件系统的 page cache,
避免双重缓存(同一份数据在内存里存两份)。
8. fscache 后端还在吗?
不在了。已在 7.2 内核被整体移除, 理由是引入 netfs 硬依赖、不灵活, 被 “file-backed mounts + fanotify pre-content hooks” 替代。
⚠️ T2/T3/T4 三份演讲都还在宣传它——材料与源码已脱节。
9. dax_dev 是全局还是每设备?
每设备的(dif->dax_dev)。
T4 讲的 “layer-granularity FSDAX” 真相是: 设备粒度的通用多设备 FSDAX,没有专门的 layer 代码。 之所以看起来像”层粒度”,只是因为每个 layer 恰好对应一个设备。
10. 48-bit 现状?
🔶 实验性。内核会打印警告(super.c):
EXPERIMENTAL 48-bit layout support in use. Use at your own risk!
11. fragment vs ztailpacking?能同时启用吗?
| fragment | ztailpacking | |
|---|---|---|
| 数据放哪 | packed inode | inode 之后 |
| 长度字段 | z_fragmentoff |
z_idata_size |
不能同时启用——zmap.c 保证二者互斥。
12. fragment 与 rolling-hash 去重是同一机制吗?
不是,是两套独立机制。最硬的区分证据在 mkfs 侧:
它们是两个独立的命令行选项 -Efragdedupe 与 -Ededupe。
13. variant-CDC 是什么?
CDC = Content-Defined Chunking(内容定义的分块)。 本质就是用 rolling hash 做变长切分。
铁证是内核提交 5c2a64252c5d 的 message:
“variable-length global compressed data deduplication with rolling hash“。
“variant-CDC” = “rolling hash” = T4 的 “Rolling-hash deduplication”, 同一机制的三种叫法。
14. INCOMPAT_DEDUPE 会被检查吗?
不会。fs/erofs/ 里没有 erofs_sb_has_dedupe() 的调用点。
(该函数由 internal.h 的 EROFS_FEATURE_FUNCS(dedupe, incompat, INCOMPAT_DEDUPE) 宏生成,
所以源码里 grep 不到完整名字 —— 宏注册存在,但从未被调用。见 6.7 节。)
运行时判据是 per-extent 的标志位
(Z_EROFS_LI_PARTIAL_REF 等),不是 superblock feature 位。
影响:镜像不设该位、而 extent 带 partial 标志时, 内核仍会走去重路径。是否成问题尚未评估。
15. metabox 的副作用?
它引入了第二个 mapping(metabox_inode->i_mapping),
使阶段 2 讲的 P0-3(跨 mapping 复用隐患)从”理论”变成”可能”。
因为 metabox 的 mapping 偏移从 0 起算,
与块设备 mapping 的低页号区间天然重叠,页号碰撞概率不为零。
16. ishare 怎么复用 inode cache?
- 建一个伪文件系统(
erofs_ishare_mnt) - 用文件指纹(某些 xattr 的哈希)作为 key
- 通过
iget5_locked()在伪 fs 里按自定义 key 查找/创建 inode - 指纹相同 → 命中同一 inode → 共享同一份 page cache
为什么不用 fscache:fscache 太重、引入 netfs 硬依赖(已在 7.2 移除)。
参考
[ATC19 论文:《EROFS: A Compression-friendly Readonly File System for Resource-scarce Devices》
OSS China 2023:《EROFS Everywhere: An Image-Based Kernel Approach for Various Use Cases》
FOSDEM 2023:《EROFS file system update and its future》
OSS North America 2024:《EROFS: Past, Present, and Future》
OSS 2019:《EROFS file system》
EROFS 官方文档 Release 0.1