EROFS advanced features

EROFS 高级特性

发布于 2026年9月11日

阶段 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 完全不走,直接映射持久内存

本阶段目标

读完这一章,你应该能够:

  1. 说出本章每个特性解决什么问题、挂在哪个模块上
  2. 解释 xattr 的 inline / shared 两种形式
  3. 说清多设备寻址的完整链路,并解释为什么”镜像分层(layering)”不是内核概念
  4. 区分 fileio 与块设备后端,知道 fscache 后端已被移除
  5. 说清 FSDAX 在 EROFS 里的作用与”设备粒度”的真相
  6. 区分 fragment 与 rolling-hash 去重(最容易混淆的一对)
  7. 解释 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 混淆)

fragment vs 去重

① 解决什么问题

不同文件(或同一文件的不同位置)可能含有完全相同的内容。 检测到重复后,只存一份,多处引用。

② 磁盘上怎么表示

索引项上的标志位 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);

两个连带影响(前面阶段提过,这里串起来):

  1. erofs_iloc() 退化:metabox 里的 inode 位置 = nid << islotbits (不加 meta_blkaddr,internal.h)
  2. 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

自测检查点

  1. xattr 有哪两种形式?shared 形式为什么能省空间?
  2. 读 xattr 代码时要特别留意什么?(提示:阶段 2 的契约)
  3. 多设备的设备信息存在什么数据结构里?按什么索引?
  4. m_deviceid == 0 时用什么设备?flatdev 模式怎么处理?
  5. 为什么说”内核支持镜像分层”这个说法是错的?给出实证方法。
  6. 多输入镜像(rebuild)与 --blobdev 有什么区别?
  7. fileio 后端借用了谁的 page cache?为什么?
  8. EROFS 早期的 fscache 后端现在还在吗?
  9. FSDAX 的 dax_dev 是全局的还是每设备的?T4 讲的”layer 粒度”真相是什么?
  10. 48-bit 地址特性的现状如何?内核怎么提示用户的?
  11. fragment 与 ztailpacking 的区别?它们能同时启用吗?
  12. fragment 与 rolling-hash 去重是同一机制吗?最硬的区分证据是什么?
  13. 演讲里的 “variant-CDC” 是什么?为什么和 rolling hash 是一回事?
  14. INCOMPAT_DEDUPE 这个 feature 位内核会检查吗?有什么影响?
  15. metabox 的引入带来了什么副作用?
  16. 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