4. cmdline 存储位置:内核中的 boot_command_line 全局变量与 saved_command_line

当引导加载程序(如 GRUB、U-Boot)将内核镜像加载到内存并跳转到入口点之前,它会在指定的内存位置放置一个以 null 结尾的字符串,即原始内核命令行。内核在启动初期会立即捕获并保存这份参数,将其存储在两个关键的全局变量中:boot_command_line 和 saved_command_line。理解这两个变量的角色、生命周期和差异,是掌握内核参数解析流程的基础。

4.1 两个全局变量的定义与角色

在 init/main.c 中,内核定义了以下两个全局字符数组:

/* 位于 init/main.c */
#define COMMAND_LINE_SIZE 2048

static char __initdata boot_command_line[COMMAND_LINE_SIZE] = { 0 };
static char *saved_command_line;
变量名 类型 存储区域 生命周期 核心用途
boot_command_line static char[] .init.data 段 仅在初始化阶段有效,内核启动完成后可被释放 保存从引导加载程序接收的原始 cmdline 字符串,供早期解析使用
saved_command_line static char * 动态分配的内存(通过 kmemdup) 整个内核运行期间持久存在 保存一份永久副本,供 /proc/cmdline、调试接口、模块参数等后续使用
关键区别: boot_command_line 是“一次性”的原始缓冲区,位于可释放的 init 段;saved_command_line 是“永久”的动态副本,确保即使 init 段被回收,内核仍能访问完整的命令行字符串。

4.2 数据流:从引导加载程序到全局变量

下图展示了 cmdline 从引导加载程序传递到内核,最终被两个全局变量存储的完整路径:

cmdline 存储路径:从引导加载程序到内核全局变量 引导加载程序 (GRUB / U-Boot) 内核入口点 (start_kernel 之前) setup_arch() (架构相关,复制到 boot_command_line) start_kernel() (调用 parse_early_param) saved_command_line (kmemdup 永久副本) 说明 boot_command_line 位于 .init.data saved_command_line 由 kmemdup 从 boot_command_line 复制而来 生命周期覆盖整个内核运行期

4.3 初始化流程:setup_arch 与 start_kernel 中的赋值

内核在 init/main.c 的 start_kernel() 函数中完成对 saved_command_line 的初始化。核心代码如下:

asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;

    /* ... 早期初始化 ... */

    /* 从 boot_command_line 复制一份永久副本 */
    command_line = (char *)__va(__pa_symbol(boot_command_line));
    saved_command_line = kmemdup(command_line, strlen(command_line) + 1, GFP_KERNEL);

    /* 解析早期参数(early params) */
    parse_early_param();

    /* ... 后续初始化 ... */
}
流程要点:
  • setup_arch()(架构相关函数)负责将引导加载程序传递的 cmdline 复制到 boot_command_line 数组中。不同架构(ARM64、x86、RISC-V)的实现细节不同,但最终都写入这个全局数组。
  • start_kernel() 在调用 parse_early_param() 之前,通过 kmemdup 创建 saved_command_line。这确保了即使后续 free_initmem() 回收了 .init.data 段,内核仍然可以访问完整的命令行字符串。
  • parse_early_param() 直接扫描 boot_command_line,提取并执行所有标记为 __early_param 的早期参数回调。

4.4 生命周期与内存管理对比

特性 boot_command_line saved_command_line
内存来源 编译时分配的静态数组(.init.data 段) 运行时通过 kmemdup 从堆分配
大小限制 固定 COMMAND_LINE_SIZE(通常 2048 字节) 动态分配,与原始字符串长度一致
可修改性 初始化阶段可写,之后不应修改 内核运行期间可读,通常不修改(除非调试)
释放时机 内核完成初始化后,free_initmem() 回收整个 .init 段 永不释放(除非内核模块显式操作,但极少见)
访问接口 仅限早期初始化代码直接引用 通过 saved_command_line 指针全局访问
注意事项: 由于 boot_command_line 位于 .init.data 段,在内核启动后期调用 free_initmem() 后,该数组占用的内存会被回收并可能被其他用途覆盖。因此,任何需要在初始化完成后访问 cmdline 的代码,都必须使用 saved_command_line,而不是直接引用 boot_command_line。

4.5 实际应用:/proc/cmdline 与调试接口

用户空间最常见的 cmdline 访问接口是 /proc/cmdline。该文件的实现直接读取 saved_command_line:

/* fs/proc/cmdline.c */
static int __init proc_cmdline_init(void)
{
    struct proc_dir_entry *pde;

    pde = proc_create_single("cmdline", 0, NULL, cmdline_proc_show);
    return 0;
}

static int cmdline_proc_show(struct seq_file *m, void *v)
{
    seq_printf(m, "%s\n", saved_command_line);
    return 0;
}

这意味着:

  • 用户执行 cat /proc/cmdline 时,内核返回的是 saved_command_line 的内容,即一份完整的、未被截断或修改的原始命令行副本。
  • 调试内核时,可以通过 printk 直接打印 saved_command_line 来验证参数是否被正确传递:pr_info("Kernel cmdline: %s\n", saved_command_line);
  • 在嵌入式系统中,如果引导加载程序传递的 cmdline 超过 COMMAND_LINE_SIZE(2048 字节),boot_command_line 会被截断,但 saved_command_line 也会反映截断后的内容。此时应检查引导加载程序配置或增大 COMMAND_LINE_SIZE。

4.6 架构差异:ARM64 与 x86 的 cmdline 传递

不同 CPU 架构在将 cmdline 从引导加载程序传递到 boot_command_line 的方式上存在差异:

架构 传递方式 关键函数 特殊处理
x86 通过 boot_params 结构体中的 hdr.cmd_line_ptr 字段传递物理地址 setup_arch() → strncpy(boot_command_line, ...) 支持 CONFIG_CMDLINE_BOOL 和 CONFIG_CMDLINE_OVERRIDE 编译时内置参数
ARM64 通过设备树(DT)的 chosen 节点下的 bootargs 属性传递 setup_arch() → early_init_dt_scan_chosen() 支持 CONFIG_CMDLINE 作为后备;如果 DT 中未提供 bootargs,则使用内置参数
RISC-V 通过设备树或 ACPI 传递,与 ARM64 类似 setup_arch() → early_init_dt_scan_chosen() 同样支持 CONFIG_CMDLINE 编译时参数
调试建议: 在嵌入式开发中,如果发现内核启动参数未生效,首先检查 saved_command_line 的内容。可以通过早期 printk 或 JTAG 调试器在 start_kernel() 入口处读取该变量,确认引导加载程序是否正确传递了参数。这是定位 cmdline 问题的第一步。