QEMU 最小系统实战:内核 + BusyBox 从零启动 Linux
前两篇准备好了两个核心组件:14 MB 的 defconfig 内核和 2.4 MB 的静态 BusyBox。这篇把它们组装起来,在 QEMU 里启动一个完整的最小 Linux 系统——从创建 rootfs、写 init 脚本、打包 initramfs,到 QEMU 启动和调试。
整体架构
先理清三个组件的关系:
QEMU 启动流程:
qemu-system-x86_64
├── -kernel bzImage ← 内核:操作系统核心
├── -initrd initramfs.cpio.gz ← 根文件系统:用户空间的所有东西
└── -append "..." ← 传给内核的启动参数
内核启动后:
1. 解压 initramfs 到内存中的 ramfs
2. 查找并执行 /init
3. /init 挂载虚拟文件系统,启动 shell
4. 用户拿到交互终端构建 rootfs
BusyBox make install 之后,_install 目录有了 bin/、sbin/、usr/,但还缺几样东西。
创建必要目录
cd ~/build/busybox-1.37.0/_install
mkdir -p proc sys dev这三个目录是 Linux 虚拟文件系统的挂载点:
| 目录 | 挂载的文件系统 | 内容 |
|---|---|---|
/proc | procfs | 进程信息、内核参数(/proc/cpuinfo、/proc/meminfo) |
/sys | sysfs | 设备模型、驱动信息(硬件拓扑的结构化视图) |
/dev | devtmpfs | 设备文件(/dev/sda、/dev/tty、/dev/null) |
这些目录本身是空的——内容由内核在挂载时动态生成。但如果目录不存在,mount 命令会报错,所以必须提前创建。
一个常见疑问:/dev 下面是设备文件,/sys 下面也有设备信息,为什么要两套?
它们的抽象层次不同。/dev 是操作接口——你对 /dev/sda 执行读写就是在操作硬盘。/sys 是信息接口——你读 /sys/block/sda/size 可以查看硬盘容量,但不能通过写入 /sys 来改变硬盘的物理大小。/sys 只暴露内核允许的属性,而且哪些可读、哪些可写都由驱动代码严格控制。
创建 init 脚本
内核解压完 initramfs 后,会寻找 /init 来执行。查找顺序:
- 内核启动参数
init=指定的路径(如init=/bin/sh) /init/sbin/init/etc/init/bin/init/bin/sh
都找不到就 panic。我们在根目录创建 /init:
cat > init << 'EOF'
#!/bin/sh
mount -t devtmpfs none /dev
mount -t proc none /proc
mount -t sysfs none /sys
echo ""
echo "====================================="
echo " Mini Linux (BusyBox + Custom Kernel)"
echo " Kernel: $(uname -r)"
echo " Boot took $(cut -d' ' -f1 /proc/uptime) seconds"
echo "====================================="
echo ""
setsid cttyhack /bin/sh
EOF
chmod +x init逐行解释:
mount -t devtmpfs none /dev:让内核自动填充/dev下的设备节点(tty、null、console 等)mount -t proc none /proc:挂载 procfs,之后ps、top才能工作,/proc/uptime才能读取启动时间mount -t sysfs none /sys:挂载 sysfs,提供设备信息setsid cttyhack /bin/sh:启动 shell。setsid创建新会话,cttyhack设置控制终端(controlling terminal)
为什么不直接 exec /bin/sh?如果直接启动 shell,你会看到:
/bin/sh: can't access tty; job control turned off这不是致命错误,shell 能用,但 Ctrl+C 和 Ctrl+Z 不工作——因为没有控制终端,内核不知道该把信号发给谁。setsid cttyhack 解决了这个问题:setsid 让 shell 成为新会话的 leader,cttyhack 把 /dev/tty 绑定为控制终端,信号才能正确传递。
init 是什么
/init 是一个普通的可执行文件(这里是 shell 脚本),不是目录也不是什么特殊格式。内核启动完成后,它是第一个用户空间进程(PID 1)。内核将控制权交给 init 之后,就只负责处理系统调用和中断了——后续所有用户空间的事情都由 init 及其子进程管理。
在真实的 Linux 发行版里,PID 1 是 systemd 或 SysVinit,它们负责启动几十个系统服务。在我们的迷你系统里,init 只做三件事:挂载虚拟文件系统、打印欢迎信息、启动 shell。
打包 initramfs
内核不能直接使用一个目录作为 rootfs——需要把目录打包成特定格式。initramfs 使用的是 cpio 归档 + gzip 压缩。
cd ~/build/busybox-1.37.0/_install
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ~/build/initramfs.cpio.gz这个命令做了三件事:
find . -print0:递归列出当前目录下所有文件,用 null 字符分隔(处理文件名中的空格和特殊字符)cpio --null -ov --format=newc:将文件列表打包成 cpio 归档。--format=newc是 Linux 内核要求的 "new (SVR4) portable format",-o表示创建归档,-v显示正在打包的文件名gzip -9:最高压缩率压缩
产出:
ls -lh ~/build/initramfs.cpio.gz-rw-r--r-- 1 hceax hceax 1.3M Mar 27 21:40 /home/hceax/build/initramfs.cpio.gz1.3 MB 的 initramfs,包含了一个完整的用户空间——shell、300+ 个命令、init 脚本。
为什么要用 cpio 而不是 tar?因为 Linux 内核的 initramfs 解析代码只认 cpio 格式——这是写死在 init/initramfs.c 里的。
为什么还要 gzip?内核同时支持 cpio 和 gzip 压缩的 cpio(以及 xz、lz4 等)。1.3 MB 压缩后加载更快,对 RAM 方案来说压缩比值得。不压缩也能用,只是体积更大。
安装 QEMU
WSL2 里直接 apt 安装:
sudo apt update
sudo apt install qemu-system-x86 -yKVM 加速
QEMU 默认用软件模拟 CPU 指令——完全可用,但慢。加上 -enable-kvm 可以利用硬件虚拟化,性能接近原生。
WSL2 支持嵌套虚拟化,但需要两步配置:
1. Windows 侧:启用嵌套虚拟化
创建或编辑 C:\Users\<你的用户名>\.wslconfig:
[wsl2]
nestedVirtualization=true保存后在 PowerShell 里重启 WSL:
wsl --shutdown2. WSL 侧:加载 KVM 模块
sudo modprobe kvm_amd # AMD CPU
# 或
sudo modprobe kvm_intel # Intel CPU
sudo chmod 666 /dev/kvm验证:
ls -la /dev/kvmcrw-rw-rw- 1 root kvm 10, 232 Mar 27 21:00 /dev/kvm如果 /dev/kvm 存在且权限正确,就可以用 -enable-kvm 了。
启动
所有组件就位,一行命令启动:
qemu-system-x86_64 \
-kernel ~/build/linux-6.18.19/arch/x86/boot/bzImage \
-initrd ~/build/initramfs.cpio.gz \
-nographic \
-append "console=ttyS0 earlyprintk=serial,ttyS0,115200" \
-enable-kvm \
-m 512M参数说明:
| 参数 | 作用 |
|---|---|
-kernel | 指定内核镜像,QEMU 直接加载(跳过 GRUB) |
-initrd | 指定 initramfs,内核解压后挂载为 rootfs |
-nographic | 不开图形窗口,所有输出走串口到当前终端 |
-append "..." | 传给内核的 cmdline 参数 |
console=ttyS0 | 内核控制台输出到第一个串口(配合 -nographic) |
earlyprintk=serial,... | 启用早期打印——内核还没初始化主控制台时就开始输出 |
-enable-kvm | 启用 KVM 硬件加速 |
-m 512M | 分配 512 MB 内存 |
earlyprintk 在调试启动问题时很关键。没有它,如果内核在初始化控制台之前就挂了(比如解压 initramfs 失败),你看到的就是黑屏,没有任何错误信息。加上 earlyprintk 后,内核从最早期就通过串口输出日志。
启动输出
[ 0.000000] Linux version 6.18.19 (hceax@DESKTOP-...) (gcc (Ubuntu ...) ...)
[ 0.000000] Command line: console=ttyS0 earlyprintk=serial,ttyS0,115200
...
[ 0.571073] Freeing unused kernel image (initmem) memory: 2592K
[ 0.573076] Run /init as init process
=====================================
Mini Linux (BusyBox + Custom Kernel)
Kernel: 6.18.19
Boot took 0.59 seconds
=====================================
~ #0.59 秒,从 QEMU 启动到拿到交互 shell。
基本操作
# 查看进程
~ # ps
PID USER COMMAND
1 0 /bin/sh
2 0 [kthreadd]
...
# 系统信息
~ # uname -a
Linux (none) 6.18.19 #2 SMP PREEMPT_DYNAMIC ... x86_64 GNU/Linux
# 内存
~ # free -m
total used free shared buff/cache available
Mem: 440 12 420 0 7 428
# 文件系统
~ # df -h
Filesystem Size Used Avail Use% Mounted on
none 220M 1.2M 219M 1% /
# 设备
~ # ls /dev
console full null ptmx pts random tty urandom zero一个完整的 Linux 系统——只是非常小。
退出 QEMU
-nographic 模式下,QEMU 占用了整个终端。退出方式:
- 先按
Ctrl+A,松开后再按X——这是 QEMU 串口控制台的退出快捷键 - 或者在 shell 里执行
poweroff
注意 Ctrl+C 和 Ctrl+Z 会被 shell 捕获(或者被 QEMU 拦截),不能用来退出 QEMU。
调试技巧
启动卡住 / 黑屏
如果 QEMU 启动后没有任何输出:
- 检查
-nographic和console=ttyS0是否都加了——少一个就可能看不到输出 - 确认
earlyprintk=serial,ttyS0,115200在-append里——这能捕获最早期的内核输出 - 检查 bzImage 格式:
file arch/x86/boot/bzImage应该输出包含 "Linux kernel" 的描述
内核 panic
如果看到类似输出:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)说明内核找不到 rootfs。检查 -initrd 路径是否正确、initramfs 是否包含 /init。
启动极慢
如果启动需要几十秒甚至更久,很可能内核编译时开了重量级调试选项(KASAN、lockdep、ftrace 测试)。检查 .config:
grep -E 'CONFIG_KASAN=|CONFIG_PROVE_LOCKING=|CONFIG_FTRACE_STARTUP_TEST=' .config如果有任何一个是 =y,建议重新 make defconfig 编译。
组件清单
最终用到的全部文件:
| 文件 | 大小 | 来源 |
|---|---|---|
bzImage | 14 MB | Linux 6.18.19 make defconfig && make -j$(nproc) bzImage |
initramfs.cpio.gz | 1.3 MB | BusyBox 1.37.0 _install 目录打包 |
| 合计 | 15.3 MB |
15 MB,一个能交互的 Linux 系统。没有 systemd、没有 apt、没有 D-Bus,但内核调度、内存管理、虚拟文件系统、设备驱动一样不缺。这就是 Linux 的魅力——内核 + 一个 shell,就是一个完整的操作系统。