Skip to content

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/,但还缺几样东西。

创建必要目录

bash
cd ~/build/busybox-1.37.0/_install
mkdir -p proc sys dev

这三个目录是 Linux 虚拟文件系统的挂载点:

目录挂载的文件系统内容
/procprocfs进程信息、内核参数(/proc/cpuinfo/proc/meminfo
/syssysfs设备模型、驱动信息(硬件拓扑的结构化视图)
/devdevtmpfs设备文件(/dev/sda/dev/tty/dev/null

这些目录本身是空的——内容由内核在挂载时动态生成。但如果目录不存在,mount 命令会报错,所以必须提前创建。

一个常见疑问:/dev 下面是设备文件,/sys 下面也有设备信息,为什么要两套?

它们的抽象层次不同。/dev操作接口——你对 /dev/sda 执行读写就是在操作硬盘。/sys信息接口——你读 /sys/block/sda/size 可以查看硬盘容量,但不能通过写入 /sys 来改变硬盘的物理大小。/sys 只暴露内核允许的属性,而且哪些可读、哪些可写都由驱动代码严格控制。

创建 init 脚本

内核解压完 initramfs 后,会寻找 /init 来执行。查找顺序:

  1. 内核启动参数 init= 指定的路径(如 init=/bin/sh
  2. /init
  3. /sbin/init
  4. /etc/init
  5. /bin/init
  6. /bin/sh

都找不到就 panic。我们在根目录创建 /init

bash
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,之后 pstop 才能工作,/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+CCtrl+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 压缩

bash
cd ~/build/busybox-1.37.0/_install
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ~/build/initramfs.cpio.gz

这个命令做了三件事:

  1. find . -print0:递归列出当前目录下所有文件,用 null 字符分隔(处理文件名中的空格和特殊字符)
  2. cpio --null -ov --format=newc:将文件列表打包成 cpio 归档。--format=newc 是 Linux 内核要求的 "new (SVR4) portable format",-o 表示创建归档,-v 显示正在打包的文件名
  3. gzip -9:最高压缩率压缩

产出:

bash
ls -lh ~/build/initramfs.cpio.gz
-rw-r--r-- 1 hceax hceax 1.3M Mar 27 21:40 /home/hceax/build/initramfs.cpio.gz

1.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 安装:

bash
sudo apt update
sudo apt install qemu-system-x86 -y

KVM 加速

QEMU 默认用软件模拟 CPU 指令——完全可用,但慢。加上 -enable-kvm 可以利用硬件虚拟化,性能接近原生。

WSL2 支持嵌套虚拟化,但需要两步配置:

1. Windows 侧:启用嵌套虚拟化

创建或编辑 C:\Users\<你的用户名>\.wslconfig

ini
[wsl2]
nestedVirtualization=true

保存后在 PowerShell 里重启 WSL:

powershell
wsl --shutdown

2. WSL 侧:加载 KVM 模块

bash
sudo modprobe kvm_amd    # AMD CPU
# 或
sudo modprobe kvm_intel   # Intel CPU

sudo chmod 666 /dev/kvm

验证:

bash
ls -la /dev/kvm
crw-rw-rw- 1 root kvm 10, 232 Mar 27 21:00 /dev/kvm

如果 /dev/kvm 存在且权限正确,就可以用 -enable-kvm 了。

启动

所有组件就位,一行命令启动:

bash
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。

基本操作

bash
# 查看进程
~ # 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+CCtrl+Z 会被 shell 捕获(或者被 QEMU 拦截),不能用来退出 QEMU。

调试技巧

启动卡住 / 黑屏

如果 QEMU 启动后没有任何输出:

  1. 检查 -nographicconsole=ttyS0 是否都加了——少一个就可能看不到输出
  2. 确认 earlyprintk=serial,ttyS0,115200-append 里——这能捕获最早期的内核输出
  3. 检查 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

bash
grep -E 'CONFIG_KASAN=|CONFIG_PROVE_LOCKING=|CONFIG_FTRACE_STARTUP_TEST=' .config

如果有任何一个是 =y,建议重新 make defconfig 编译。

组件清单

最终用到的全部文件:

文件大小来源
bzImage14 MBLinux 6.18.19 make defconfig && make -j$(nproc) bzImage
initramfs.cpio.gz1.3 MBBusyBox 1.37.0 _install 目录打包
合计15.3 MB

15 MB,一个能交互的 Linux 系统。没有 systemd、没有 apt、没有 D-Bus,但内核调度、内存管理、虚拟文件系统、设备驱动一样不缺。这就是 Linux 的魅力——内核 + 一个 shell,就是一个完整的操作系统。

最后更新于:

Hosted by GitHub Pages