4. 模拟启动故障:模拟内存不足导致的 OOM 崩溃
内存不足(OOM,Out-Of-Memory)是操作系统启动阶段最隐蔽的故障之一。当虚拟机被分配过少的物理内存,或内核在初始化阶段过度消耗内存时,系统可能在 init 进程启动前或启动瞬间触发内核 OOM Killer,导致启动失败、进程被随机杀死甚至内核 panic。本节将演示如何通过 QEMU 精确构造 OOM 场景,并利用调试手段捕获崩溃现场。
4.1 OOM 崩溃的触发条件与内核行为
Linux 内核在启动阶段会依次完成页表初始化、伙伴系统建立、slab 分配器初始化等关键步骤。如果物理内存总量低于内核自身运行所需的最小阈值(通常为 32 MB ~ 64 MB),内核会在以下位置之一失败:
- mm_init():内存管理子系统初始化时,无法分配管理结构体。
- kmem_cache_init():slab 缓存创建失败。
- init_post():启动用户态 init 进程时,fork() 因内存不足返回 -ENOMEM。
下表总结了不同内存配置下内核的典型行为:
| 分配内存 | 内核版本 | 启动阶段 | 崩溃表现 |
|---|---|---|---|
| 8 MB | 5.10+ | setup_arch() | 早期 panic,无法解压内核 |
| 16 MB | 5.10+ | mm_init() | OOM Killer 触发,内核 panic |
| 32 MB | 5.10+ | init 启动 | init 进程被 OOM 杀死,系统挂起 |
| 64 MB | 5.10+ | 正常启动 | 系统可用,但 swap 压力大 |
4.2 构造 OOM 场景:QEMU 命令行与内存参数
使用 -m 16M 参数将虚拟机内存限制为 16 MB。同时关闭 KVM 加速(-machine accel=tcg)以模拟纯软件环境下的内存压力。完整命令行如下:
qemu-system-x86_64 \
-m 16M \
-machine accel=tcg \
-kernel bzImage \
-append "console=ttyS0 oops=panic panic=1" \
-initrd initramfs.cpio.gz \
-nographic \
-serial mon:stdio \
-s -S
关键参数说明:
-m 16M:分配 16 MB 物理内存,触发 OOM 临界条件。-machine accel=tcg:使用 TCG 软件模拟,避免 KVM 硬件加速掩盖内存压力。oops=panic panic=1:内核遇到 OOPS 立即 panic,并 1 秒后重启(便于观察)。-s -S:在启动时暂停 CPU,等待 GDB 连接。
-s -S 暂停后,先通过 QEMU Monitor 执行 info mtree 查看内存布局,确认物理地址空间是否被正确映射。再通过 GDB 设置断点 __alloc_pages_nodemask 跟踪内存分配失败路径。
4.3 启动故障复现与串口日志分析
执行上述命令后,虚拟机启动并立即输出内核早期日志。典型 OOM 崩溃日志如下:
[ 0.000000] Linux version 5.10.0 ...
[ 0.000000] Kernel command line: console=ttyS0 oops=panic panic=1
[ 0.000000] Memory: 14336K/16384K available (6144K kernel code, ...)
[ 0.000000] SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=1, Nodes=1
[ 0.000000] kmem_cache_init: Failed to allocate 0x1000 bytes for 'names_cache'
[ 0.000000] Kernel panic - not syncing: Out of memory and no killable processes
[ 0.000000] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 5.10.0
[ 0.000000] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996)
[ 0.000000] Call Trace:
[ 0.000000] dump_stack+0x57/0x6a
[ 0.000000] panic+0x101/0x2d7
[ 0.000000] out_of_memory+0x4a/0x5c0
[ 0.000000] __alloc_pages_slowpath+0x5d0/0x6e0
[ 0.000000] __alloc_pages_nodemask+0x2c0/0x320
[ 0.000000] kmem_getpages+0x3a/0x100
[ 0.000000] fallback_alloc+0x12a/0x1c0
[ 0.000000] ____cache_alloc+0x1a0/0x1e0
[ 0.000000] kmem_cache_alloc+0x9a/0x100
[ 0.000000] create_kmalloc_cache+0x40/0x80
[ 0.000000] kmem_cache_init+0x1a0/0x2c0
[ 0.000000] start_kernel+0x2a0/0x500
[ 0.000000] secondary_startup_64+0xb6/0xc0
从调用栈可以清晰看到:kmem_cache_init() 调用 create_kmalloc_cache() 时,底层 __alloc_pages_nodemask() 返回 NULL,最终触发 out_of_memory() 导致 panic。这是典型的 slab 分配器初始化阶段 OOM。
4.4 OOM 崩溃路径的 SVG 流程图
下图展示了从内核启动到 OOM panic 的完整调用链与内存状态变化:
4.5 使用 QEMU Monitor 和 GDB 定位内存分配失败
当虚拟机因 -s -S 暂停时,可以分步执行以下调试流程:
- QEMU Monitor 检查内存布局: 在 QEMU 控制台输入
info mtree,确认 RAM 区域大小是否为 16 MB。 - GDB 连接并设置断点: 在宿主机启动 GDB,连接目标
localhost:1234,设置断点:
gdb vmlinux
(gdb) target remote localhost:1234
(gdb) break __alloc_pages_nodemask
(gdb) continue
- 观察分配参数: 当断点命中时,打印
gfp_mask和order参数:
(gdb) print/x gfp_mask
$1 = 0x10cc0
(gdb) print order
$2 = 0
gfp_mask = 0x10cc0 表示 __GFP_IO | __GFP_FS | __GFP_RECLAIM,但此时系统尚未完成 IO 初始化,回收机制不可用,导致分配失败。
kmem_cache_init() 阶段,内存回收(kswapd)尚未启动,所有 __GFP_RECLAIM 请求都会直接失败。因此 16 MB 内存中,内核代码段 + 固定映射 + 页表已经占用约 12 MB,剩余不足 4 MB 无法满足 slab 分配器的连续页需求。
4.6 故障复现检查清单
为确保 OOM 场景可稳定复现,请逐项核对以下配置:
| 检查项 | 推荐值 | 验证方法 |
|---|---|---|
| QEMU 版本 | ≥ 6.0 | qemu-system-x86_64 --version |
| 内存大小 | 16 MB | -m 16M |
| 加速模式 | TCG | -machine accel=tcg |
| 内核版本 | 5.10 LTS | make kernelversion |
| initramfs | 最小 busybox | 文件大小 < 2 MB |
| 串口日志 | 完整捕获 | console=ttyS0 |
-enable-kvm),QEMU 可能会自动扩展内存页或使用大页,导致 16 MB 配置下仍能勉强启动。务必使用 TCG 模式以获得最精确的内存压力模拟。
4.7 小结与调试要点
- OOM 在启动阶段不可恢复: 内核尚未建立完整的回收机制,任何内存分配失败都会直接 panic。
- 串口日志是首要线索: 通过
console=ttyS0捕获的调用栈能精确指出失败函数。 - QEMU Monitor + GDB 组合: 使用
info mtree验证物理内存布局,使用 GDB 断点跟踪分配参数。 - 最小内存阈值: 对于 5.x 内核,32 MB 是安全启动的最低推荐值;16 MB 用于构造故障场景。