18 · 特性专题:metabox(元数据压缩)
相关源码:
internal.h(erofs_inode_in_metabox()、erofs_iloc())、super.c(初始化与自检)、inode.c/xattr.c/zdata.c(使用点) 关键字段:metabox_nid、nid的EROFS_DIRENT_NID_METABOX_BIT位本文回答:元数据也能压缩吗 → metabox 是什么 → 怎么标记”在 metabox 里” → 位置计算为什么特殊 → self-loop 检测在防什么 → 关键结构体与函数。
本专题目标(读完你应该能做到什么)
- 说清 metabox 解决什么问题(元数据太多)
- 解释 metabox 与常规元数据区的关系
- 看懂 nid 的一个 bit 怎么用来标记”在 metabox 内”
- 解释
erofs_iloc()在两种情况下的计算差异 - 说清 self-loop 检测为什么必要(防止无限递归)
- 知道这个标志怎么一路传递到各个读取点
图解
一句话:文件多了元数据也很大,metabox 就是装压缩元数据的盒子—— 部分 inode 压缩存进去省空间,代价是读的时候要先解压。
自上而下四层:
| 层 | 回答什么 | 一句话 |
|---|---|---|
| ① 为什么(淡红) | 元数据也会很大 | 几十万个小文件 ⇒ 元数据可能几十上百 MB;只压数据,省空间打折扣 |
| ② 两块区域(淡绿) | 元数据存哪 | 常规元数据区(meta_blkaddr,不压缩,直接读)vs metabox(metabox_nid 指向,压缩,读时解压);放哪由 mkfs 决定 |
| ③④ 怎么标记 + 怎么定位(淡蓝 / 淡黄) | 靠什么区分、怎么算位置 | nid 的 bit 63 当标志位;erofs_iloc() 据此选两套算法——metabox 内不加 meta_blkaddr |
| ⑤⑥ 两个必须注意(红框 / 淡紫) | 坑在哪 | self-loop 必须显式禁止;标志要一路传递给每个读元数据的点 |
三个最容易错的判断:
- metabox 内不加
meta_blkaddr—— metabox 是另一块独立区域(有自己的metabox_nid与映射), 不是从meta_blkaddr开始的,加上反而算错。 - self-loop 必须禁止 —— metabox 本身也是个 inode。若它的 nid 自己带了 bit 63,
就是「metabox 在 metabox 里」⇒ 读 metabox 需要 metabox ⇒ 无限递归,挂载时直接
-EFSCORRUPTED。 - 标志不是”查一次就扔” ——
erofs_read_metabuf(&buf, sb, pos, in_metabox)的最后一个参数 要一路传下去(inode.c/xattr.c/zdata.c/data.c都要传),因为每层都可能重新定位。
动手改图:源文件 dot/37-metabox-design.dot,改完执行
cd /sdd/erofs/study && dot -Tsvg dot/37-metabox-design.dot -o dot/37-metabox-design.svg
一、特性缘由:元数据也会很大
1.1 问题:文件多 ⇒ 元数据多
EROFS 压缩的是文件数据。但一个镜像里,除了数据还有大量元数据:
- 每个文件/目录一个 inode
- 目录项(dirent)
- xattr
- 各种索引
当文件数量极大(比如一个完整发行版的 rootfs,几十万个小文件), 元数据本身就可能占几十上百 MB。
⇒ 只压数据不压元数据,省空间的效果打了折扣。
1.2 解决:把元数据也压缩起来
metabox 就是”装压缩元数据的盒子”:
常规元数据区(meta_blkaddr) ← 不压缩
metabox ← 压缩存放的元数据
某些 inode 可以放到 metabox 里,以压缩形式存储。
1.3 代价
读 metabox 里的 inode 需要先解压——比常规元数据慢一点。 所以对”冷”的、不常访问的元数据放 metabox 更划算 (具体策略由 mkfs 决定)。
二、设计理念
理念 1:用 nid 的一个 bit 做标记
怎么知道某个 inode 在不在 metabox 里?
EROFS 的做法:在 nid 里占一个位。
static inline bool erofs_inode_in_metabox(struct inode *inode)
{
return EROFS_I(inode)->nid & BIT_ULL(EROFS_DIRENT_NID_METABOX_BIT);
}
nid = [ METABOX 标志位 (1 bit) ][ 实际 nid (其余位) ]
好处:不需要额外字段——”这个 inode 在哪”的信息 跟着 nid 一起传递,任何拿到 nid 的地方都能判断。
代价:nid 可用位数少 1 位。
理念 2:位置计算分两套
erofs_iloc() 算出 inode 在磁盘上的位置:
static inline erofs_off_t erofs_iloc(struct inode *inode)
{
struct erofs_sb_info *sbi = EROFS_I_SB(inode);
erofs_nid_t nid_lo = EROFS_I(inode)->nid & EROFS_DIRENT_NID_MASK;
if (erofs_inode_in_metabox(inode))
return nid_lo << sbi->islotbits; /* ① metabox 内 */
return erofs_pos(inode->i_sb, sbi->meta_blkaddr) + /* ② 常规 */
(nid_lo << sbi->islotbits);
}
| 情况 | 计算 |
|---|---|
| 在 metabox 内 | nid_lo << islotbits —— 不加 meta_blkaddr |
| 常规 | meta_blkaddr 转字节 + (nid_lo << islotbits) |
为什么 metabox 内不加 meta_blkaddr?
因为 metabox 是另一块独立的区域(有自己的 metabox_nid 与映射),
不是从 meta_blkaddr 开始的。
加上 meta_blkaddr 反而会算错。
⇒ 这也是为什么需要理念 1 的标记位—— 不知道”在哪个区域”就没法算出正确位置。
理念 3:必须防 self-loop
super.c 初始化时:
if (erofs_sb_has_metabox(sbi)) {
ret = -EFSCORRUPTED;
if (sbi->sb_size <= offsetof(struct erofs_super_block, metabox_nid))
goto out;
sbi->metabox_nid = le64_to_cpu(dsb->metabox_nid);
if (sbi->metabox_nid & BIT_ULL(EROFS_DIRENT_NID_METABOX_BIT))
goto out; /* self-loop detection */
}
在防什么:
metabox 本身也是个 inode(由 metabox_nid 指向)。
如果 metabox 的 nid 自己带了 METABOX 标志位,
就意味着”metabox 在 metabox 里”——
⇒ 读 metabox 需要 metabox ⇒ 无限递归。
所以检测到就报错(-EFSCORRUPTED)。
这是一个很典型的”自引用必须显式禁止“的例子。
理念 4:标志一路传递
看使用点:
erofs_read_metabuf(&buf, sb, pos, erofs_inode_in_metabox(inode));
最后那个参数就是”是否在 metabox”标志。 每个读元数据的地方都要传它—— 因为底层需要知道该用哪套位置计算。
出现位置:
inode.c(读 inode)xattr.c(读 xattr 头部)zdata.c(压缩路径读元数据)data.c(读映射索引)
三、实现架构
3.1 对象关系
erofs_sb_info
├─ meta_blkaddr ← 常规元数据区起点
├─ metabox_nid ← metabox 这个 inode 的 nid
└─ metabox_inode ← 按 metabox_nid igot 出来的 inode
每个 inode 的 nid
└─ 若 METABOX 位为 1 → 该 inode 在 metabox 内(压缩存放)
3.2 读取流程(对比)
读一个 inode:
│
├─ erofs_inode_in_metabox(inode)?
│
┌────┴────┐
▼ ▼
是 否
│ │
│ └─ erofs_iloc() = meta_blkaddr + nid_lo << islotbits
│ └─ 直接从元数据区读(未压缩)
│
└─ erofs_iloc() = nid_lo << islotbits(不加 meta_blkaddr)
└─ 通过 metabox_inode 的映射读(压缩,需解压)
3.3 标志如何在调用链上传播
erofs_iget() / 读 inode
└─ erofs_iloc() 判断并算位置
│
└─ erofs_read_metabuf(&buf, sb, pos, in_metabox)
↑
标志一路传下去
⇒ 设计要点:这个标志不是”查一次就扔”, 而是伴随整个元数据读取过程,因为每一层都可能要重新定位。
四、关键结构体与字段
4.1 erofs_sb_info(internal.h)
u32 meta_blkaddr; /* 常规元数据区起始块 */
erofs_nid_t metabox_nid; /* metabox 的 nid */
struct inode *metabox_inode; /* metabox 对应的 inode */
4.2 erofs_inode
erofs_nid_t nid; /* ★ 最高位(METABOX_BIT)表示在 metabox 内 */
具体是 bit 63(EROFS_DIRENT_NID_METABOX_BIT,见 4.3)。nid 是 u64,
所以实际布局是:
bit 63 62 0
┌──┬──────────────────────────────────────┐
│ M│ 实际 nid │
└──┴──────────────────────────────────────┘
↑
METABOX 标志位(1 = 该 inode 在 metabox 内,压缩存放)
M = 0 → 在常规元数据区(meta_blkaddr),不压缩
M = 1 → 在 metabox 内,压缩存放,读时需解压
⇒ 可用 nid 只有 63 位(最高位被借走了)。
4.3 相关常量
两个常量都定义在 erofs_fs.h(不是 internal.h),因为它们属于 on-disk 格式:
/* fs/erofs/erofs_fs.h:277 */
#define EROFS_DIRENT_NID_METABOX_BIT 63
#define EROFS_DIRENT_NID_MASK (BIT_ULL(EROFS_DIRENT_NID_METABOX_BIT) - 1)
| 常量 | 值 | 含义 |
|---|---|---|
EROFS_DIRENT_NID_METABOX_BIT |
63 | nid 中标记 metabox 的位号(即 u64 的最高位) |
EROFS_DIRENT_NID_MASK |
BIT_ULL(63) - 1= 0x7FFF_FFFF_FFFF_FFFF |
取 nid 的有效部分(低 63 位全 1,去掉标志位) |
用法:
nid & BIT_ULL(EROFS_DIRENT_NID_METABOX_BIT) /* 是否 metabox(取 bit 63)*/
nid & EROFS_DIRENT_NID_MASK /* 实际 nid(低 63 位)*/
⚠️ 注意 bit 63 这个取值带来的连锁影响:
因为标志位在最高位,
nid直接当 inode 号用会产生过大的数字 (带 bit 63 的 nid 会是一个接近 2⁶⁴ 的数)。 所以还需要erofs_nid_to_ino64()做移位映射(见 5.3):return ((nid << 1) & GENMASK_ULL(63, 32)) | (nid & GENMASK(30, 0)) | ((nid >> EROFS_DIRENT_NID_METABOX_BIT) << 31);即把标志位挪到 bit 31,避免 inode 号膨胀。 注释里也写了”on-disk NIDs remain unchanged”, 保证对非 LFS 的 32 位应用仍然兼容。
五、主要函数
metabox 的内核改动很小,全部集中在三个 inline 函数 + 一处 buf 初始化上。 按调用频率从高到低讲。
5.1 erofs_inode_in_metabox()(internal.h)
static inline bool erofs_inode_in_metabox(struct inode *inode)
{
return EROFS_I(inode)->nid & BIT_ULL(EROFS_DIRENT_NID_METABOX_BIT);
}
配套的常量在 erofs_fs.h:
#define EROFS_DIRENT_NID_METABOX_BIT 63
#define EROFS_DIRENT_NID_MASK (BIT_ULL(EROFS_DIRENT_NID_METABOX_BIT) - 1)
⇒ 判断的就是 nid 的第 63 位(最高位)。EROFS_DIRENT_NID_MASK 是低 63 位的掩码,
用来把标志位剔掉、取回真正的 nid 数值(下一节的 erofs_iloc() 就用它)。
这是全专题最高频的一个判断——每次读元数据都要问一次, 因为答案决定了”这次要用哪一套地址空间去读”(见 5.4)。
5.2 erofs_iloc()(internal.h)—— 两套位置计算
这是 metabox 最核心的一段:同一个 inode,位置怎么算,取决于它在不在 metabox 里。
static inline erofs_off_t erofs_iloc(struct inode *inode)
{
struct erofs_sb_info *sbi = EROFS_I_SB(inode);
erofs_nid_t nid_lo = EROFS_I(inode)->nid & EROFS_DIRENT_NID_MASK;
if (erofs_inode_in_metabox(inode))
return nid_lo << sbi->islotbits;
return erofs_pos(inode->i_sb, sbi->meta_blkaddr) +
(nid_lo << sbi->islotbits);
}
拆开看:
nid_lo = nid & EROFS_DIRENT_NID_MASK—— 先去掉第 63 位的标志,得到纯 nidnid_lo << sbi->islotbits—— nid 乘以 inode slot 大小(islotbits通常为 5,即 32 字节/inode), 得到相对于某个起点的偏移- 关键在”加上什么作为起点”:
| 情况 | 返回值 | 含义 |
|---|---|---|
| 在 metabox 内 | nid_lo << islotbits |
偏移是相对 metabox 自己的,不加 meta_blkaddr |
| 普通 inode | erofs_pos(sb, meta_blkaddr) + (nid_lo << islotbits) |
偏移是相对整个元数据区起点的 |
erofs_pos() 就是 internal.h 里那个块→字节的换算宏(blk << s_blocksize_bits)。
⇒ 一句话记住:metabox 内的 inode 用的是”箱内相对地址”,所以不要再加 meta_blkaddr。
这也是”误解 3”要纠正的点。
5.3 erofs_nid_to_ino64()(internal.h)—— inode 号的映射
static inline u64 erofs_nid_to_ino64(struct erofs_sb_info *sbi, erofs_nid_t nid)
{
if (!erofs_sb_has_metabox(sbi))
return nid;
/*
* When metadata compression is enabled, avoid generating excessively
* large inode numbers for metadata-compressed inodes. Shift NIDs in
* the 31-62 bit range left by one and move the metabox flag to bit 31.
*
* Note: on-disk NIDs remain unchanged as they are primarily used for
* compatibility with non-LFS 32-bit applications.
*/
return ((nid << 1) & GENMASK_ULL(63, 32)) | (nid & GENMASK(30, 0)) |
((nid >> EROFS_DIRENT_NID_METABOX_BIT) << 31);
}
为什么要多这一步?
不启用 metabox 时,nid 直接当 inode 号用。启用后 nid 的最高位(bit 63)被借去当标志位, 若还直接当 inode 号,数值就会大到 2^63 量级——超过很多用户态程序能处理的范围 (尤其是没开 LFS 的 32 位应用)。
所以这个函数做一次重排位:
(nid << 1) & GENMASK_ULL(63, 32)—— 把 31~62 这段整体左移一位,腾出 bit 31(nid & GENMASK(30, 0))—— 低 31 位(0~30)原样保留(保证小 nid 的 inode 号不变,兼容 32 位应用)((nid >> EROFS_DIRENT_NID_METABOX_BIT) << 31)—— 把原来 bit 63 的标志挪到 bit 31
⇒ 注释里那句很重要:on-disk 的 nid 没变——这只影响内核给 VFS 看的 i_ino,
磁盘格式完全不动。
⇒ 这也说明 metabox 的影响不止在读路径,还波及 inode 号的生成。
5.4 erofs_init_metabuf() / erofs_read_metabuf()(data.c)—— 决定”从哪读”
这是把 5.1 的判断落到实处的那一层:元数据到底从哪个 address_space 读。
int erofs_init_metabuf(struct erofs_buf *buf, struct super_block *sb,
bool in_metabox)
{
struct erofs_sb_info *sbi = EROFS_SB(sb);
buf->mc = false;
if (in_metabox) {
if (unlikely(!sbi->metabox_inode))
return -EFSCORRUPTED;
buf->mapping = sbi->metabox_inode->i_mapping;
return 0;
}
if (erofs_is_fileio_mode(sbi)) {
buf->mapping = sbi->managed_cache->i_mapping;
buf->mc = true;
} else {
buf->off = sbi->dif0.fsoff;
buf->mapping = sb->s_bdev->bd_mapping;
}
return 0;
}
void *erofs_read_metabuf(struct erofs_buf *buf, struct super_block *sb,
erofs_off_t offset, bool in_metabox)
{
int err;
err = erofs_init_metabuf(buf, sb, in_metabox);
if (err)
return ERR_PTR(err);
return erofs_bread(buf, offset, true);
}
三条路,对应三种元数据来源:
| 分支 | buf->mapping | 说明 |
|—|—|—|
| metabox | sbi->metabox_inode->i_mapping | 走 metabox 那个”假 inode”的地址空间。
读它时会触发解压——这正是”元数据压缩”的实现方式:
元数据被当成一份压缩文件存着,读的时候按需解压
| fileio 模式 | sbi->managed_cache->i_mapping,mc = true | 镜像是文件后端时,走 managed cache
| 普通块设备 | sb->s_bdev->bd_mapping,off = sbi->dif0.fsoff | 直接读块设备,off 记上 fsoffset
两个容易忽略的点:
buf->off只在非 metabox 分支被设置(=dif0.fsoff)。 metabox 分支直接返回,buf->off保持__EROFS_BUF_INITIALIZER给的 0 ⇒ 又一次体现”metabox 内是另一套坐标系”sbi->metabox_inode为空时返回-EFSCORRUPTED(镜像坏了)。 因为 nid 的标志位说”我在 metabox 里”,但 superblock 根本没建 metabox inode——自相矛盾
5.5 使用点(标志一路传下去)
| 位置 | 用途 |
|---|---|
inode.c |
读 inode 本体:bool in_mbox = erofs_inode_in_metabox(inode), |
传给 erofs_read_metabuf();同时用 erofs_iloc(inode) 算 blkaddr/of |
|
xattr.c |
读 xattr 头部与 shared xattr,erofs_iloc(inode) + vi->inode_isize 后同样要传标志 |
zdata.c |
压缩路径读元数据(把 erofs_inode_in_metabox(fe->inode) 传给 metabuf) |
data.c |
读映射索引;erofs_init_metabuf() / erofs_read_metabuf() 就定义在这里 |
zmap.c |
把 erofs_inode_in_metabox(inode) 存进 m.in_mbox,随映射结果往下传 |
fileio.c |
文件后端路径读元数据时也要带上标志 |
⇒ 可以看到同一个模式:先 erofs_iloc() 算位置,再把 erofs_inode_in_metabox() 的结果
当作参数传给 erofs_read_metabuf()。两个函数几乎总是成对出现。
六、来龙去脉:完整串一遍
① mkfs 阶段
├ 把一部分 inode(通常是"冷"的)压缩
├ 存进 metabox
├ 这些 inode 的 nid 打上 METABOX 标志位
└ 把 metabox 自身的 nid 写进 superblock(不带标志位!)
│
② 挂载
├ erofs_sb_has_metabox()?
├ 读 metabox_nid
├ ★ self-loop 检测:metabox_nid 带标志位 → -EFSCORRUPTED
└ igot 出 metabox_inode
│
③ 读某个文件
└ erofs_iget(sb, nid)
│
④ erofs_inode_in_metabox(inode)?
├ 否 → 位置 = meta_blkaddr + nid_lo << islotbits,直接读
└ 是 → 位置 = nid_lo << islotbits,走 metabox_inode 的映射(解压)
│
⑤ erofs_read_metabuf(..., in_metabox) ← 标志传下去
│
⑥ 拿到 inode 数据
七、动手验证
验证 1:确认特性定义与自检
cd /sdd/linux/linux-stable/fs/erofs
grep -rn "metabox" .
重点看:
internal.h的erofs_inode_in_metabox()与erofs_iloc()super.c里的 self-loop 检测那几行
验证 2:确认两套位置计算的差异
cd /sdd/linux/linux-stable/fs/erofs
grep -n -A8 "static inline erofs_off_t erofs_iloc" internal.h
对照 if (erofs_inode_in_metabox(inode)) 分支与 else 分支,
确认一个加 meta_blkaddr、一个不加。
验证 3:看标志位怎么传递
grep -rn "erofs_inode_in_metabox" /sdd/linux/linux-stable/fs/erofs/*.c
看它出现在哪些 erofs_read_metabuf() 调用里——
体会”标志伴随整个读取过程”这一点。
验证 4:确认镜像是否启用
/opt/erofs-utils/bin/dump.erofs -s /tmp/erofs-lab/comp2.erofs
看 superblock 的 feature 位 / metabox 相关信息。
八、常见误解(重要)
误解 1:metabox 是”另一种元数据区”,只是位置不同
不只是位置不同——metabox 里的元数据是压缩存放的, 读取需要解压。所以它同时影响:
- 位置计算(
erofs_iloc) - 读取方式(要解压)
- inode 号生成(
erofs_nid_to_ino64)
误解 2:nid 就是 inode 编号,不含其他信息
启用 metabox 时,nid 最高位被借去当标志位了。
用 nid 前要注意是否该 & EROFS_DIRENT_NID_MASK。
误解 3:metabox 内的位置也要加 meta_blkaddr
不要。metabox 是独立区域,
erofs_iloc() 里 metabox 分支故意不加 meta_blkaddr。
加了反而算错。
误解 4:self-loop 检测是多余的检查
不是。如果 metabox_nid 自己带 METABOX 标志位,
意味着”读 metabox 要先读 metabox”——无限递归。
必须显式拦住。
误解 5:metabox 只影响读 inode
不止。它还影响 inode 号的生成(erofs_nid_to_ino64 要移位),
以及每个读元数据的调用点(都要传标志)。
九、与其他特性的关系
| 特性 | 关系 |
|---|---|
| xattr(12 专题) | shared xattr 也可放 metabox(erofs_sb_has_shared_ea_in_metabox()) |
| 压缩(04) | 压缩路径读元数据也要判断 metabox(zdata.c) |
| on-disk 格式(01) | nid / meta_blkaddr 概念的延伸 |
| ishare(11) | 无关——metabox 管元数据存放,ishare 管页缓存共享 |
自测检查点
- metabox 解决什么问题?
- 怎么标记一个 inode “在 metabox 内”?代价是什么?
erofs_iloc()在 metabox 内外的计算有什么不同?为什么?- self-loop 检测在防什么?不检测会怎样?
EROSF_DIRENT_NID_MASK的作用?- 这个标志在调用链上是怎么传播的?举两个使用点。
- metabox 里的元数据是压缩的吗?
erofs_nid_to_ino64()为什么要在启用 metabox 时特殊处理?metabox_nid自己能不能带 METABOX 标志位?- metabox 除了读路径,还影响了什么?
自测答案
点击展开
1. 元数据(inode / dirent / xattr / 索引)**本身也很大**, 只压数据不压元数据,省空间效果打折。metabox 用来**压缩存放元数据**。 2. 用 **nid 的一个 bit**(`EROFS_DIRENT_NID_METABOX_BIT`)。 代价:nid 可用位数少 1 位。 3. **metabox 内**:`nid_lo << islotbits`,**不加 `meta_blkaddr`**; **常规**:`erofs_pos(sb, meta_blkaddr) + (nid_lo << islotbits)`。 因为 metabox 是独立区域,不是从 `meta_blkaddr` 开始的,加上反而算错。 4. 防"**metabox 在 metabox 里**"。 那样读 metabox 需要先读 metabox ⇒ **无限递归**。 不检测会导致启动即死循环/栈溢出。 5. 取 nid 的**有效部分**(屏蔽掉 METABOX 标志位), 得到真正的 nid 用于位置计算。 6. 作为**参数传给每次元数据读取**。例如 `erofs_read_metabuf(&buf, sb, pos, erofs_inode_in_metabox(inode))`—— 出现在 `inode.c`、`xattr.c`、`zdata.c`、`data.c` 的读取点。 7. **是**。metabox 里的元数据以压缩形式存放,读取需要解压 (这也是为什么它适合放"冷"元数据)。 8. 因为启用 metabox 后 nid 多占了一个标志位, 直接当 inode 号会产生**过大的数字**。 该函数做移位/映射避免 inode 号膨胀。 9. **不能**。若带了该位,说明 metabox 自引用, `super.c` 会检测并报错 `-EFSCORRUPTED`。 10. 还影响 **inode 号的生成**(`erofs_nid_to_ino64()` 需特殊处理), 以及**每个读元数据的调用点**(都要传标志)。参考
linux-stable
EROFS 官方文档 Release 0.1