early_param 内核启动参数实现原理
基于上节内核下PCIE的扫描流程内容, 其中有个
early_param关键字, 对于启动内核时能够使用诸多参数, 本人早就非常好奇, 趁着这个机会学习一下.
early_param
相关代码:
1 |
|
可见early_param只是给内核用的, modules中不处理.
换句话说, 宏展开之后, 也就是放了一个名字以__setup_str_起始的静态指针常量, 单字节对齐, 置于.init.rodata的section中, 值为参数的名字. 又定义了一个存在于.init.setup中, 以long长度对齐的obs_kernel_param类型结构提, 名字以__setup_开头, 成员为刚刚的静态指针常量, 及函数名等.
那么关键点就是这个.init.rodata区域的值是怎么用作参数的呢?
已经获得的信息:
- 两个声明的
section分别是.init.rodata和.init.setup - 结构体类型为
obs_kernel_param. - 变量名以
__setup_str_起始.
先简单grep一下相关的变量名发现没找到:
1 | [mxd@5 linux-4.19-loongson]$ grep __setup_str_ -rnI arch/loongarch/ |
再cscope一下结构体obs_kernel_param:
1 | Cscope tag: obs_kernel_param |
可见, 第一个是结构提定义, 第二个和第三个是定义了一个全局数组, 第四个和第五个是在函数中调用. 总之在init/main.c中, 进去看看:
1 | extern const struct obs_kernel_param __setup_start[], __setup_end[]; |
其中obsolete_checksetup函数是在参数未知情况下的注册, 本文暂不讨论.
在do_early_param中, 就是从先前声明的__setup_start和__setup_end中取值, 那么这俩值又是从哪来的呢?
通过cscope并没有找到相应的定义, 这时只有两种可能, 一是该变量由链接脚本提供, 二是在非C源文件中定义, 比如汇编代码. 但无论如何, 这两者均在arch目录下. grep一下看看:
1 | [mxd@5 linux-4.19-loongson]$ grep __setup_start -rn arch/loongarch/ |
处理一下核心信息:
1 | . = ALIGN(16); |
也就是在.init.setup的section中的内容, 也就是前面得到的线索1:
- 两个声明的
section分别是.init.rodata和.init.setup
继续分析代码, 从__setup_start开始遍历, 这里也有两种情况:
- 如果成员是
early类型, 且成员中的变量名与传入的参数名一致 - 如果参数名是
console, 且成员中的变量名是earlycon时.
这时则会调用该成员的setup_func函数.
举例
以earlycon为例:
1 | /* early_param wrapper for setup_earlycon() */ |
根据前面对宏和代码的分析, 上面代码注册了一个obs_kernel_param类型的变量存在.init.setup中: __setup_param_setup_earlycon, 及一个存在于.init.rodata中的字符数组:__setup_str_param_setup_earlycon. 宏展开为:
1 | static const char __setup_str_param_setup_earlycon[] __section(.init.rodata) |
所以当do_early_param函数中传入的参数是"earlycon"时, 其对应的early类型已经是1, 满足情景1:
- 如果成员是
early类型, 且成员中的变量名与传入的参数名一致- 如果参数名是
console, 且成员中的变量名是earlycon时.
所以会执行setup_func函数, 也就是param_setup_earlycon函数, 执行的参数是do_early_param函数中传入的val, 这里假设上述代码中第一个分支便成立, 则执行earlycon_acpi_spcr_enable = true;return 0; 这其中的earlycon_acpi_spcr_enable是一个全局变量, 将在其他驱动中被调用, 此文不继续展开.
至此, 内核启动时, 若传入earlycon参数, 将会由此继续注册设备并生效.
参数格式
到现在, 我们已经明白了参数是如何注册和解析的, 那么参数的形式是怎么样的, 还并不清楚. 我们从新回到do_early_param函数. 通过cscope查看其调用过程:
1 | void __init parse_early_options(char *cmdline) |
除此之外还有一个
early_platform_driver_register_all函数会调用parse_early_options, 经过查看这是特殊行为, 不继续展开了.
所以核心代码是从boot_command_line复制一份到tmp_cmdline并解析. 而boot_command_line通过early_memremap_ro(fw_arg1, COMMAND_LINE_SIZE)而来, 细追了一下发现是从一个物理地址传来, 也就是bootloader传递进来的, 暂不讨论.
解析过程其中涉及许多字符串, 锁, 及中断的解析, 不详细在本文展开, 在此仅作简单说明, 大致解析的格式按照如下顺序进行:
stateDiagram-v2
state 传入cmdline给parse_early_options {
parse_early_options --> parse_args
parse_args --> skip_spaces去除空格
skip_spaces去除空格 --> next_arg处理cmdline
去除双引号和空格,\n在'='位置处增加'\0'作为分隔符区分参数名和参数值,\n在空格处标记为参数值的结束位置,\n举例earlycon=uart,mmio,0x1fe001e0将会\n被拆分为earlycon\0和uart,mmio,0x1fe001e0\n前者作为参数名,后者作为参数值\n而类似single的参数将只有参数名,\n没有参数值 --> irqs_disabled关闭中断
irqs_disabled关闭中断 --> parse_one
parse_one --> do_early_param传入对应的参数和值,\n进而执行相应的setup_func函数,\n如第一章内容
--
next_arg处理cmdline --> 去除双引号和空格,\n在'='位置处增加'\0'作为分隔符区分参数名和参数值,\n在空格处标记为参数值的结束位置,\n举例earlycon=uart,mmio,0x1fe001e0将会\n被拆分为earlycon\0和uart,mmio,0x1fe001e0\n前者作为参数名,后者作为参数值\n而类似single的参数将只有参数名,\n没有参数值
}
所以参数的格式将为:arg1=xxx或arg, 千万不可使用空格随意拆分.
日常使用
除了根据代码学习增加参数的方式, 还要会看已经传递的参数:
1 | mxd@mxd:~$ cat /proc/cmdline |
详细的功能即可在代码实现层进一步了解


