25 · erofs mount 流程 ftrace 完整跟踪(blobdev 多设备实测)
源码树:
linux-stable(Linux 7.3.0-rc4)
方法:QEMU VM 内 ftracefunction_graph,根函数__x64_sys_mount,
从 mount(2) 进入到系统调用返回的全部函数调用零遗漏抓取(不设任何 notrace)。
0. 摘要
| 项 | 值 |
|---|---|
| 挂载命令 | mount -t erofs -o device=/dev/vdc /dev/vdb /mnt/md |
| 结果 | 成功(RC=0) |
| mount(2) 总耗时 | 1002.610 µs(单 CPU 单进程,无跨核噪声) |
| 抓到的函数调用记录 | 3778 行(全部属于 mount-106 进程) |
| erofs 专属函数命中 | 22 种(另 2 种被编译器内联,见 §6.9) |
| 挂载后 dmesg | erofs (device vdb): initialized per-cpu workers successfully.erofs (device vdb): mounted with root inode @ nid 40. |
一句话:mount(2) 走 VFS 的 fs_context 新框架,
把 source(主设备)与device=(blob 设备)两个参数分两次喂给 erofs_fc_parse_param;
get_tree_bdev_flags 打开主设备后进入 erofs_fc_fill_super,在其中按固定顺序:
读 superblock → 解析全局配置 → 打开 blob 设备 → 初始化压缩子系统 → 读根 inode →注册 shrinker/xattr/sysfs;
最后 do_add_mount 把 vfsmount 挂进 namespace。
1. 测试环境配置
1.1 宿主机与虚拟机
| 项 | 值 |
|---|---|
| 宿主机 | linux系统,QEMU /usr/bin/qemu-system-x86_64 |
| 内核 | linux-stable(7.3.0-rc4),bzImage:linux-stable/arch/x86/boot/bzImage |
| 启动方式 | -kernel bzImage -initrd erofs-boot-initrd.img,rdinit=/init,console=ttyS0 |
| 内存/CPU | -m 8192 -smp 4(实际 mount 全程落在 CPU0) |
| 用户态 | busybox initramfs(switch root 到 ext4 rootfs 盘) |
| 数据盘 | virtio:vdb=主镜像,vdc=blob 镜像 |
| 文件回传 | 9p:宿主 /home/linux/erofs ↔ VM /host(trace 不走串口,防堵死) |
QEMU 命令摘录:
qemu-system-x86_64 -m 8192 -smp 4 -nographic \
-kernel arch/x86/boot/bzImage --enable-kvm \
-initrd /home/linux/erofs/erofs-boot-initrd.img \
-append "console=ttyS0 rdinit=/init" \
-drive file=/home/linux/erofs/erofs-rootfs-ext4.img,format=raw,if=virtio \
-drive file=/home/linux/erofs/multidev/main.erofs,format=raw,if=virtio \
-drive file=/home/linux/erofs/multidev/blob.img,format=raw,if=virtio \
-virtfs local,path=/home/linux/erofs,mount_tag=host,security_model=none,id=host0 \
-monitor none -no-reboot
虚拟机内部命令(vm-cmds-mount.txt):
# VM 内依次执行的命令(宿主机 02-vm-run.sh 一次性喂入)
# busybox sh 顺序执行,一条跑完才读下一条;最后 poweroff -f 主动关机
# 9p 共享:把宿主机的 /sdd/erofs 挂到 VM 的 /host
mkdir -p /host
mount -t 9p -o trans=virtio,version=9p2000.L host /host
# 主戏:ftrace function_graph 完整跟踪 erofs 多设备 mount
sh /host/multidev/trace-mount.sh
# 收尾
echo '@@@@@@ ALL_DONE @@@@@@'
sleep 2
poweroff -f
1.2 镜像制作(blobdev)
脚本 multidev/01-mkimg.sh(/opt/erofs-utils/bin/mkfs.erofs):
head -c 4194304 /dev/urandom > src2/rnd4m.bin # 真随机:压不动 → chunk 落 blob
yes "repeatable payload for lz4" | head -c 1048576 > src2/text1m.bin # 高度可压
truncate -s 64M blob.img
mkfs.erofs -b4096 --chunksize=65536 -zlz4 \
--blobdev=/home/linux/erofs/multidev/blob.img main.erofs src2
dump.erofs 验证结论(实测输出):
| 文件 | 落点 | 证据 |
|---|---|---|
/rnd4m.bin(4 MiB 随机) |
device 1(blob) | 0..4194304 : 0..4194304 # device 1 |
/text1m.bin(1 MiB 重复文本) |
device 0(主设备,压缩) | 2 个 extent,phys 落主设备 4096..12288 |
| superblock | incompat 含 chunked_file device_table |
dump -s 输出 |
即:镜像自带 device table(devt_slotoff 槽位),
blob.img 是第 1 号设备的数据后端,-o device=/dev/vdc 在挂载时把这个后端交还给内核。
2. ftrace 跟踪设置
VM 内脚本 multidev/trace-mount.sh 核心配置:
TD=/sys/kernel/tracing
echo 0 > tracing_on
echo > set_graph_function # 清空根函数
echo > set_graph_notrace # ★ 不设任何 notrace → 函数零遗漏
echo > set_ftrace_filter # ★ 不设任何 filter → 子树全展开
echo nop > current_tracer
echo 2048 > buffer_size_kb # 4 vCPU × 2048 KB 防溢出
echo function_graph > current_tracer
echo __x64_sys_mount > set_graph_function
echo 0 > options/funcgraph-irqs # 不展开中断(中断不属于 mount 调用链)
echo 1 > options/funcgraph-proc # 输出带进程名+PID
echo 1 > options/funcgraph-tail # 叶子带 } /* func */ 尾注
echo 1 > options/funcgraph-abstime # 绝对时间戳
echo 1 > tracing_on
mount -t erofs -o device=/dev/vdc /dev/vdb /mnt/md
RC=$?
echo 0 > tracing_on
cat trace > /host/multidev/graph-mount-full.log # 直写 9p,不走串口
2.1 为什么根函数是 __x64_sys_mount
do_mount / ksys_mount / vfs_kern_mount 与调用方同一编译单元(TU),
被编译器内联后 ftrace 的运行时插桩(mcount/fentry)打不进去,实测 0 命中(该坑在 multidev 专题测读路径时已踩过)。
syscall 入口 __x64_sys_mount是独立的可插桩函数,从它开始既能覆盖完整 VFS 路径,
又天然把 shell 的 fork/exec 等无关活动排除在外(function_graph 只输出根函数命中的子树)。
2.2 输出格式怎么读
时间戳 | CPU) 进程-PID | DURATION | FUNCTION CALLS
| | | | | |
10.302056 | 0) mount-106 | | __x64_sys_mount() {
10.302057 | 0) mount-106 | | strndup_user() {
10.302057 | 0) mount-106 | 1.518 us | } /* memdup_user */
- 缩进(每级 2 格 +
|)= 调用层级;}行 = 函数返回 DURATION列:叶子函数给出耗时;+表示 >10 µs、!表示 >100 µs、#表示 >1000 µs- 空白 DURATION 的
{行是”有子调用”的容器节点,耗时见其配对的}行
3. 被跟踪的 mount 命令与结果
# VM 内实际执行(tracing_on=1 与 tracing_on=0 之间仅此一条命令):
mount -t erofs -o device=/dev/vdc /dev/vdb /mnt/md
# echo @@@@@@ MOUNT_RC=0 @@@@@@
# /proc/mounts:
/dev/vdb on /mnt/md type erofs (ro,relatime,user_xattr,acl,cache_strategy=readaround)
# dmesg:
[ 10.299348] erofs (device vdb): initialized per-cpu workers successfully.
[ 10.299538] erofs (device vdb): mounted with root inode @ nid 40.
# ls /mnt/md:
-rw-r--r-- 0 4194304 rnd4m.bin
-rw-r--r-- 0 1048576 text1m.bin
注意 dmesg 时间戳(10.299538)早于 trace 起始(10.302056)约 2.5 ms:
串口输出走了重定向缓冲,属正常现象;trace 内部时序(10.302056 → 10.303058)自洽。
4. 完整函数调用栈(原始 trace 全文,3783 行)
阶段定位索引(行号为本文代码块内的行号,与原始 log 文件一致):
| 行号 | 节点 | 阶段 |
|---|---|---|
| 6 | __x64_sys_mount() { |
系统调用入口 |
| 158 | path_mount() |
VFS 挂载主流程 |
| 188 | fs_context_for_mount() |
创建 fs_context |
| 198 | erofs_init_fs_context() |
erofs:sbi/devs 分配 |
| 232 | erofs_fc_parse_param() #1 |
erofs:source=/dev/vdb |
| 240 | parse_monolithic_mount_data() |
data 串解析 |
| 264 | erofs_fc_parse_param() #2 |
erofs:device=/dev/vdc |
| 315 | vfs_get_tree() |
构建超级块树 |
| 316 | erofs_fc_get_tree() |
erofs:get_tree 钩子 |
| 317 | get_tree_bdev_flags() |
块设备型 fs 通用逻辑 |
| 318 | lookup_bdev() |
解析主设备路径 |
| 388 | sget_fc() |
分配 super_block |
| 538 | setup_bdev_super() |
打开主设备 → sb->s_bdev |
| 662 | erofs_fc_fill_super() |
erofs:fill_super 主体(808.7 µs) |
| 666 | erofs_read_metabuf() #1 |
读 superblock |
| 915 | z_erofs_parse_cfgs() |
全局缓冲/per-cpu worker 配置 |
| 1201 | erofs_init_device() |
erofs:打开 blob 设备 |
| 1428 | z_erofs_init_super() |
压缩子系统初始化 |
| 2635 | erofs_setup_managed_cache() |
managed cache 伪 inode |
| 2905 | erofs_iget() |
根 inode(nid 40) |
| 3041 | erofs_shrinker_register() |
shrinker |
| 3051 | erofs_register_sysfs() |
sysfs |
| 3650 | do_add_mount() → graft_tree() |
挂入 namespace |
| 3736 | put_fs_context() → erofs_fc_free() |
erofs:fc 清理 |
| 3783 | } /* __x64_sys_mount */ |
系统调用返回 |
5. 主干调用链提炼(erofs 视角)
从 3778 行原始 trace 中提炼出的主干(缩进即层级,★ 标 erofs 专属函数,
耗时取自 trace 配对的 } 行):
__x64_sys_mount # 1002.610 us
├─ strndup_user ×3 # dev_name / dir_name / type 从用户态拷入
└─ path_mount
├─ security_sb_mount # SELinux 授权
├─ get_fs_type("erofs") # 找到 erofs_fs_type, try_module_get
├─ fs_context_for_mount
│ └─ alloc_fs_context
│ ├─ security_fs_context_alloc
│ └─ erofs_init_fs_context ★ super.c:914 分配 sbi + devs(dev_context)
├─ vfs_parse_fs_qstr("source") # 第 1 次
│ └─ vfs_parse_fs_param
│ ├─ security_fs_context_parse_param
│ └─ erofs_fc_parse_param ★ super.c:476 记录 source=/dev/vdb
├─ parse_monolithic_mount_data # data 串 "device=/dev/vdc"
│ └─ generic_parse_monolithic
│ └─ vfs_parse_monolithic_sep
│ └─ vfs_parse_comma_sep
│ └─ vfs_parse_fs_param # 第 2 次
│ ├─ security_fs_context_parse_param
│ └─ erofs_fc_parse_param ★ Opt_device: kstrdup 路径 + down_write
│ + kmem_cache_alloc(erofs_device_info)
├─ vfs_get_tree
│ └─ erofs_fc_get_tree ★ super.c:797
│ └─ get_tree_bdev_flags # 块设备型文件系统通用逻辑
│ ├─ lookup_bdev → kern_path # 路径解析 /dev/vdb
│ ├─ sget_fc # 31.4 us 分配 super_block
│ ├─ setup_bdev_super
│ │ └─ bdev_file_open_by_dev # 21.5 us 打开主设备 → sb->s_bdev
│ ├─ fs_bdev_register
│ └─ erofs_fc_fill_super ★ super.c:651 808.7 us(占全程 80%)
│ ├─ erofs_read_metabuf ★ 读 superblock 92.6 us
│ │ └─ erofs_bread → read_cache_folio →(filemap 全路径)
│ ├─ z_erofs_parse_cfgs ★ decompressor.c:417 59.4 us
│ │ └─ z_erofs_gbuf_growsize # 全局缓冲 + per-cpu worker 池
│ ├─ erofs_scan_devices ★ super.c:183(内联,无节点)
│ │ └─ erofs_init_device ★ super.c:125 42.2 us 打开 blob 设备
│ │ ├─ erofs_read_metabuf # 读 device table 槽位
│ │ └─ fs_bdev_file_open_by_path → bdev_file_open_by_path
│ │ → lookup_bdev(/dev/vdc) → fs_bdev_register
│ ├─ erofs_put_metabuf ×2 ★ 释放元数据 folio 引用
│ ├─ z_erofs_init_super ★ zdata.c:677 478.1 us
│ │ ├─ _erofs_printk # "mounted with root inode @ nid 40"
│ │ └─ erofs_setup_managed_cache ★ super.c:636 65.2 us
│ │ └─ erofs_alloc_inode → 新 slab
│ │ → erofs_inode_init_once ×24 # slab 构造器
│ ├─ erofs_iget ★ inode.c:293 17.2 us 根 inode(nid 40)
│ │ └─ iget5_locked
│ │ ├─ erofs_iget5_set ★ inode.c:284
│ │ └─ erofs_read_inode ★ inode.c:36
│ ├─ erofs_shrinker_register ★ zutil.c:214 1.4 us
│ ├─ erofs_xattr_prefixes_init ★ xattr.c:494
│ └─ erofs_register_sysfs ★ sysfs.c:256 84.6 us(kernfs 全路径)
├─ mount_too_revealing # namespace 逃逸检查
├─ do_add_mount → graft_tree → attach_recursive_mnt 13.1 us
│ # vfsmount 挂入 mount hash + 父计数
├─ __unlock_mount → namespace_unlock
└─ put_fs_context
└─ erofs_fc_free ★ super.c:894 释放 fc 临时结构
6. 知识要点
6.1 mount(2) 如何走上新 fs_context 框架
老接口 mount(2) 并没有独立的 legacy 路径,而是在 path_mount 里被翻译成新 mount API 的五个回调:
init_fs_context → parse_param(逐个参数)→get_tree →(间接)fill_super → 成功/失败后的 fc_free。
erofs 五个钩子在 trace 中全部命中。读 mount 源码时盯住这五个函数即可:
| fc 钩子 | erofs 实现 | trace 命中 |
|---|---|---|
| init_fs_context | erofs_init_fs_context(super.c:914) |
行 198 |
| parse_param | erofs_fc_parse_param(super.c:476) |
行 232、264 |
| get_tree | erofs_fc_get_tree(super.c:797) |
行 316 |
| fill_super | erofs_fc_fill_super(super.c:651,由 get_tree_bdev_flags 调) |
行 662 |
| free | erofs_fc_free(super.c:894) |
行 3744 |
6.2 两次 parse_param 的分工:source 与 device=
- 第 1 次(行 232,1.4 µs):
path_mount直接用vfs_parse_fs_qstr传 source,即 mount(2) 的 dev_name/dev/vdb。erofs 只把它记进ctx->devs的 zero 号设备位(dif0,主设备),不做任何打开动作。 - 第 2 次(行 264,5.9 µs):
parse_monolithic_mount_data → generic_parse_monolithic把 data 串"device=/dev/vdc"按逗号拆开逐项解析。erofs_fc_parse_param的Opt_device分支同样只做登记:kstrdup设备路径 +down_write(&devs->rwsem)kmem_cache_alloc(erofs_device_info)+ 挂入 idr。
- 要点:参数解析阶段 erofs 对两个设备都只存字符串,真正打开主设备在
get_tree_bdev_flags(setup_bdev_super),打开 blob 设备在erofs_fc_fill_super → erofs_init_device。挂载失败时回滚很干净——erofs_fc_free释放 devs 上下文即可,没有半开的设备。
6.3 get_tree_bdev_flags 的固定三步
erofs 的 get_tree 钩子不自己干活,调 get_tree_bdev_flags(fs/super.c):
lookup_bdev → kern_path:把/dev/vdb解析成 block_devicesget_fc:在 fc 的 sb 队列里查找或新建 super_block(冷挂载走新建; 同设备重复挂载走erofs_fc_test比较复用)setup_bdev_super → bdev_file_open_by_dev:打开主设备,sb->s_bdev就位;随后fs_bdev_register注册 bdev 伪文件系统项- 最后才调
erofs_fc_fill_super—— fill_super 是在 sb 已拿到 bdev 之后
6.4 erofs_fc_fill_super 的固定顺序(本 trace 实测)
| 步 | 函数 | 干什么 | 耗时 |
|---|---|---|---|
| 1 | erofs_read_metabuf |
读 superblock(block 0),校验 magic/布局 | 92.6 µs |
| 2 | z_erofs_parse_cfgs |
全局缓冲 metabox、per-cpu worker 池(”initialized per-cpu workers”) | 59.4 µs |
| 3 | erofs_scan_devices(内联)→ erofs_init_device |
逐槽读 device table;打开 device= 指定的 blob | 42.2 µs |
| 4 | z_erofs_init_super |
压缩子系统、erofs_setup_managed_cache(managed cache 伪 inode)、打印 root nid |
478.1 µs |
| 5 | erofs_iget |
iget5_locked 根 inode(nid 40)→ erofs_read_inode |
17.2 µs |
| 6 | erofs_shrinker_register / erofs_xattr_prefixes_init |
shrinker、xattr 前缀表 | ~1.5 µs |
| 7 | erofs_register_sysfs |
/sys/fs/erofs/<disk> 属性 |
84.6 µs |
6.5 blobdev 场景的两个独立 bdev 句柄
本 trace 最值得看的一段:erofs_init_device(行 1201)内部,
- 先
erofs_read_metabuf读 device table 的 1 号槽(erofs_deviceslot,128 B); - 再
fs_bdev_file_open_by_path → bdev_file_open_by_path → lookup_bdev(/dev/vdc)→fs_bdev_register—— 与主设备走setup_bdev_super的路径平行且独立。
挂载成功后的 dev 结构:sbi->devs->tree idr 上挂着 erofs_device_info,
dif0(主设备)用 sb->s_bdev,dif1(blob)用自己打开的 file/bdev。
读数据时 erofs_map_dev 按 chunk 的 device_id 选用二者之一。
6.6 erofs_inode_init_once ×24 不是 mount 在建 24 个 inode
行 2678-2900 连续 24 次 erofs_inode_init_once(super.c)
发生在erofs_setup_managed_cache → erofs_alloc_inode 内:
这是 slab 分配器给inode cache 补新页时对页内 24 个对象逐个跑构造函数(slab ctor),
managed_cache 伪 inode 只是碰巧成了这块新 slab 的第一个使用者。
mount 真正”读”出来的 inode 只有一个:根 inode(erofs_iget,nid 40)。
6.7 成功路径也会调 erofs_fc_free
行 3736 put_fs_context → erofs_fc_free(super.c:894)出现在 mount 成功之后:
fs_context 本身是”脚手架”,fill_super 成功后 sb/sbi 已被 VFS 接管,
脚手架(含 parse_param 阶段登记的 devs 结构被 sbi 引用的部分)就地拆除。
失败路径同样走 fc_free,因此它是理解 erofs mount 回滚的入口。
6.8 耗时热点:fill_super 占 80%
| 热点 | 耗时 | 原因 |
|---|---|---|
| 读 superblock | 92.6 µs | 首次 read_cache_folio:page 分配 + block 层读 + unlock 等待全路径 |
z_erofs_init_super |
478.1 µs | printk 串口输出 + slab 预热(新 slab ×24 ctor)+ per-cpu 初始化 |
erofs_register_sysfs |
84.6 µs | kernfs 建目录/属性文件的全路径 |
erofs_init_device |
42.2 µs | blob 的路径解析 + bdev 打开 |
sget_fc |
31.4 µs | super_block 分配(alloc_super 大结构体) |
6.9 trace 里”缺席”的 erofs 函数 ≠ 流程缺失
erofs_scan_devices(super.c:183)、do_new_mount(fs/namespace.c)等在trace 中没有独立节点,
是因为被编译器内联进调用者(fill_super / path_mount),ftrace 的 function_graph 只画有独立栈帧的函数。
这是方法论的固有限制,读 trace 时要把 erofs_init_device 的参数处理理解成 scan_devices 循环体。
6.10 从 mount 参数到运行期 dev 结构的完整时序
mount(2) data="device=/dev/vdc"
→ erofs_fc_parse_param: kstrdup + idr 登记 (只存字符串)
→ erofs_fc_fill_super: erofs_scan_devices 循环
→ erofs_init_device: 读 deviceslot + 打开 blob (变成 struct file/bdev)
→ 挂载完成: sbi->devs = { dif0(sb->s_bdev), dif1(blob) }
→ 之后每次读: erofs_map_dev 按 chunk device_id 选设备
7. 复现步骤
# 宿主机(root@55.154.14.159):
cd /home/linux/erofs/multidev
sh 01-mkimg.sh # ① 制作 main.erofs + blob.img(--blobdev)
sh 02-vm-run.sh \
/home/linux/erofs/multidev/vm-cmds-mount.txt \
/home/linux/erofs/multidev/trace-mount.log # ② 起 VM,喂命令,等关机
# ③ 结果已在 /home/linux/erofs/multidev/graph-mount-full.log(VM 经 9p 直写)
grep -a "MOUNT_RC\|GRAPH_MOUNT" trace-mount.log # 确认 RC=0 与行数
VM 侧命令序列(vm-cmds-mount.txt):9p 挂载 → sh /host/multidev/trace-mount.sh
→ poweroff -f。trace-mount.sh 见 §2。
01-mkimg.sh
02-vm-run.sh
trace-mount.sh
附录 A. 产物清单
| 文件 | 说明 |
|---|---|
multidev/trace-mount.sh |
VM 内跟踪脚本(function_graph,根 __x64_sys_mount) |
multidev/vm-cmds-mount.txt |
VM 命令序列 |
multidev/trace-mount.log |
串口全程日志(517 行,含 dmesg 与 mount 输出) |
multidev/graph-mount-full.log |
原始 ftrace 输出(3783 行) |
multidev/main.erofs / blob.img |
本次测试镜像(2026-10-15 17:42 制作) |