3、Bootloader阶段参数传递:GRUB2配置与内核参数注入方法
Bootloader 是系统启动过程中第一个获得硬件控制权的软件,负责加载内核映像并传递启动参数。在 Linux 生态中,GRUB2(GRand Unified Bootloader version 2)是事实上的标准引导程序。理解 GRUB2 如何构造和传递内核参数,是调试 systemd 初始化行为的基础。
3.1 GRUB2 参数传递链路
从 GRUB2 菜单选择到内核最终接收到参数,经历以下关键阶段:
- GRUB2 环境变量展开:
grub.cfg中的linux命令后的字符串会经过变量替换(如${root}、$cmdline)。 - 内核命令行构造:GRUB2 将处理后的字符串拼接成一条完整的命令行,存入
linux_cmdline结构。 - Boot Protocol 加载:GRUB2 根据 Linux Boot Protocol(x86 为 setup_header)将命令行地址和长度写入内核预定义的内存位置。
- 内核早期解析:内核入口代码(
arch/x86/boot/header.S)读取该内存区域,提取参数字符串供后续初始化使用。
关键点:GRUB2 传递的参数是纯文本字符串,内核在启动早期按空格分割为 argv 数组。参数顺序不影响语义,但重复参数可能导致覆盖或叠加行为。
3.2 GRUB2 配置文件结构
GRUB2 的主配置文件通常位于 /boot/grub/grub.cfg(或 /boot/grub2/grub.cfg)。该文件由 grub2-mkconfig 工具自动生成,但用户可通过 /etc/default/grub 和 /etc/grub.d/ 目录下的脚本控制其内容。
| 文件/目录 | 作用 | 典型内容 |
|---|---|---|
/etc/default/grub |
全局默认配置,定义内核参数模板 | GRUB_CMDLINE_LINUX、GRUB_CMDLINE_LINUX_DEFAULT |
/etc/grub.d/10_linux |
自动检测内核并生成菜单项 | 读取 GRUB_CMDLINE_LINUX 变量 |
/etc/grub.d/40_custom |
用户自定义菜单项入口 | 手动编写 menuentry 块 |
/boot/grub/grub.cfg |
最终生成的引导配置文件 | 包含 linux 命令及完整参数 |
3.3 内核参数注入方法
根据注入时机和持久化程度,内核参数注入分为以下四种方法:
方法一:通过 /etc/default/grub 持久化注入
这是最常用的方法,修改后执行 grub2-mkconfig -o /boot/grub/grub.cfg 生效。
# 编辑 /etc/default/grub
GRUB_CMDLINE_LINUX="console=tty0 console=ttyS0,115200n8"
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
# 重新生成 grub.cfg
grub2-mkconfig -o /boot/grub/grub.cfg
变量区别:
GRUB_CMDLINE_LINUX 对所有菜单项生效(包括正常模式和恢复模式);GRUB_CMDLINE_LINUX_DEFAULT 仅对默认菜单项生效。两者最终会拼接在一起。
方法二:GRUB2 启动时临时编辑
在 GRUB2 菜单界面按 e 键进入编辑模式,直接修改 linux 行。此修改仅本次启动有效,重启后恢复。
# 按 e 后看到的典型 linux 行
linux /vmlinuz-5.14.0-362.el9.x86_64 root=/dev/mapper/rl-root ro
console=tty0 console=ttyS0,115200n8 crashkernel=auto
rd.lvm.lv=rl/root rd.lvm.lv=rl/swap
# 在行尾追加参数,例如:
systemd.log_level=debug systemd.log_target=console
方法三:通过 /etc/grub.d/40_custom 添加专用菜单项
适合保留一个带调试参数的独立启动项,不影响默认启动。
#!/bin/sh
exec tail -n +3 $0
# 在 /etc/grub.d/40_custom 中添加
menuentry 'Linux with debug (custom)' --class red --class gnu-linux {
set root=(hd0,msdos1)
linux /vmlinuz-5.14.0-362.el9.x86_64 root=/dev/mapper/rl-root ro
console=tty0 systemd.log_level=debug systemd.journald.forward_to_console=1
initrd /initramfs-5.14.0-362.el9.x86_64.img
}
方法四:通过内核 append 参数(PXE/网络引导)
在 PXE 配置或 iPXE 脚本中,通过 append 指令传递参数。
# pxelinux.cfg/default 示例
LABEL linux
KERNEL vmlinuz
APPEND root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs
ip=dhcp systemd.unit=emergency.target
3.4 常用调试内核参数清单
| 参数 | 作用 | 适用场景 |
|---|---|---|
systemd.log_level=debug |
将 systemd 日志级别设为 debug | 追踪服务启动顺序、单元依赖问题 |
systemd.log_target=console |
将日志输出到控制台(而非 journal) | 无磁盘或 journald 未启动时的调试 |
systemd.journald.forward_to_console=1 |
将 journald 日志同时转发到控制台 | 需要同时查看内核和用户态日志 |
systemd.unit=emergency.target |
直接启动到 emergency 模式 | 修复损坏的 rootfs 或 fstab |
systemd.mask=network.service |
屏蔽指定单元(本次启动) | 隔离网络服务故障 |
rd.debug |
开启 initramfs 阶段的 shell 调试 | 调试 dracut 或 initramfs 问题 |
console=ttyS0,115200n8 |
启用串口控制台 | 无头服务器、虚拟机串口调试 |
3.5 参数注入验证与调试
注入参数后,可通过以下方式验证参数是否被正确传递:
- 查看
/proc/cmdline:内核启动后,该文件包含最终生效的完整命令行。 - 检查 GRUB2 环境:在 GRUB2 命令行(按
c键)执行cat $cmdline查看当前构造的命令行。 - 使用
systemd-analyze:systemd-analyze blame可查看各服务启动耗时,辅助验证调试参数效果。
# 验证内核参数
cat /proc/cmdline
# 输出示例:
# BOOT_IMAGE=/vmlinuz-5.14.0-362.el9.x86_64 root=/dev/mapper/rl-root ro
# console=tty0 console=ttyS0,115200n8 systemd.log_level=debug
# 在 GRUB2 命令行中检查
grub> cat $cmdline
# 输出当前构造的命令行字符串
常见陷阱:
1. 参数中的引号会被 GRUB2 移除,如需传递包含空格的参数(如
2. 重复参数(如两个
3. 修改
1. 参数中的引号会被 GRUB2 移除,如需传递包含空格的参数(如
systemd.mask=multi-user.target),请使用转义或确保参数无空格。2. 重复参数(如两个
console=)可能导致内核只识别最后一个,除非驱动明确支持多实例。3. 修改
/etc/default/grub 后必须执行 grub2-mkconfig,否则修改不会生效。
3.6 最佳实践与避坑指南
- 先临时后持久:在 GRUB2 菜单按
e编辑验证参数有效后,再写入/etc/default/grub。 - 保留回退菜单项:在
40_custom中添加调试菜单项时,保留一个不带调试参数的正常启动项作为回退。 - 注意参数顺序:虽然内核参数顺序通常不敏感,但
console=参数中第一个有效设备会成为主控制台。 - 避免敏感信息:内核参数可通过
/proc/cmdline被所有用户读取,不要在其中传递密码或密钥。 - 使用
systemd-analyze验证:注入systemd.log_level=debug后,使用journalctl -b -p debug确认日志级别已生效。
核心要点:GRUB2 通过
linux 命令将参数字符串传递给内核,内核在启动早期解析后存入 /proc/cmdline。systemd 作为 PID 1 继承该命令行,并根据参数调整自身行为。掌握临时编辑和持久化配置两种注入方式,是高效调试 systemd 初始化问题的前提。