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 从引导加载程序传递到内核,最终被两个全局变量存储的完整路径:
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 问题的第一步。