4. 模拟启动故障:模拟内存不足导致的 OOM 崩溃

内存不足(OOM,Out-Of-Memory)是操作系统启动阶段最隐蔽的故障之一。当虚拟机被分配过少的物理内存,或内核在初始化阶段过度消耗内存时,系统可能在 init 进程启动前或启动瞬间触发内核 OOM Killer,导致启动失败、进程被随机杀死甚至内核 panic。本节将演示如何通过 QEMU 精确构造 OOM 场景,并利用调试手段捕获崩溃现场。

实验目标: 配置 QEMU 虚拟机仅分配 16 MB 内存,观察 Linux 内核在启动过程中因无法完成内存初始化而触发 OOM 崩溃,并通过 QEMU Monitor 和串口日志定位故障根因。

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.x 内核在 16 MB 下可能仅表现为启动缓慢,而 5.x 内核会直接 panic。本实验基于 Linux 5.10 LTS 内核。

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 的完整调用链与内存状态变化:

OOM 崩溃路径:从 start_kernel 到 panic start_kernel() mm_init() kmem_cache_init() __alloc_pages_nodemask() out_of_memory() panic() 内存状态 可用内存: 14 MB slab 需求: 4 KB 分配结果: NULL 关键调用栈 create_kmalloc_cache() kmem_cache_alloc()

4.5 使用 QEMU Monitor 和 GDB 定位内存分配失败

当虚拟机因 -s -S 暂停时,可以分步执行以下调试流程:

  1. QEMU Monitor 检查内存布局: 在 QEMU 控制台输入 info mtree,确认 RAM 区域大小是否为 16 MB。
  2. GDB 连接并设置断点: 在宿主机启动 GDB,连接目标 localhost:1234,设置断点:
gdb vmlinux
(gdb) target remote localhost:1234
(gdb) break __alloc_pages_nodemask
(gdb) continue
  1. 观察分配参数: 当断点命中时,打印 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
常见陷阱: 如果使用 KVM 加速(-enable-kvm),QEMU 可能会自动扩展内存页或使用大页,导致 16 MB 配置下仍能勉强启动。务必使用 TCG 模式以获得最精确的内存压力模拟。

4.7 小结与调试要点

  • OOM 在启动阶段不可恢复: 内核尚未建立完整的回收机制,任何内存分配失败都会直接 panic。
  • 串口日志是首要线索: 通过 console=ttyS0 捕获的调用栈能精确指出失败函数。
  • QEMU Monitor + GDB 组合: 使用 info mtree 验证物理内存布局,使用 GDB 断点跟踪分配参数。
  • 最小内存阈值: 对于 5.x 内核,32 MB 是安全启动的最低推荐值;16 MB 用于构造故障场景。