EROFS dev(设备)专题:从 mount 初始化到文件访问
基线:
/home/linux/erofs/linux-stable(Linux 7.3-rc5),fs/erofs/统一假设:bs = 4096;代码引用格式为「文件:行号」。本文覆盖你点名的四个东西:
sb->s_bdev·devs->flatdev·map->m_bdev·devs->extra_devices, 以及它们背后的struct erofs_dev_context/erofs_device_info/erofs_map_dev。
0. 一句话速览
| 字段 | 归属 | 是什么 | 何时确定 |
|---|---|---|---|
sb->s_bdev |
VFS super_block |
主设备的块设备(mknod 打开的 bdev) | mount 时由 VFS get_tree_bdev_flags() 设置 |
devs->extra_devices |
sbi->devs(erofs_dev_context) |
额外设备的个数(不含主设备) | ① -o device= 时逐个 +1;② 无 -o device= 时按镜像声明的 dsb->extra_devices 循环创建 |
devs->flatdev |
同上 | “扁平设备”标志:镜像声明了额外设备,但挂载时一个都没给 | erofs_scan_devices():镜像有 extra_devices 而 devs->extra_devices == 0 ⇒ flatdev = true |
map->m_bdev |
erofs_map_dev |
解析后”这一块数据实际所在的”块设备 | 每次访问经 erofs_map_dev() 解析得出;主设备时 = sb->s_bdev |
一句话总结它们的分工:
sb->s_bdev = 主设备(唯一由 VFS 直接持有)
devs->extra_devices = 有几个"额外设备"
devs->flatdev = 额外设备是否"扁平拼接在主设备之后"(不需要真的打开别的设备)
map->m_bdev = 本次访问落到哪个块设备(= sb->s_bdev 或某个额外设备的 bdev)
1. 数据结构全景
1.1 struct erofs_dev_context(internal.h:66)—— 挂在 sbi->devs
struct erofs_dev_context {
struct idr tree; /* ★device_id → struct erofs_device_info* */
struct rw_semaphore rwsem; /* 保护 tree */
unsigned int extra_devices; /* ★额外设备个数 */
bool flatdev; /* ★扁平设备标志 */
};
tree是 idr:key =device_id - 1(所以device_id=1存在 key 0)。rwsem保护tree的并发访问(读路径 down_read,-o device=解析时 down_write)。
1.2 struct erofs_device_info(internal.h:44)—— 描述”一个设备”
struct erofs_device_info {
char *path; /* 设备/文件路径(来自 -o device= 或 device table 的 tag) */
struct file *file; /* 打开后的文件对象(bdev 或普通文件) */
struct dax_device *dax_dev; /* DAX 设备 */
u64 fsoff, dax_part_off; /* 文件系统在这台设备上的字节偏移 / DAX 分区偏移 */
erofs_blk_t blocks; /* 这台设备有多少块 */
erofs_blk_t uniaddr; /* ★这台设备在「统一地址空间」里的起始块号 */
};
主设备也用这个结构表示:sbi->dif0(EROFS_SB(sb)->dif0)。
1.3 struct erofs_map_dev(internal.h:371)—— 一次映射的”落点”
struct erofs_map_dev {
struct super_block *m_sb;
struct erofs_device_info *m_dif; /* 落在哪个 device_info */
struct block_device *m_bdev; /* ★落在哪个块设备 */
erofs_off_t m_pa; /* 设备内的物理偏移 */
unsigned int m_deviceid; /* 设备号(0 = 主设备) */
};
1.4 关系图
VFS
sb->s_bdev ──────────────┐ (主块设备,mount 时由 VFS 设置)
│
sbi->dif0 ←─────────────┘ (主设备的 device_info:fsoff / dax_dev / blocks)
│
sbi->devs ── extra_devices = N
│ ── flatdev = ?
└── tree (idr)
├── key 0 → dif (device_id=1) → file/bdev/dax_dev + blocks + uniaddr
├── key 1 → dif (device_id=2)
└── ...
一次访问:
erofs_map_blocks() → map.m_pa / map.m_deviceid
│
▼
erofs_map_dev() ── 填 mdev.m_dif / mdev.m_bdev / mdev.m_pa
│
▼
iomap->bdev = mdev.m_bdev (非压缩 / DAX 走 dax_dev)
bio_alloc(mdev.m_bdev, ...) (压缩)
rq->iocb.ki_filp = mdev->m_dif->file (fileio 文件后端)
2. mount 初始化:每一步
2.1 sb->s_bdev 从哪来
/* super.c:797 erofs_fc_get_tree() */
ret = get_tree_bdev_flags(fc, erofs_fc_fill_super, ...);
get_tree_bdev_flags() 是 VFS 提供的通用实现:解析 fc->source(如 /dev/vdb)→ 打开块设备 →
设置 sb->s_bdev → 再回调 erofs_fc_fill_super()。
⇒ EROFS 自己不打开主设备,主设备由 VFS 搞定,sb->s_bdev 就是它。
(file-backed 挂载时 sb->s_bdev == NULL,走另一条路,见 §2.4。)
2.2 sbi->devs 的创建
/* super.c:922 erofs_fc_fill_super() */
sbi->devs = kzalloc_obj(struct erofs_dev_context);
if (!sbi->devs) { ... }
fc->s_fs_info = sbi;
idr_init(&sbi->devs->tree); /* super.c:929 */
init_rwsem(&sbi->devs->rwsem); /* super.c:930 */
此时 extra_devices = 0、flatdev = false(kzalloc 清零)。
2.3 解析 -o device=(只记路径,不打开)
/* super.c:519 case Opt_device: */
dif = kzalloc_obj(*dif);
dif->path = kstrdup(param->string, GFP_KERNEL); /* ★只保存路径字符串 */
down_write(&sbi->devs->rwsem);
ret = idr_alloc(&sbi->devs->tree, dif, 0, 0, GFP_KERNEL); /* 分配 id(key) */
up_write(&sbi->devs->rwsem);
...
++sbi->devs->extra_devices; /* ★计数 +1 */
关键:这一步不打开设备,只是把路径按”出现顺序”放进 idr。
所以 -o device=A,device=B ⇒ A 是 device_id=1、B 是 device_id=2(顺序必须对应镜像里的 device table)。
2.4 dif0(主设备的 device_info)填什么
/* super.c:302 erofs_read_superblock() */
sbi->dif0.blocks = le32_to_cpu(dsb->blocks_lo);
/* 48bit 时补高位:super.c:322 */
sbi->dif0.blocks |= ((u64)le16_to_cpu(dsb->rb.blocks_hi) << 32);
sbi->packed_nid = le64_to_cpu(dsb->packed_nid);
/* super.c:710 erofs_fc_fill_super() —— 块设备挂载分支 */
sbi->dif0.dax_dev = fs_dax_get_by_bdev(sb->s_bdev,
&sbi->dif0.dax_part_off, NULL, NULL);
⇒ dif0 没有 file/path:主设备由 sb->s_bdev 直接表达。
(file-backed 挂载时反过来:sbi->dif0.file 由 fc->source 打开,sb->s_bdev == NULL。)
2.5 erofs_scan_devices():读 device table(super.c:183)
sbi->total_blocks = sbi->dif0.blocks; /* 先算主设备的块数 */
ondisk_extradevs = erofs_sb_has_device_table(sbi) ? le16_to_cpu(dsb->extra_devices) : 0;
/* ① 数量必须一致(super.c:199) */
if (sbi->devs->extra_devices && ondisk_extradevs != sbi->devs->extra_devices) {
erofs_err(sb, "extra devices don't match (ondisk %u, given %u)", ...);
return -EINVAL;
}
if (!ondisk_extradevs) return 0; /* 没有额外设备,直接返回 */
/* ② flatdev 的判定(super.c:213) */
if (!sbi->devs->extra_devices)
sbi->devs->flatdev = true; /* ★声明了但没给 ⇒ 扁平 */
/* ③ device_id 掩码(super.c:216) */
sbi->device_id_mask = roundup_pow_of_two(ondisk_extradevs + 1) - 1;
/* ④ 逐个初始化 */
pos = le16_to_cpu(dsb->devt_slotoff) * EROFS_DEVT_SLOT_SIZE; /* device table 起点 */
if (sbi->devs->extra_devices) {
/* 给了 -o device= ⇒ 遍历已有的(顺序即 device_id) */
idr_for_each_entry(&sbi->devs->tree, dif, id)
erofs_init_device(&buf, sb, dif, &pos);
} else {
/* 没给 ⇒ 按 slot 顺序创建(flatdev 场景) */
for (id = 0; id < ondisk_extradevs; id++) {
dif = kzalloc_obj(*dif);
idr_alloc(&sbi->devs->tree, dif, 0, 0, GFP_KERNEL);
++sbi->devs->extra_devices;
erofs_init_device(&buf, sb, dif, &pos);
}
}
flatdev 的三态含义
镜像 extra_devices |
挂载给 -o device= |
结果 |
|---|---|---|
| 0 | 无 | 单设备,flatdev=false |
| >0 | 给了 N 个(N 必须相等) | 真多设备,flatdev=false |
| >0 | 一个都没给 | flatdev=true:假设所有设备在主设备之后连续拼接,不打开任何新设备 |
2.6 erofs_init_device():打开设备 / 填 blocks uniaddr(super.c:125)
dis = erofs_read_metabuf(buf, sb, *pos, false); /* 读一个 erofs_deviceslot(128 B) */
/* ① 路径:没给 -o device= 时从 tag 里取 */
if (!sbi->devs->flatdev && !dif->path) {
if (!dis->tag[0]) { erofs_err("empty device tag"); return -EINVAL; }
dif->path = kmemdup_nul(dis->tag, sizeof(dis->tag), GFP_KERNEL);
}
/* ② 打开设备:★flatdev 时跳过(不打开任何东西) */
if (!sbi->devs->flatdev) {
file = erofs_is_fileio_mode(sbi) ?
filp_open(dif->path, O_RDONLY|O_LARGEFILE, 0) : /* 文件后端 */
fs_bdev_file_open_by_path(dif->path, BLK_OPEN_READ, sb->s_type, sb);
if (IS_ERR(file)) { ... }
if (!erofs_is_fileio_mode(sbi))
dif->dax_dev = fs_dax_get_by_bdev(file_bdev(file), &dif->dax_part_off, NULL, NULL);
else if (!S_ISREG(file_inode(file)->i_mode)) { fput(file); return -EINVAL; }
if (!dif->dax_dev && test_opt(&sbi->opt, DAX_ALWAYS)) { ...关闭 DAX... }
dif->file = file;
}
/* ③ 无条件填:块数与统一起始地址(48bit 时拼高位) */
_48bit = erofs_sb_has_48bit(sbi);
dif->blocks = le32_to_cpu(dis->blocks_lo) | (_48bit ? (u64)le16_to_cpu(dis->blocks_hi) << 32 : 0);
dif->uniaddr = le32_to_cpu(dis->uniaddr_lo) | (_48bit ? (u64)le16_to_cpu(dis->uniaddr_hi) << 32 : 0);
sbi->total_blocks += dif->blocks;
*pos += EROFS_DEVT_SLOT_SIZE; /* 下一个 slot */
⇒ 即使是 flatdev,blocks / uniaddr 也会被填上——因为扁平模式要靠 uniaddr 做地址平移。
3. 文件访问:dev 怎么被用
3.1 元数据读取:永远走主设备(或 managed_cache / metabox)
/* data.c:55 erofs_init_metabuf() */
if (in_metabox) {
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; /* ★普通块设备:主设备的 mapping */
}
⇒ 元数据永远从主设备读(sb->s_bdev),与 map->m_bdev 无关——
元数据(inode / dirent / xattr / 各类索引)不会被分到额外设备。
3.2 erofs_map_dev():统一地址 → 具体设备(data.c:197)
int erofs_map_dev(struct super_block *sb, struct erofs_map_dev *map)
{
struct erofs_dev_context *devs = EROFS_SB(sb)->devs;
...
/* ① 默认填主设备 */
erofs_fill_from_devinfo(map, sb, &EROFS_SB(sb)->dif0);
map->m_bdev = sb->s_bdev; /* ★use s_bdev for the primary device */
if (map->m_deviceid) { /* ② 非主设备 */
down_read(&devs->rwsem);
dif = idr_find(&devs->tree, map->m_deviceid - 1); /* device_id → dif */
if (!dif) { up_read; return -ENODEV; }
if (devs->flatdev) { /* ★扁平:只平移地址 */
map->m_pa += erofs_pos(sb, dif->uniaddr);
up_read(&devs->rwsem);
return 0; /* 不换 dif / bdev */
}
erofs_fill_from_devinfo(map, sb, dif); /* 切到该设备 */
up_read(&devs->rwsem);
} else if (devs->extra_devices && !devs->flatdev) { /* ③ 按区间反查设备 */
down_read(&devs->rwsem);
idr_for_each_entry(&devs->tree, dif, id) {
if (!dif->uniaddr) continue;
startoff = erofs_pos(sb, dif->uniaddr);
if (map->m_pa >= startoff &&
map->m_pa < startoff + erofs_pos(sb, dif->blocks)) {
map->m_pa -= startoff; /* → 设备内偏移 */
erofs_fill_from_devinfo(map, sb, dif);
break;
}
}
up_read(&devs->rwsem);
}
return 0;
}
erofs_fill_from_devinfo()(data.c:187,本文最关键的一小段):
static void erofs_fill_from_devinfo(struct erofs_map_dev *map,
struct super_block *sb, struct erofs_device_info *dif)
{
map->m_sb = sb;
map->m_dif = dif;
map->m_bdev = NULL;
if (dif->file && S_ISBLK(file_inode(dif->file)->i_mode))
map->m_bdev = file_bdev(dif->file); /* 额外设备:从 file 取 bdev */
}
⇒ map->m_bdev 的两个来源:
- 主设备:
map->m_bdev = sb->s_bdev(data.c:207显式赋值,覆盖fill_from_devinfo里的 NULL) - 额外设备:
fill_from_devinfo里file_bdev(dif->file)(前提是该设备的 file 是块设备)
3.3 非压缩数据读:iomap 用 m_bdev
/* data.c:315 erofs_iomap_begin() —— 仅数据(非 EROFS_MAP_META) */
mdev = (struct erofs_map_dev) { .m_deviceid = map.m_deviceid, .m_pa = map.m_pa };
ret = erofs_map_dev(sb, &mdev);
if (ret) return ret;
if (flags & IOMAP_DAX)
iomap->dax_dev = mdev.m_dif->dax_dev; /* DAX:用 dax_dev */
else
iomap->bdev = mdev.m_bdev; /* ★普通:用 m_bdev */
iomap->addr = mdev.m_dif->fsoff + mdev.m_pa; /* 设备内偏移(含 fsoff 偏移) */
if (flags & IOMAP_DAX)
iomap->addr += mdev.m_dif->dax_part_off;
注意 iomap->addr = fsoff + m_pa:支持”文件系统不是从设备 0 开始”(-o fsoffset= / 分区偏移)。
3.4 压缩数据读:bio_alloc(mdev.m_bdev)
/* zdata.c:1685 z_erofs_submit_bio / 提交循环 */
(void)erofs_map_dev(sb, &mdev);
cur = mdev.m_pa;
...
/* ① bio 需要拆分的两个条件:物理不连续 或 设备变了 */
if (bio && (cur != last_pa || bio->bi_bdev != mdev.m_bdev)) {
if (erofs_is_fileio_mode(EROFS_SB(sb))) erofs_fileio_submit_bio(bio);
else submit_bio(bio);
bio = NULL;
}
...
/* ② 分配 bio 时绑定设备 */
if (!bio) {
if (erofs_is_fileio_mode(EROFS_SB(sb)))
bio = erofs_fileio_bio_alloc(&mdev);
else
bio = bio_alloc(mdev.m_bdev, BIO_MAX_VECS, REQ_OP_READ, GFP_NOIO); /* ★ */
bio->bi_end_io = z_erofs_endio;
bio->bi_iter.bi_sector = (mdev.m_dif->fsoff + cur) >> 9; /* 扇区 = 字节/512 */
...
}
⇒ bi_bdev != mdev.m_bdev 这个判断很关键:同一个 bio 不能跨设备,
遇到不同设备(或地址不连续)就必须先提交、再开新 bio。
3.5 fileio(文件后端):用 m_dif->file,没有 bdev
/* fileio.c:66 erofs_fileio_rq_alloc() */
bio_init(&rq->bio, NULL, rq->bvecs, ARRAY_SIZE(rq->bvecs), REQ_OP_READ); /* bdev=NULL */
rq->iocb.ki_filp = mdev->m_dif->file; /* ★用文件指针做 VFS 读 */
rq->sb = mdev->m_sb;
/* fileio.c:212 —— 计算文件内的字节偏移 */
(io.dev.m_dif->fsoff + io.dev.m_pa) >> 9;
⇒ fileio 模式下 map->m_bdev 是 NULL(因为 fill_from_devinfo 只在 S_ISBLK 时设 bdev),
数据走 vfs_iocb_iter_read()。
3.6 chunk-based 文件:m_deviceid 从哪来
chunk 索引项把 device_id 编在块地址的 bit48 以上(见 erofs_map_chunks 专题),
erofs_map_dev() 收到 m_deviceid 后按 §3.2 的第 ② 支路处理:
chunk index 里的 device_id ──► map->m_deviceid ──► erofs_map_dev()
├─ flatdev → m_pa += uniaddr(不换设备)
└─ 否则 → idr_find → 换 m_dif / m_bdev
⇒ 这是”数据真正落在另一台设备上”的唯一入口。
4. 三种典型场景的完整链路
4.1 单设备(无 device table)
mount /dev/vdb
→ get_tree_bdev_flags() 设 sb->s_bdev = /dev/vdb
→ devs->extra_devices = 0, flatdev = false
→ erofs_scan_devices(): ondisk_extradevs = 0 ⇒ 直接 return 0(不建 idr 项)
读文件
→ erofs_map_blocks(): m_deviceid = 0, m_pa = 块地址
→ erofs_map_dev(): fill_from_devinfo(dif0) + m_bdev = sb->s_bdev;m_deviceid=0 且 extra_devices=0 ⇒ 不动
→ iomap->bdev = sb->s_bdev;addr = dif0.fsoff + m_pa
4.2 flatdev(镜像声明了额外设备,但挂载时没给)
mkfs --blobdev=... (镜像 dsb->extra_devices = 1)
mount /dev/vdb ← 忘了 -o device=
→ erofs_scan_devices(): ondisk=1, devs->extra_devices=0 ⇒ ★flatdev = true
→ device_id_mask = roundup_pow_of_two(1+1)-1 = 1
→ 走 else 分支:为每个 slot 建 dif(填 blocks/uniaddr),但 erofs_init_device() 里
「!flatdev」不成立 ⇒ 不打开任何设备、dif->file = NULL
→ devs->extra_devices 变成 1
读一个落在 device 1 的 chunk
→ m_deviceid = 1
→ erofs_map_dev(): idr_find(device_id-1=0) 拿到 dif(有 uniaddr,无 file)
→ flatdev ⇒ m_pa += erofs_pos(dif->uniaddr),★不换 m_dif/m_bdev
→ iomap->bdev 仍是 sb->s_bdev
⇒ 含义:内核假设所有额外设备的数据就拼接在主设备之后,于是”设备 1 的块 0” = “主设备的块 uniaddr”。若实际不是这样,读到的就是错数据(这是挂载者的责任)。
4.3 真多设备(-o device= 给足)
mkfs --blobdev=blob.img
mount -t erofs -o device=/dev/vdc /dev/vdb /mnt
→ Opt_device: dif->path="/dev/vdc" 入 idr(key 0),extra_devices = 1
→ erofs_scan_devices(): ondisk=1 == given=1 ✓;flatdev 保持 false
→ device_id_mask = 1
→ idr_for_each_entry → erofs_init_device(): fs_bdev_file_open_by_path() 打开 /dev/vdc
→ dif->file(块设备)→ dif->dax_dev(若支持)
→ 填 blocks / uniaddr
读一个落在 device 1 的 chunk
→ m_deviceid = 1
→ erofs_map_dev(): idr_find(0) → dif → erofs_fill_from_devinfo(map, sb, dif)
⇒ m_dif = dif(device 1)
⇒ m_bdev = file_bdev(dif->file) = /dev/vdc 的 bdev ★
→ iomap->bdev = /dev/vdc;addr = dif->fsoff + m_pa
→ bio 也从 /dev/vdc 提交
5. 生命周期与清理
/* super.c:879 erofs_free_dev_context() */
idr_for_each(&devs->tree, &erofs_release_device_info, sb); /* 逐个释放 */
idr_destroy(&devs->tree);
erofs_release_device_info():
fs_bdev_file_release(dif->file, sb)(super.c:865)关闭打开的设备文件- 释放
dif->path - 释放
dif本身
⇒ 只有真正打开过(非 flatdev)的设备才需要关闭;flatdev 建的 dif 只有内存数据。
6. 实测锚点(本机验证过的几个值)
| 场景 | 实测值 | 出处 |
|---|---|---|
带 --blobdev 的镜像 extra_devices |
1 | superblock 偏移 86(od 实测 01 00) |
devt_slotoff |
9 ⇒ device table 在 9 * 128 = 1152 |
superblock 偏移 88 |
chunk-based 文件的 device_id |
固定 1(mkfs 只支持一个 blob 设备) | erofs_map_chunks VM 打印 last = 0x1_0000_0000_0010 |
flatdev |
未触发(我们给了 -o device=) |
— |
7. 关键代码引用
| 环节 | 位置 |
|---|---|
sb->s_bdev 设置(VFS) |
super.c:797 erofs_fc_get_tree() → get_tree_bdev_flags() |
sbi->devs 创建 + idr/rwsem 初始化 |
super.c:922 / 929 / 930 |
-o device= 解析(只存路径) |
super.c:519 case Opt_device |
dif0.blocks / packed_nid |
super.c:302 / 322 / 327 |
dif0.dax_dev |
super.c:710 |
设备扫描 / flatdev / device_id_mask |
super.c:183 erofs_scan_devices()(含 199/213/216 三处判定) |
| 打开单个设备 + 填 blocks/uniaddr | super.c:125 erofs_init_device() |
| 释放 | super.c:879 erofs_free_dev_context()、super.c:865 fs_bdev_file_release() |
| 统一地址 → 具体设备 | data.c:197 erofs_map_dev() |
m_bdev 赋值(主设备 / 额外设备) |
data.c:187 erofs_fill_from_devinfo() + data.c:207 |
| 元数据 mapping(主设备) | data.c:55 erofs_init_metabuf()(data.c:71) |
iomap 用 m_bdev / dax_dev |
data.c:315–330 |
压缩路径 bio_alloc(m_bdev) |
zdata.c:1693 / 1726 |
fileio 用 m_dif->file |
fileio.c:66 / 72 / 212 |
| 结构体定义 | internal.h:44(erofs_device_info)、66(erofs_dev_context)、371(erofs_map_dev) |
| on-disk device table 项 | erofs_fs.h: struct erofs_deviceslot(128 B,含 tag[64] / blocks / uniaddr) |
参考
EROFS 官方文档 Release 0.1
erofs-utils(1.9.4)
linux-stable (93f51579e7df)