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 |
详细的功能即可在代码实现层进一步了解