基于上节内核下PCIE的扫描流程内容, 其中有个early_param关键字, 对于启动内核时能够使用诸多参数, 本人早就非常好奇, 趁着这个机会学习一下.

early_param

相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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.rodatasection中, 值为参数的名字. 又定义了一个存在于.init.setup中, 以long长度对齐的obs_kernel_param类型结构提, 名字以__setup_开头, 成员为刚刚的静态指针常量, 及函数名等.

那么关键点就是这个.init.rodata区域的值是怎么用作参数的呢?

已经获得的信息:

  1. 两个声明的section分别是.init.rodata.init.setup
  2. 结构体类型为obs_kernel_param.
  3. 变量名以__setup_str_起始.

先简单grep一下相关的变量名发现没找到:

1
2
[mxd@5 linux-4.19-loongson]$ grep __setup_str_ -rnI arch/loongarch/
[mxd@5 linux-4.19-loongson]$

cscope一下结构体obs_kernel_param:

1
2
3
4
5
6
7
8
9
10
11
12
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中, 进去看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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一下看看:

1
2
3
[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 = .;

处理一下核心信息:

1
2
3
4
. = ALIGN(16);
__setup_start = .;
KEEP(*(.init.setup))
__setup_end = .;

也就是在.init.setupsection中的内容, 也就是前面得到的线索1:

  1. 两个声明的section分别是.init.rodata.init.setup

继续分析代码, 从__setup_start开始遍历, 这里也有两种情况:

  1. 如果成员是early类型, 且成员中的变量名与传入的参数名一致
  2. 如果参数名是console, 且成员中的变量名是earlycon时.

这时则会调用该成员的setup_func函数.

举例

earlycon为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 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. 宏展开为:

1
2
3
4
5
6
7
8
9
10
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:

  1. 如果成员是early类型, 且成员中的变量名与传入的参数名一致
  2. 如果参数名是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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=xxxarg, 千万不可使用空格随意拆分.

日常使用

除了根据代码学习增加参数的方式, 还要会看已经传递的参数:

1
2
mxd@mxd:~$ cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-linux root=UUID=a67aaec3-baec-4239-af30-f0a8ed5346ed rw loglevel=3 quiet

详细的功能即可在代码实现层进一步了解

参考文献

  1. 内核源码