3、Bootloader阶段参数传递:GRUB2配置与内核参数注入方法

Bootloader 是系统启动过程中第一个获得硬件控制权的软件,负责加载内核映像并传递启动参数。在 Linux 生态中,GRUB2(GRand Unified Bootloader version 2)是事实上的标准引导程序。理解 GRUB2 如何构造和传递内核参数,是调试 systemd 初始化行为的基础。

3.1 GRUB2 参数传递链路

从 GRUB2 菜单选择到内核最终接收到参数,经历以下关键阶段:

  1. GRUB2 环境变量展开:grub.cfg 中的 linux 命令后的字符串会经过变量替换(如 ${root}、$cmdline)。
  2. 内核命令行构造:GRUB2 将处理后的字符串拼接成一条完整的命令行,存入 linux_cmdline 结构。
  3. Boot Protocol 加载:GRUB2 根据 Linux Boot Protocol(x86 为 setup_header)将命令行地址和长度写入内核预定义的内存位置。
  4. 内核早期解析:内核入口代码(arch/x86/boot/header.S)读取该内存区域,提取参数字符串供后续初始化使用。
关键点:GRUB2 传递的参数是纯文本字符串,内核在启动早期按空格分割为 argv 数组。参数顺序不影响语义,但重复参数可能导致覆盖或叠加行为。
GRUB2 参数传递链路 grub.cfg 变量展开 命令行构造 Boot Protocol 内核早期解析 systemd 接收 说明:GRUB2 在 linux 命令后拼接的字符串,经过变量展开后,通过 Boot Protocol 写入内核 setup_header。 内核在解压缩前即完成参数解析,systemd 作为 PID 1 继承整个 cmdline。

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 移除,如需传递包含空格的参数(如 systemd.mask=multi-user.target),请使用转义或确保参数无空格。
2. 重复参数(如两个 console=)可能导致内核只识别最后一个,除非驱动明确支持多实例。
3. 修改 /etc/default/grub 后必须执行 grub2-mkconfig,否则修改不会生效。
四种内核参数注入方法对比 方法一 /etc/default/grub 方法二 GRUB2 按 e 临时编辑 方法三 40_custom 自定义菜单 方法四 PXE append 参数 属性对比 持久化 ✔ 永久 ✘ 仅本次 ✔ 永久 取决于配置 适用环境 本地磁盘 任何 GRUB2 环境 本地磁盘 PXE/网络引导 调试友好度 中等(需重启) 高(即时生效) 高(独立菜单项) 中等 风险等级 低 低(临时) 低 中(影响批量机器) 推荐:日常调试使用方法二(临时),确认参数正确后再使用方法一(持久化)。

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 初始化问题的前提。