让最小 Linux 脱离 QEMU:GRUB 引导与磁盘镜像制作
前几篇我们的最小 Linux 一直靠 QEMU 的 -kernel 和 -initrd 参数"喂"给虚拟机——QEMU 充当保姆,把内核和文件系统直接放进内存。这种方式快速方便,但系统完全依赖 QEMU:换个虚拟机就跑不起来,关机所有数据丢失,连引导过程都被跳过了。
这篇要做的事:让最小 Linux 学会自己从磁盘启动。制作一块包含引导器、内核和根文件系统的磁盘镜像——插到任何支持 BIOS 引导的虚拟机(甚至物理机)里,都能自己启动。
QEMU 保姆模式 vs 独立引导
先对比两种启动方式的本质差异:
QEMU 保姆模式(前几篇):
qemu-system-x86_64 -kernel bzImage -initrd initramfs.cpio.gz -append "..."
└── QEMU 看到 -kernel,直接把内核加载到内存地址 0x100000
└── QEMU 看到 -initrd,直接把 initramfs 加载到内存
└── QEMU 看到 -append,直接设置内核启动参数
→ 跳过了整个引导过程,没有 BIOS、没有 GRUB、没有磁盘读取
独立引导(本篇):
qemu-system-x86_64 -drive file=minilinux.img
└── QEMU 只知道有一块磁盘,不管里面是什么
└── BIOS 读磁盘第一个扇区 → 找到 MBR → 加载 GRUB
└── GRUB 读磁盘上的配置文件 → 找到内核 → 加载到内存
└── 内核启动 → 读磁盘上的根文件系统 → 运行 init
→ 完整的引导链,和真实物理机一样保姆模式的问题:
- 只能在 QEMU 里跑(VMware、VirtualBox 不支持
-kernel参数) - 根文件系统在 RAM 里,关机数据全丢
- 开发者无法学到真实的引导过程
独立引导意味着:系统自给自足,不需要外部帮手。
从 RAM 到磁盘:根文件系统的变化
脱离 QEMU 意味着根文件系统也要从 RAM 搬到磁盘。这不只是存储位置的变化,init 的运行方式也完全不同:
| initramfs 方案(RAM) | 磁盘方案 | |
|---|---|---|
| 存储 | cpio 压缩包解压到 RAM | ext4 分区在磁盘上 |
| 持久性 | 关机全丢 | 关机保留 |
| 内核怎么找到它 | QEMU 的 -initrd 直接喂 | 内核参数 root=/dev/sda1 |
| PID 1 是谁 | 我们自己写的 /init 脚本 | BusyBox 的 /sbin/init 程序 |
| init 怎么工作 | 顺序执行脚本里的命令 | 读取 /etc/inittab 配置 |
BusyBox 的 /sbin/init(实际是 busybox 的符号链接)是一个真正的 init 程序,不是我们手写的脚本。它启动后读取 /etc/inittab,按配置管理系统生命周期——开机初始化、维持终端、处理关机。
引导器:GRUB
系统要自己启动,第一步需要一个引导器。BIOS 只会读磁盘第一个扇区(512 字节),里面必须有能找到内核的代码。
GRUB(GRand Unified Bootloader)就是干这个的。它分多个阶段工作:
BIOS 加载 MBR(512 字节)
→ MBR 的代码加载 GRUB core.img(~30 KB,在 MBR 后面)
→ core.img 内嵌文件系统驱动,能读磁盘上的文件
→ 加载 /boot/grub/ 下的模块和 grub.cfg 配置
→ 根据配置找到并加载内核
→ 把控制权交给内核GRUB 本质上是一个运行在内核之前的微型系统——有自己的文件系统驱动、命令行、脚本语言和模块化插件(.mod 文件)。
从源码编译
cd ~/build
wget https://ftp.gnu.org/gnu/grub/grub-2.14.tar.gz
tar -xzf grub-2.14.tar.gz
cd grub-2.14
sudo apt install -y build-essential bison flex autoconf automake \
gettext libdevmapper-dev python3 pkg-config
./configure --target=i386 --with-platform=pc --prefix=/usr/local
make -j$(nproc)
sudo make install--target=i386 --with-platform=pc 的意思:生成面向传统 BIOS + MBR 的 32 位引导代码。即使宿主机是 64 位的,BIOS 引导阶段运行在实模式/保护模式下,需要 32 位代码。
安装完成后 grub-install 命令可用,GRUB 模块库位于 /usr/local/lib/grub/i386-pc/。
制作磁盘镜像
创建空白磁盘
dd if=/dev/zero of=~/build/minilinux.img bs=1M count=128128 MB 的全零文件——我们的虚拟硬盘。比 initramfs 大了 100 倍,但换来了持久化存储和完整的引导链。
分区
sudo apt install -y parted
parted ~/build/minilinux.img mklabel msdos
parted ~/build/minilinux.img mkpart primary ext4 1MiB 100%mklabel msdos 创建 MBR 分区表。mkpart 从 1 MiB 处开始创建分区——前 1 MiB 留给 MBR 和 GRUB 的 core.img。这 1 MiB 的间隙是 GRUB 安装的前提条件:core.img 要写在 MBR 之后、第一个分区之前。
关联 loop 设备
文件不能直接被 mkfs/mount 操作,需要用 losetup 把它伪装成块设备:
sudo losetup --show -fP ~/build/minilinux.img
# 输出 /dev/loop0-P 让内核自动扫描分区表,创建 /dev/loop0p1。之后 /dev/loop0 = 整块磁盘,/dev/loop0p1 = 第一个分区。
格式化并挂载
sudo mkfs.ext4 /dev/loop0p1
mkdir -p ~/build/mnt
sudo mount /dev/loop0p1 ~/build/mnt安装系统
BusyBox + 目录结构
sudo cp -a ~/build/busybox-1.37.0/_install/* ~/build/mnt/
sudo mkdir -p ~/build/mnt/{proc,sys,dev,etc/init.d,boot}cp -a 保留符号链接和权限。比 initramfs 多了 etc/init.d(init 配置)和 boot(内核 + GRUB)。
inittab
cat <<'EOF' | sudo tee ~/build/mnt/etc/inittab > /dev/null
::sysinit:/etc/init.d/rcS
tty0::respawn:-/bin/sh
ttyS0::respawn:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
EOFBusyBox inittab 格式:设备::动作:命令。
关键设计:在 tty0(VGA)和 ttyS0(串口)上各开一个 shell。这让系统自动适配运行环境——图形界面看 tty0,串口终端看 ttyS0,不存在的设备自动忽略。不需要知道自己运行在 QEMU 还是 VMware 里。
-/bin/sh 前面的 - 表示 login shell,会读取 /etc/profile。respawn 表示 shell 退出后自动重启。
rcS 启动脚本
cat <<'RCSEOF' | sudo tee ~/build/mnt/etc/init.d/rcS > /dev/null
#!/bin/sh
mount -t devtmpfs none /dev 2>/dev/null
mount -t proc none /proc
mount -t sysfs none /sys
dmesg -n 1
printf "\033c" > /dev/tty0 2>/dev/null
printf "\033c" > /dev/ttyS0 2>/dev/null
for tty in /dev/tty0 /dev/ttyS0; do
[ -e "$tty" ] && {
echo "" > "$tty"
echo "=====================================" > "$tty"
echo " Mini Linux (BusyBox + Custom Kernel)" > "$tty"
echo " Kernel: $(uname -r)" > "$tty"
echo " Boot took $(cut -d' ' -f1 /proc/uptime) seconds" > "$tty"
echo "=====================================" > "$tty"
echo "" > "$tty"
}
done
RCSEOF
sudo chmod +x ~/build/mnt/etc/init.d/rcS和 initramfs 的 /init 相比,多了几个关键处理:
dmesg -n 1:把内核 console loglevel 设为 1(只输出紧急消息)。内核有些驱动是延迟初始化的,会在 init 开始运行后继续往屏幕打日志。不先堵住这个口,后面的清屏会被新日志覆盖printf "\033c" > /dev/tty0和> /dev/ttyS0:分别对 VGA 和串口终端执行 VT100 重置。这里不能只写printf "\033c"——rcS 的标准输出走的是/dev/console(即最后一个console=参数指定的设备),只会清一个终端。必须显式重定向到每个终端设备- banner 循环输出到两个终端:同理,欢迎信息也要分别写入
/dev/tty0和/dev/ttyS0,这样不管从哪个终端看都能看到
执行顺序很重要:先 dmesg -n 1 堵住日志源,再清屏。反过来的话清屏和禁日志之间可能有新日志溜进来。
内核
sudo cp ~/build/linux-6.18.19/arch/x86/boot/bzImage ~/build/mnt/boot/vmlinuz安装 GRUB
sudo grub-install \
--boot-directory=~/build/mnt/boot \
--target=i386-pc \
/dev/loop0这一步做了三件事:
- 把引导代码写入 MBR 的前 446 字节
- 把 core.img 写入 MBR 和第一个分区之间的间隙
- 把 GRUB 模块复制到
/boot/grub/i386-pc/
GRUB 配置
cat <<'EOF' | sudo tee ~/build/mnt/boot/grub/grub.cfg > /dev/null
set timeout=3
set default=0
menuentry "Mini Linux (BusyBox + Custom Kernel 6.18.19)" {
linux /boot/vmlinuz root=/dev/sda1 console=tty0 console=ttyS0
}
EOFlinux 命令告诉 GRUB 加载内核并传递启动参数:
root=/dev/sda1:根文件系统所在的分区console=tty0 console=ttyS0:同时启用 VGA 和串口输出
收尾并测试
sudo umount ~/build/mnt
sudo losetup -D磁盘镜像准备完毕。启动命令变得极其简洁:
qemu-system-x86_64 \
-drive file=~/build/minilinux.img,format=raw \
-m 128 \
-nographic没有 -kernel、没有 -initrd、没有 -append。QEMU 只提供硬件模拟,系统自己完成一切。
启动全过程
SeaBIOS → MBR → GRUB 2.14 →
┌────────────────────────────────────────────────────┐
│ *Mini Linux (BusyBox + Custom Kernel 6.18.19) │
│ │
│ The highlighted entry will be executed in 3s. │
└────────────────────────────────────────────────────┘
→ 加载 /boot/vmlinuz → 内核启动 → 挂载 /dev/sda1
→ /sbin/init → 读 /etc/inittab → 执行 rcS → 清屏 →
=====================================
Mini Linux (BusyBox + Custom Kernel)
Kernel: 6.18.19
Boot took 2.18 seconds
=====================================
~ #从 BIOS 到 shell 提示符,和真实物理机的开机过程一模一样。退出 QEMU:Ctrl+A 然后 X。
对比:保姆模式 vs 独立引导
| 保姆模式(前几篇) | 独立引导(本篇) | |
|---|---|---|
| 启动命令 | 需要指定 -kernel、-initrd、-append | 只需要 -drive |
| 引导器 | 无(QEMU 直接加载) | GRUB |
| rootfs 位置 | RAM | 磁盘分区 |
| 数据持久性 | 关机丢失 | 关机保留 |
| init 方式 | 自写脚本(/init) | BusyBox init(/etc/inittab) |
| 可移植性 | 仅限 QEMU | 任何 BIOS 引导环境 |
| 到 shell 的时间 | ~0.6 秒 | ~2 秒 + 3 秒 GRUB 菜单 |
独立引导更慢,但那 5 秒多出来的时间里,系统经历了和你每天开机一样的完整引导链。
磁盘文件结构
minilinux.img (128 MB)
├── [MBR + GRUB core.img] ← 分区之前的磁盘头部
└── 分区 1 (ext4)
├── boot/
│ ├── vmlinuz ← 内核 (14 MB)
│ └── grub/
│ ├── grub.cfg ← GRUB 配置
│ └── i386-pc/ ← GRUB 模块 (~5 MB)
├── bin/
│ ├── busybox ← BusyBox (2.4 MB)
│ ├── sh → busybox
│ └── ...
├── sbin/
│ └── init → ../bin/busybox
├── etc/
│ ├── inittab
│ └── init.d/rcS
├── proc/ sys/ dev/ ← 虚拟文件系统挂载点
└── [剩余空间可自由使用] ← 这就是持久化的好处注意最后一行——磁盘上还有大量可用空间。和 initramfs 方案不同,你在这个系统里创建的文件、修改的配置,关机后都还在。
下一步
磁盘镜像是 raw 格式——可以直接给 QEMU 用,但 VMware 和 VirtualBox 需要各自的格式(VMDK、VDI)。格式转换本身很简单(一行 qemu-img convert),真正的坑在驱动兼容性和终端配置。下一篇讲把这个磁盘搬到 VMware 的完整流程和踩过的坑。