2. 启动参数结构:内核参数、initrd与dtb传递机制、UEFI与BIOS差异

启动参数这事儿,说白了就是操作系统在“睁眼”之前,你塞给它的最后一张小纸条。纸条写对了,系统一路绿灯;写错了,直接黑屏或者 kernel panic。我这些年排查过的启动故障,少说有一半都能追溯到参数传递环节。

这一节我们就把启动参数的骨架拆开看。你搞懂了内核参数、initrd、dtb 是怎么传进去的,以及 UEFI 和 BIOS 在这件事上到底有什么不同,后面遇到问题才能快速定位。

2.1 内核参数:console、root、init 三剑客

内核参数是启动时传给 Linux 内核的字符串,格式通常是 key=value 或者单纯的 flag。我习惯把它们分成三类:控制台、根文件系统、初始化程序。

参数 作用 典型值 我踩过的坑
console 指定内核日志输出到哪里 ttyS0,115200 / tty0 串口线没接,console 配了也白配
root 指定根文件系统设备 /dev/sda1 / UUID=xxx 磁盘顺序变了,root 指向了空盘
init 指定第一个用户态进程 /sbin/init / /bin/bash init 路径写错,系统直接挂起

console 参数的一个细节:如果你同时指定了 console=tty0 console=ttyS0,115200,内核会把日志同时输出到两个设备。但最后一个 console 参数会成为 /dev/console 的默认目标。我遇到过有人把顺序搞反,结果串口日志全无,还以为硬件坏了。

举个例子,一个典型的 x86 服务器启动参数长这样:

console=tty0 console=ttyS0,115200n8 root=UUID=3a6b8c9d-... ro quiet

这里 ro 表示以只读方式挂载根文件系统,quiet 是减少内核日志输出。嗯,调试阶段我一般会去掉 quiet,甚至加上 earlyprintk 来捕获早期崩溃信息。

2.2 initrd 与 dtb:内核的“助跑器”和“地图”

initrd(Initial RAM Disk)是一个临时根文件系统,加载到内存里让内核先跑起来。为什么需要它?因为内核自己不认识磁盘文件系统,得靠 initrd 里的驱动去加载真正的根分区。

dtb(Device Tree Blob)则是嵌入式领域的“硬件地图”。ARM 或 RISC-V 平台不像 x86 那样能自动枚举设备,必须靠 dtb 告诉内核:CPU 有几个核、内存地址范围、外设挂在哪里。

我个人习惯:在调试阶段,我会把 initrd 里的 /init 脚本改一改,加几行 echo 输出。这样能确认 initrd 是否被正确加载,以及内核参数是否传到了用户态。

传递机制其实不复杂。以 UEFI 系统为例:

  1. Bootloader(如 GRUB)读取配置文件,拼接出完整的命令行字符串。
  2. Bootloader 把 initrd 文件加载到内存,把它的起始地址和大小传给内核。
  3. 对于 dtb,要么由 Bootloader 从磁盘加载,要么由 UEFI 固件提供(通过 EFI_DTB_TABLE)。
  4. 内核启动时,解析命令行,挂载 initrd,执行 /init。

我曾经遇到过一个案例:某 ARM 服务器换了新网卡,但内核始终认不出来。折腾了半天,发现是 dtb 文件没更新,固件还在用旧的设备树。新网卡的寄存器地址根本不在 dtb 里,内核自然找不到它。

2.3 UEFI 与 BIOS 启动参数差异

BIOS 和 UEFI 在启动参数传递上,思路完全不同。我整理了一张对比表,你一看就明白:

对比项 BIOS (Legacy) UEFI
参数传递方式 通过 bootloader(如 GRUB Legacy)拼接字符串 通过 EFI_LOADED_IMAGE_PROTOCOL 传递命令行
initrd 加载 bootloader 直接读取磁盘扇区 bootloader 调用 UEFI 文件系统协议读取
dtb 来源 通常由 bootloader 从固定分区加载 可由 UEFI 固件提供,也可由 bootloader 覆盖
参数长度限制 通常 256 字节以内(GRUB Legacy 限制) 可达 4096 字节甚至更长
调试难度 参数写错只能看屏幕或串口 可通过 UEFI Shell 或 efibootmgr 查看

注意:UEFI 下如果使用 efibootmgr 设置启动参数,参数是存储在 NVRAM 里的。如果 NVRAM 损坏或空间不足,启动参数可能丢失或截断。我见过一台机器因为 NVRAM 满了,导致内核参数被截断,根文件系统挂载失败。

UEFI 还有一个好处:它支持 EFI_STUB 内核。也就是说,内核本身可以作为一个 UEFI 应用程序直接启动,不需要 GRUB 这类 bootloader。这时候启动参数通过 UEFI 的 LoadOptions 传递。我在调试嵌入式 UEFI 设备时经常用这个特性,少了一层 bootloader,问题定位更直接。

2.4 参数传递流程图

下面这张图展示了从 Bootloader 到内核的参数传递全链路。我建议你把它存下来,排查问题时对着看,思路会清晰很多。

启动参数传递流程 Bootloader (GRUB/uboot) 内核参数字符串 initrd 镜像 设备树 (dtb) Linux 内核 用户态 /init 进程 说明:UEFI 下参数通过 LoadOptions 传递,BIOS 下通过 bootloader 命令行拼接。 dtb 在 UEFI 环境中可由固件提供,也可由 bootloader 覆盖。

2.5 实战:如何验证参数是否传对

排查启动参数问题,我一般按这个顺序来:

  • 看内核日志:启动后执行 dmesg | grep "Command line",确认内核实际收到的参数。如果跟你配置的不一致,说明 bootloader 或固件做了手脚。
  • 检查 /proc/cmdline:这个文件就是内核最终使用的命令行。我见过有人改了 GRUB 配置但忘了更新引导记录,结果 /proc/cmdline 还是旧的。
  • UEFI 下用 efibootmgr:efibootmgr -v 可以查看当前启动项的完整参数。如果参数里有乱码或截断,基本就是 NVRAM 的问题。
  • BIOS 下看 GRUB 菜单:启动时按 e 键编辑启动项,确认参数显示完整。我曾经遇到过 GRUB 配置文件里参数写对了,但菜单显示时被换行符截断,导致后半段参数丢失。

一个小技巧:如果你不确定 dtb 是否被正确加载,可以在内核参数里加上 dtb_debug(某些内核支持),或者直接 cat /sys/firmware/devicetree/base/model 查看设备树里的 model 字段。如果文件不存在或内容不对,说明 dtb 没传进来。

嗯,这一节的内容就到这里。参数传递看起来是小事,但一旦出问题,排查起来往往很绕。你把这些结构理清了,后面遇到“内核没输出”、“根文件系统挂不上”、“设备认不出”这类故障,就能快速判断是参数的问题,还是硬件的问题。