QEMU 直接引导内核:-kernel 与 -initramfs 跑通最小 RISC-V Linux
常规装系统的路径是先把 GRUB 或 U-Boot 这类引导加载器装进磁盘,开机后由它读分区、找内核、拼命令行,再跳进内核。做内核实验、驱动调试或启动流程分析时,这条链路偏重:你只是想让一个指定的内核尽快跑起来,并不关心引导加载器自己的配置。QEMU 提供了更短的路径——QEMU 直接引导内核:-kernel 把内核镜像直接装进客户机内存并跳到入口,-initrd 顺带挂一个 initramfs 作为早期根文件系统,-append 把内核命令行当面递给内核,磁盘里有没有引导扇区都无所谓。
本篇在 RISC-V virt 虚拟平台上实测这条路径:不安装完整发行版的引导流程,直接用 Ubuntu 云镜像里取出的 vmlinuz 与 initrd.img 启动到登录提示符。过程并不一帆风顺,首跑内核日志报 IO charset iso8859-1 not found,boot-efi.mount 挂载失败把系统拖进 emergency mode;在 cmdline 上 mask 掉问题单元后干净启动,并在客户机内用 uname 取证架构为 riscv64。如果你还不熟悉 QEMU 的整体玩法,先读 认识 QEMU:从零看懂虚拟机、镜像与快照 建立基本图景;每个参数的逐字解读可以配合 QEMU 命令行参数逐读:一份 qemu-system 启动参数的注释手册 一起看。
本文要回答的问题
- 不装 GRUB/U-Boot,QEMU 凭什么能直接把内核跑起来?
-kernel、-initrd、-append三个参数各自负责什么? - initramfs 是什么,为什么内核启动早期需要一个「临时根文件系统」?
- 首跑为什么会卡在
IO charset iso8859-1 not found,一个挂载单元失败凭什么把整机拖进 emergency mode? systemd.mask=写在 cmdline 上到底做了什么,为什么能绕过挂载失败?- 直挂内核与走完整发行版引导流程各自适合什么场景,边界在哪里?
一、跳过引导加载器:QEMU 怎么把内核送进内存
先看真实硬件的开机顺序。机器上电后先执行固件(RISC-V 平台上是 OpenSBI),固件不知道你的内核放在磁盘哪个分区、长什么样,于是把控制权交给引导加载器;引导加载器具备读文件系统的能力,由它找到内核镜像、initramfs 和启动参数,再跳进内核入口。每多一层,就多一层配置与故障点。
QEMU 直接引导内核把中间环节整个绕开。QEMU 进程本身运行在主机上,能直接读主机文件系统里的内核文件,于是它扮演了「引导加载器」的角色:按客户机架构的引导约定,把内核镜像加载到客户机物理内存的约定地址,把 initramfs 放到另一块内存并向内核登记起始地址与长度,再把 -append 给出的字符串挂到内核约定的命令行位置,最后让客户机从内核入口开始取指。对客户机内核来说,它看到的启动状态与真实引导加载器交接的没有区别,自然照常往下跑。
这张流程卡是本篇的主线。读的时候抓住一个要点:前三步全部发生在「内核见到用户态之前」,QEMU 与内核之间的交接介质是内存而不是磁盘——这也是直挂方式启动快、且不依赖磁盘引导记录的根本原因。第四步交给 systemd 之后,故事就和普通发行版完全一样了,后面的 emergency 故障也正发生在这一段。
二、三个核心参数:本次实测的启动脚本
本次实验的启动脚本是从真实排错过程中保留下来的第一版,也就是首跑进 emergency mode 的那条命令。脚本里没有任何引导加载器相关参数,内核与 initramfs 直接以文件形式给出:
#!/bin/bash
set -u
IMG=$HOME/soc-qemu-b2/q015-distro/noble-riscv64.img
SEED=$HOME/soc-qemu-b2/q015-distro/seed.iso
BIOS=/usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.bin
timeout 480 qemu-system-riscv64 \
-machine virt -m 2048 -smp 2 -nographic \
-bios "$BIOS" \
-kernel vmlinuz-7.0.0-38-generic -initrd initrd.img \
-append "root=LABEL=cloudimg-rootfs ro rootwait earlycon console=ttyS0" \
-drive file="$IMG",if=virtio,format=qcow2 \
-drive file="$SEED",if=virtio,format=raw
echo "QEMU_EXIT=$?"三个核心参数逐个看。-kernel vmlinuz-7.0.0-38-generic 指定内核镜像,QEMU 能识别架构对应的镜像格式,RISC-V 上这里是未压缩或自解压的内核;-initrd initrd.img 指定 initramfs,内核启动早期先以它作为临时根文件系统,里面带着挂载真实根分区所需的内核模块与用户态工具;-append 后面的字符串就是内核命令行(cmdline),本例的 root=LABEL=cloudimg-rootfs 告诉内核真实根分区在哪个卷标上,ro 要求只读挂载、rootwait 表示等设备就绪、console=ttyS0 把控制台接到串口,配合 -nographic 让全部日志直接打在当前终端。
initramfs 的存在值得多说一句。根文件系统在 virtio 磁盘上,而访问 virtio 磁盘需要相应驱动;如果驱动编成了模块而不是内建,内核刚启动时还没有能力读磁盘,就会陷入「要读磁盘得先有驱动、要加载驱动得先能读磁盘」的死结。initramfs 是解开这个结的临时环境:它被 QEMU 直接放进内存,不依赖任何块设备,里面的 init 程序负责加载模块、识别磁盘,再把根切换(pivot)到真实根分区。理解了这条链路,就能理解为什么 -kernel 与 -initrd 几乎总是成对出现。
脚本里另外两块磁盘是真实根文件系统所在的 qcow2 云镜像,以及一个 cloud-init 种子盘;-bios 指定的 OpenSBI 固件只做最低限度的硬件初始化,不承担引导加载器的选盘职责。这与 QEMU 跑 RISC-V Linux:OpenSBI、内核与 rootfs 的完整发行版启动 里走完整引导流程的路径形成对照——那篇让磁盘上的引导设施自己找内核,本篇则由 QEMU 在命令行里把一切指定死。
三、首跑踩坑:一个字符集缺失如何拖垮整个启动
带着上面的脚本首跑,内核解压、驱动加载、systemd 启动都正常,日志一路刷到本地文件系统挂载阶段,然后出现了本次故障的核心报错。下面是首跑实录中的关键片段:
这五行要从下往上读因果。最底层的一行是内核 FAT 文件系统驱动打出的 FAT-fs (vda15): IO charset iso8859-1 not found:vda15 是磁盘上的 EFI 系统分区,FAT 格式,默认要用 ISO-8859-1 字符集处理短文件名,而提供该字符集的内核模块(nls 相关模块)此刻尚未加载到位,直挂内核时 initramfs 与模块加载的时序和完整引导流程不同,恰好踩空。中间三行是 systemd 的连锁反应:boot-efi.mount 这个挂载单元失败,紧接着 Dependency failed for local-fs.target——本地文件系统目标把该挂载单元列为依赖,依赖不满足,整个 target 宣告失败。
local-fs.target 是 systemd 启动路径上的关键枢纽,正常启动必须跨过它才能继续走向多用户目标。跨不过去时,systemd 的兜底策略是降级到 emergency mode,给出一个维护 shell 等人工介入。实录后半段明确出现了这段提示:
You are in emergency mode. After logging in, type "journalctl -xb" to view
system logs, "systemctl reboot" to reboot, or "exit"
to continue bootup.
Press Enter for maintenance
(or press Control-D to continue):注意这里容易产生一个误判:系统并没有「死机」,内核、systemd、串口 getty 都活着,登录提示符最终也会出现,但启动流程停在降级状态,cloud-init 等后续单元全部异常,日志里还能看到只读根文件系统引发的一连串报错。换句话说,这是一次「活着但没走完」的启动,判据不是黑屏,而是 emergency 提示与 local-fs.target 的依赖失败记录。
四、修复机理:在 cmdline 上 mask 掉问题单元
应急处理有两条路。一是进 emergency shell 手工加载 nls 模块再 exit 继续启动,但这只救当场,下次开机照旧失败;二是从根上让 systemd 不要尝试这个挂载——虚拟平台并不需要从 EFI 分区读取任何引导材料,/boot/efi 挂不上对本次实验毫无影响。systemd 支持在 cmdline 上用 systemd.mask=<单元名> 直接屏蔽单元,效果等同于把该单元软链到 /dev/null,启动时视为不存在,依赖关系也一并解除。
修复后的自动化脚本在 -append 里加了两个 mask,除了罪魁 boot-efi.mount,还预先屏蔽了在无网环境会干等两分钟的 systemd-networkd-wait-online.service:
qemu-system-riscv64 \
-machine virt -m 2048 -smp 2 -nographic \
-bios /usr/lib/riscv64-linux-gnu/opensbi/generic/fw_jump.bin \
-kernel vmlinuz-7.0.0-38-generic -initrd initrd.img \
-append "root=LABEL=cloudimg-rootfs ro rootwait earlycon console=ttyS0 \
systemd.mask=boot-efi.mount \
systemd.mask=systemd-networkd-wait-online.service" \
-drive file=noble-riscv64.img,if=virtio,format=qcow2 \
-drive file=seed.iso,if=virtio,format=raw下图把首跑与修复后复跑的两条路径放在一起对照。红色是故障链路上的两个节点,橙色是修复动作,绿色是干净启动的落点;蓝、紫两色分别表示 QEMU 的引导交接与 cmdline 这个交接载体。
读这张图抓住主线:故障链与修复链共用同一个起点,分叉点就在 cmdline 里有没有那两条 mask。红色两节点是「字符集缺失 → 挂载失败 → 依赖失败 → emergency」的因果链,橙色节点并没有解决字符集模块本身,而是让 systemd 不再发起这次挂载——在虚拟平台上这是更省事也更对症的处理;若在真实物理机上需要 EFI 分区,正确做法则是补齐 nls 模块而不是屏蔽挂载。
五、干净启动取证:cmdline、架构识别与 guest 内 uname
修复后复跑,内核日志开头就能核对 cmdline 是否如预期送达。下面这行是内核自己打印的命令行,两个 systemd.mask 与脚本参数逐字一致,证明 -append 的内容完整进入了内核:
[ 0.000000] Kernel command line: root=LABEL=cloudimg-rootfs ro rootwait earlycon console=ttyS0 systemd.mask=boot-efi.mount systemd.mask=systemd-networkd-wait-online.service继续往后,systemd 接管后的几行输出给出了第二层证据:虚拟化被识别为 QEMU,架构被识别为 riscv64,欢迎语说明用户态已经是完整的 Ubuntu 24.04.5,而不再是 emergency shell。
Detected architecture riscv64 是 systemd 视角的判断,最后一层证据要进到客户机内部取。自动化脚本在串口完成登录后,执行了一组探针命令:uname -m、uname -r、发行版信息与当前用户,第三次复跑的实录如下。
ubuntu@ubuntu:~$ echo GUEST_PROBE_START; uname -m; uname -r; cat /etc/os-release | head -2; whoami; echo GUEST_PROBE_DONE
GUEST_PROBE_START
riscv64
7.0.0-38-generic
PRETTY_NAME="Ubuntu 24.04.5 LTS"
NAME="Ubuntu"
ubuntu
GUEST_PROBE_DONE四个字段各有分工:uname -m 输出 riscv64,从客户机内核自身视角确认跑的是 64 位 RISC-V 架构,而不是主机的 x86;uname -r 的 7.0.0-38-generic 与 -kernel 指定的镜像版本对得上,证明当前内核确实来自直挂文件而非磁盘引导;发行版字段确认根文件系统是 Ubuntu 24.04.5;whoami 说明登录链路完整可用。探针跑完后脚本执行 sudo poweroff,实录末尾出现 Reached target poweroff.target,关机流程也正常收口。
上面的对照实录上半段是首跑的红色失败记录:FAT-fs 字符集报错、boot-efi.mount 的 FAILED、紧随其后的 emergency mode 提示;下半段是加上 mask 后的复跑,同一位置不再出现挂载尝试,启动一路走到 ubuntu login:,登录后探针打印出 riscv64 与内核版本。两段记录出自同一台虚拟机器、同一块磁盘镜像,唯一变量就是 cmdline 里的两条 mask,对照关系干净利落。
六、直挂内核的边界:什么时候该用,什么时候别用
直挂内核不是完整发行版安装的替代品,两者解决的问题不同。把适用场景与代价摊开对比,选型时按表判断:
| 对比维度 | QEMU 直挂内核(-kernel/-initrd) | 完整发行版引导(磁盘内 GRUB/U-Boot) |
|---|---|---|
| 引导环节 | QEMU 直接装内存跳入口,无引导加载器 | 固件 → 引导加载器 → 内核,链路完整 |
| 内核来源 | 主机文件系统任意指定,换内核只改参数 | 磁盘分区内的固定镜像,换内核要更新引导配置 |
| 启动速度 | 少一层引导,适合反复重启的调试循环 | 多一段引导加载器耗时 |
| 适用场景 | 内核开发、启动流程分析、CI 里跑固定内核 | 模拟真实机器、验证引导设施、长期运行的虚拟机 |
| 主要坑 | cmdline 与模块时序需自己兜,本次 mask 即一例 | 引导配置出错时排查链路更长 |
| 与磁盘关系 | 磁盘可无引导扇区,只承担根文件系统角色 | 磁盘必须含可工作的引导设施 |
表里第三行的代价在本次故障里体现得很充分:直挂方式把引导决策收回到命令行,灵活度高,但 initramfs 与 systemd 的行为时序和完整引导不同,原本被引导流程掩盖的问题会直接暴露。反过来,当你要验证的恰恰是「一台真实机器从上电到登录的全流程」时,引导加载器本身就是被测对象,直挂方式会把它跳过,结论不再具备代表性,那时应改用 QEMU 跑 RISC-V Linux:OpenSBI、内核与 rootfs 的完整发行版启动 的路径。
另外两个实操要点。第一,-append 的 cmdline 是一次性的,只对本次启动生效,调参成本极低,适合做多组对照实验;想固化就写进启动脚本或磁盘引导配置。第二,mask 是绕过而不是修复:本篇屏蔽 boot-efi.mount 成立的前提是虚拟平台不依赖 EFI 分区,换个需要该分区的场景,正确方向是把 nls 模块补进 initramfs(如 initramfs-tools 配置里纳入 nls_iso8859-1 后重新生成),而不是照搬 mask。
实验环境
- 机器/系统:云主机 2C8G / Ubuntu 22.04.5
- 工具版本:QEMU(qemu-system-riscv64),客户机内核 vmlinuz-7.0.0-38-generic 与配套 initrd.img 均为 RISC-V 版本,固件 OpenSBI fw_jump.bin
- 日期:2026-11-08
- 完整命令序列:首跑
run-q017.sh(无 mask,进入 emergency mode);修复后由boot-exp.exp经 expect 在串口自动登录、执行uname探针并poweroff,三次运行的完整串口输出分别留存为 run1 至 run3 实录。
代码获取:本篇实验的启动脚本、expect 自动化与实录说明见公开仓库 derekzhuo/geek-doc.cn 的
experiments/qemu-direct-kernel-initramfs目录。按 README 准备镜像与内核文件后执行./boot.exp,即可复现从直挂引导、串口登录到uname取证的完整过程。
这一篇在知识体系里的位置
本篇是 SoC 与 QEMU 入门路线里的启动机制篇,回答「内核怎么被跑起来」。想看引导设施齐全的完整路径,对照阅读同批的 QEMU 跑 RISC-V Linux:OpenSBI、内核与 rootfs 的完整发行版启动;本篇每个 qemu 参数的逐字释义与可选值,在 QEMU 命令行参数逐读:一份 qemu-system 启动参数的注释手册 里展开。串口是直挂方式下唯一的控制台,-nographic 与 console=ttyS0 的接线原理见 QEMU 串口与控制台接线:nographic、serial 与 monitor;再往后做内核调试时,-kernel 配合 -s -S 与 gdbstub 可以把断点打在内核入口,那是 QEMU GDB 调试入门:-s -S 与 gdb 连接第一个断点 的主题。
总结
QEMU 直接引导内核用 -kernel 把内核镜像装入客户机内存、-initrd 挂入承担早期根文件系统职责的 initramfs、-append 当面递交 cmdline,三件套跳过引导加载器完成交接,适合内核调试与反复重启的实验场景。本次首跑的故障链是:FAT 驱动缺 iso8859-1 字符集导致 boot-efi.mount 失败,依赖失败拖垮 local-fs.target,systemd 降级进入 emergency mode;修复手段是在 cmdline 上加 systemd.mask=boot-efi.mount,让问题单元在虚拟平台上不被发起,复跑即干净走到登录提示符,客户机内 uname -m 取证为 riscv64。记住 mask 只是对症绕过,真实机器需要 EFI 分区时仍应补齐 nls 模块;直挂方式与完整发行版引导不是替代关系,而是分别服务于「调内核」与「验整机」两类目标。