基于上节内核下PCIE的扫描流程内容, 其中有个
early_param
关键字, 对于启动内核时能够使用诸多参数, 本人早就非常好奇, 趁着这个机会学习一下.
early_param
相关代码:
C
#define __initconst __section(.init.rodata)
/*
* NOTE: fn is as per module_param, not __setup!
* Emits warning if fn returns non-zero.
*/
#define early_param(str, fn) \
__setup_param(str, fn, fn, 1)
#ifndef MODULE
struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};
/*
* Only for really core code. See moduleparam.h for the normal way.
*
* Force the alignment so the compiler doesn't space elements of the
* obs_kernel_param "array" too far apart in .init.setup.
*/
#define __setup_param(str, unique_id, fn, early) \
static const char __setup_str_##unique_id[] __initconst \
__aligned(1) = str; \
static struct obs_kernel_param __setup_##unique_id \
__used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
#else /* MODULE */
#define __setup_param(str, unique_id, fn) /* nothing */
#endif
可见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
一下相关的变量名发现没找到:
bash
[mxd@5 linux-4.19-loongson]$ grep __setup_str_ -rnI arch/loongarch/
[mxd@5 linux-4.19-loongson]$
再cscope
一下结构体obs_kernel_param
:
html
Cscope tag: obs_kernel_param
# line filename / context / line
1 241 include/linux/init.h <<GLOBAL>>
struct obs_kernel_param {
2 175 init/main.c <<GLOBAL>>
extern const struct obs_kernel_param __setup_start[], __setup_end[];
3 256 include/linux/init.h <<__setup_param>>
static struct obs_kernel_param __setup_##unique_id \
4 179 init/main.c <<obsolete_checksetup>>
const struct obs_kernel_param *p;
5 448 init/main.c <<do_early_param>>
const struct obs_kernel_param *p;
可见, 第一个是结构提定义, 第二个和第三个是定义了一个全局数组, 第四个和第五个是在函数中调用. 总之在init/main.c
中, 进去看看:
C
extern const struct obs_kernel_param __setup_start[], __setup_end[];
/* Check for early params. */
static int __init do_early_param(char *param, char *val,
const char *unused, void *arg)
{
const struct obs_kernel_param *p;
for (p = __setup_start; p < __setup_end; p++) {
if ((p->early && parameq(param, p->str)) ||
(strcmp(param, "console") == 0 &&
strcmp(p->str, "earlycon") == 0)
) {
if (p->setup_func(val) != 0)
pr_warn("Malformed early option '%s'\n", param);
}
}
/* We accept everything at this stage. */
return 0;
}
其中obsolete_checksetup
函数是在参数未知情况下的注册, 本文暂不讨论.
在do_early_param
中, 就是从先前声明的__setup_start
和__setup_end
中取值, 那么这俩值又是从哪来的呢?
通过cscope
并没有找到相应的定义, 这时只有两种可能, 一是该变量由链接脚本提供, 二是在非C
源文件中定义, 比如汇编代码. 但无论如何, 这两者均在arch
目录下. grep
一下看看:
bash
[mxd@5 linux-4.19-loongson]$ grep __setup_start -rn arch/loongarch/
arch/loongarch/kernel/vmlinux.lds:60: .init.data : AT(ADDR(.init.data) - 0) { KEEP(*(SORT(___kentry+*))) *(.init.data init.data.*) . = ALIGN(16); __setup_start = .; KEEP(*(.init.setup)) __setup_end = .;
处理一下核心信息:
C
. = ALIGN(16);
__setup_start = .;
KEEP(*(.init.setup))
__setup_end = .;
也就是在.init.setup
的section
中的内容, 也就是前面得到的线索1
:
- 两个声明的
section
分别是.init.rodata
和.init.setup
继续分析代码, 从__setup_start
开始遍历, 这里也有两种情况:
- 如果成员是
early
类型, 且成员中的变量名与传入的参数名一致 - 如果参数名是
console
, 且成员中的变量名是earlycon
时.
这时则会调用该成员的setup_func
函数.
举例
以earlycon
为例:
C
/* early_param wrapper for setup_earlycon() */
static int __init param_setup_earlycon(char *buf)
{
int err;
/* Just 'earlycon' is a valid param for devicetree and ACPI SPCR. */
if (!buf || !buf[0]) {
if (IS_ENABLED(CONFIG_ACPI_SPCR_TABLE)) {
earlycon_acpi_spcr_enable = true;
return 0;
} else if (!buf) {
return early_init_dt_scan_chosen_stdout();
}
}
err = setup_earlycon(buf);
if (err == -ENOENT || err == -EALREADY)
return 0;
return err;
}
early_param("earlycon", param_setup_earlycon);
根据前面对宏和代码的分析, 上面代码注册了一个obs_kernel_param
类型的变量存在.init.setup
中: __setup_param_setup_earlycon
, 及一个存在于.init.rodata
中的字符数组:__setup_str_param_setup_earlycon
. 宏展开为:
C
static const char __setup_str_param_setup_earlycon[] __section(.init.rodata)
__aligned(1) = "earlycon";
static struct obs_kernel_param __setup_param_setup_earlycon __attribute__((__used__))
__section(.init.setup) __attribute__((aligned((sizeof(long)))))
= {
.str = __setup_str_param_setup_earlycon,
.setup_func = param_setup_earlycon,
.early = 1,
};
所以当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
查看其调用过程:
C
void __init parse_early_options(char *cmdline)
{
parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
do_early_param);
}
/* Arch code calls this early on, or if not, just before other parsing. */
void __init parse_early_param(void)
{
static int done __initdata;
static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;
if (done)
return;
/* All fall through to do_early_param. */
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
parse_early_options(tmp_cmdline);
done = 1;
}
除此之外还有一个
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传递进来的, 暂不讨论.
解析过程其中涉及许多字符串, 锁, 及中断的解析, 不详细在本文展开, 在此仅作简单说明, 大致解析的格式按照如下顺序进行:
所以参数的格式将为:arg1=xxx
或arg
, 千万不可使用空格随意拆分.
日常使用
除了根据代码学习增加参数的方式, 还要会看已经传递的参数:
bash
mxd@mxd:~$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-linux root=UUID=a67aaec3-baec-4239-af30-f0a8ed5346ed rw loglevel=3 quiet
详细的功能即可在代码实现层进一步了解