Linux kernel’s cmdline parse
kernel版本号:4.9.229
最近在工作中碰到了console的相关bug,于是抽时间学习了一下kernel的命令行解析原理。本文以4.9版本为例,粗略地介绍一下学习心得总结一下cmdline的解析机制。
cmdline往往由BootLoader和dts共同作用后得到。形式一般如下:
Kernel command line: console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait rw
kernel留出单独一块data段,即.ini.setup段
。
arch/arm/kernel/vmlinux.lds.S
==>
.init.data : {
INIT_DATA
INIT_SETUP(16)
INIT_CALLS
CON_INITCALL
SECURITY_INITCALL
INIT_RAM_FS
}
include/asm-generic/vmlinux.lds.hs
==>
#define INIT_SETUP(initsetup_align) \
. = ALIGN(initsetup_align); \
VMLINUX_SYMBOL(__setup_start) = .; \
*(.init.setup) \
VMLINUX_SYMBOL(__setup_end) = .;
init.setup段起止__setup_start
和__setup_end
。.init.setup段中存放的就是kernel通用参数和对应处理函数的映射表。
include/linux/init.h
中定义了obs_kernel_param
结构体,该结构体表征参数和对应处理函数,存放在.init.setup段中。
struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};
#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 }
#define __setup(str, fn) \
__setup_param(str, fn, fn, 0)
#define early_param(str, fn) \
__setup_param(str, fn, fn, 1)
我们重点关注console,在kernel/printk/printk.c中定义了
static int __init console_setup(char *str)
{
...
}
__setup("console=", console_setup);
所以我们将__setup("console=", console_setup);
带入展开后得到:
static struct obs_kernel_param __setup_console_setup
__used_section(.init.setup) __attribute__((aligned((sizeof(long)))) = {
.str = “console=”,
.setup_func = console_setup,
.early = 0
}
__setup_console_setup编译时就会链接到.init.setup段中,kernel运行时就会根据cmdline中的参数名与.init.setup段中obs_kernel_param的name对比。
匹配则调用console_setup来解析该参数,console_setup的参数就是cmdline中console的值。
接下来,当start_kernel函数执行,我们看是怎么一步一步地开始解析cmdline的。重点函数如下:
asmlinkage __visible void __init start_kernel(void)
{
...
/*
* 解析dtb中的bootargs并放置到boot_command_line中
* 并且会执行early param的解析
*/
setup_arch(&command_line);
...
setup_command_line(command_line); //简单的备份和拷贝boot_command_line
...
/*
* 执行early param的解析,由于setup_arch已经执行过一次,
* 所以这里不会重复执行,会直接return
*/
parse_early_param();
/*
* 执行普通的非early类型的cmdline的解析
*/
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
if (!IS_ERR_OR_NULL(after_dashes))
parse_args("Setting init args", after_dashes, NULL, 0, -1, -1,
NULL, set_init_arg);
...
}
我们依次看一下这4个关键函数。
setup_arch
该函数与具体架构相关,不同架构对应不同的setup_arch函数,本文我们以arm为例。
setup_arch中解析tags获取cmdline,拷贝到boot_command_line中。同时内存和页表也做了一些对应的初始化。
关键函数如下所示:
void __init setup_arch(char **cmdline_p)
{
...
setup_processor();
// 搜索dtb中的chosen并解析bootargs参数,并放到boot_command_line中
mdesc = setup_machine_fdt(__atags_pointer);
...
strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
*cmdline_p = cmd_line;
...
// 解析cmdline中的early param,从boot_command_line中获取bootargs参数
parse_early_param();
...
early_paging_init(mdesc);
...
paging_init(mdesc);
...
}
setup_machine_fdt函数的调用链如下:
setup_machine_fdt
early_init_dt_scan_nodes
of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
最终代码会调用到early_init_dt_scan_chosen,它的功能是扫描dts节点中的chosen,并解析对应的bootargs参数。
接下来调用parse_early_param,解析cmdline中的early param,从boot_command_line中获取bootargs参数。
void __init parse_early_param(void)
{
static int done __initdata;
static char tmp_cmdline[COMMAND_LINE_SIZE] __initdata;
if (done) //注意这个done flag,在一次启动过程中,该函数可能会被多次调用,但只会执行一次
return; //因为结尾将done设为1,再次执行时会直接return
strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
parse_early_options(tmp_cmdline); //解析动作会破坏tmp_cmdline中的数据,所以才有了前面一步copy动作
done = 1;
}
==>
void __init parse_early_options(char *cmdline)
{
parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
do_early_param);
}
parse_args的实现如下:
/* Args looks like "foo=bar,bar2 baz=fuz wiz". */
char *parse_args(const char *doing,
char *args,
const struct kernel_param *params,
unsigned num,
s16 min_level,
s16 max_level,
void *arg,
int (*unknown)(char *param, char *val,
const char *doing, void *arg))
{
char *param, *val, *err = NULL;
/* Chew leading spaces */
args = skip_spaces(args);
if (*args)
pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);
while (*args) {
int ret;
int irq_was_disabled;
args = next_arg(args, ¶m, &val);
/* Stop at -- */
if (!val && strcmp(param, "--") == 0)
return err ?: args;
irq_was_disabled = irqs_disabled();
ret = parse_one(param, val, doing, params, num,
min_level, max_level, arg, unknown);
if (irq_was_disabled && !irqs_disabled())
pr_warn("%s: option '%s' enabled irq's!\n",
doing, param);
switch (ret) {
case 0:
continue;
case -ENOENT:
pr_err("%s: Unknown parameter `%s'\n", doing, param);
break;
case -ENOSPC:
pr_err("%s: `%s' too large for parameter `%s'\n",
doing, val ?: "", param);
break;
default:
pr_err("%s: `%s' invalid for parameter `%s'\n",
doing, val ?: "", param);
break;
}
err = ERR_PTR(ret);
}
return err;
}
parse_args遍历cmdline字符串,按照空格切割获取参数,对所有参数调用next_arg获取(param, val)键值对。如console=ttymxc0,115200,则param=console,val=ttymxc0,115200。
随后调用parse_one对键值对进行处理。
static int parse_one(char *param,
char *val,
const char *doing,
const struct kernel_param *params,
unsigned num_params,
s16 min_level,
s16 max_level,
void *arg,
int (*handle_unknown)(char *param, char *val,
const char *doing, void *arg))
{
unsigned int i;
int err;
/* Find parameter */
for (i = 0; i < num_params; i++) {
if (parameq(param, params[i].name)) {
if (params[i].level < min_level
|| params[i].level > max_level)
return 0;
/* No one handled NULL, so do it here. */
if (!val &&
!(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG))
return -EINVAL;
pr_debug("handling %s with %p\n", param,
params[i].ops->set);
kernel_param_lock(params[i].mod);
param_check_unsafe(¶ms[i]);
err = params[i].ops->set(val, ¶ms[i]);
kernel_param_unlock(params[i].mod);
return err;
}
}
if (handle_unknown) {
pr_debug("doing %s: %s='%s'\n", doing, param, val);
return handle_unknown(param, val, doing, arg);
}
pr_debug("Unknown argument '%s'\n", param);
return -ENOENT;
}
由于从parse_early_options传入的num_params=0,所以parse_one是直接走的最后handle_unknown函数,即parse-early_options传入的do_early_param。
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)) || //early是否置为1
(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;
}
do_early_param会从__setup_start
到__setup_end
区域进行搜索,这个区域其实就是前面说的__section(.init.setup)
,并找到对应的obs_kernel_param结构数组,轮询其中定义的成员。
如果有obs_kernel_param的early为1,或cmdline中有console参数并且obs_kernel_param有earlycon参数,则会调用该obs_kernel_param的setup函数来解析参数。
do_early_param是为kernel中需要尽早配置的功能(如earlyprintk earlycon)做cmdline的解析。
而obs_kernel_param的early为0的,则延后执行解析,因为会再次调用到parse_args。
setup_command_line
调用setup_command_line将cmdline拷贝2份,放在saved_command_line
和static_command_line
中。
static void __init setup_command_line(char *command_line)
{
saved_command_line =
memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
initcall_command_line =
memblock_virt_alloc(strlen(boot_command_line) + 1, 0);
static_command_line = memblock_virt_alloc(strlen(command_line) + 1, 0);
strcpy(saved_command_line, boot_command_line);
strcpy(static_command_line, command_line);
}
parse_early_param
parse_early_param拷贝了一份boot_command_line,通过parse_early_options调用到了parse_args。
注意:start_kernel一共会调用2次parse_early_param,这是第2次。
/* 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;
}
如前文所述,done flag已被置1,所以这里会直接return。
parse_args
继续往下走,parse_early_param执行完成后,会执行parse_args。
注意,此时是start_kernel第2次执行parse_args。
第二次执行parse_args,其形参parse_args不再是NULL,而是指定了.__param段
。
after_dashes = parse_args("Booting kernel",
static_command_line, __start___param,
__stop___param - __start___param,
-1, -1, NULL, &unknown_bootoption);
parse_args还是会遍历cmdline,分割cmdline为param和val键值对,对每对参数调用parse_one。这次parse_one的处理方式为:
- 首先会遍历
.__param
段中所有kernel_param
,将其name与参数的param对比,同名则调用该kernel_param成员变量kernel_param_ops的set方法来设置参数值。这里主要是针对加载驱动的命令行参数的。 - 如果parse_args传给parse_one是kernel通用参数,如console=ttyS0,115200。则parse_one前面遍历.__param段不会找到匹配的kernel_param。就走到后面调用handle_unknown。就是parse_args传来的
unknown_bootoption
。
unknown_bootoption如下:
static int __init unknown_bootoption(char *param, char *val,
const char *unused, void *arg)
{
repair_env_string(param, val, unused, NULL);
/* Handle obsolete-style parameters */
if (obsolete_checksetup(param)) //该函数是最终解析early=0类型param的
return 0;
/* Unused module parameter. */
if (strchr(param, '.') && (!val || strchr(param, '.') < val))
return 0;
if (panic_later)
return 0;
if (val) {
/* Environment option */
unsigned int i;
for (i = 0; envp_init[i]; i++) {
if (i == MAX_INIT_ENVS) {
panic_later = "env";
panic_param = param;
}
if (!strncmp(param, envp_init[i], val - param))
break;
}
envp_init[i] = param;
} else {
/* Command line option */
unsigned int i;
for (i = 0; argv_init[i]; i++) {
if (i == MAX_INIT_ARGS) {
panic_later = "init";
panic_param = param;
}
}
argv_init[i] = param;
}
return 0;
}
static int __init obsolete_checksetup(char *line)
{
const struct obs_kernel_param *p;
int had_early_param = 0;
p = __setup_start;
do {
int n = strlen(p->str);
if (parameqn(line, p->str, n)) {
if (p->early) { //如果early=1,跳过,继续轮询
/* Already done in parse_early_param?
* (Needs exact match on param part).
* Keep iterating, as we can have early
* params and __setups of same names 8( */
if (line[n] == '\0' || line[n] == '=')
had_early_param = 1;
} else if (!p->setup_func) { //如果setup_func不存在,就停止
pr_warn("Parameter %s is obsolete, ignored\n",
p->str);
return 1;
} else if (p->setup_func(line + n)) //循环执行setup_func
return 1;
}
p++;
} while (p < __setup_end);
return had_early_param;
}
- 首先repair_env_string会将param val重新组合为param=val形式。
- obsolete_checksetup则遍历-init_setup段所有obs_kernel_param,如有param->str与param匹配,则调用param_>setup进行参数值配置。
- parse_one对于parse_args传来的每一个cmdline参数都会将
.__param
以及.init.setup
段遍历匹配,匹配到str或name一致,则调用其相应的set或setup函数进行参数值解析或设置。
start_kernel中parse_args结束,kernel的cmdline就解析完成!
总结
-
kernel编译链接,利用
.__param
和.init.setup
段将kernel所需参数和对应处理函数的映射表存放起来; -
kernel启动,do_early_param处理kernel早期使用的参数(如earlyprintk earlycon)
-
parse_args对cmdline每个参数都遍历
__param
以及.init.setup
进行匹配,匹配成功,则调用对应处理函数进行参数值的解析和设置。
需要注意的点
- parse_early_param会执行2次:
- 第一次在setup_arch中,解析early=1时对应的early params
- 第二次由于done flag已经置1,会直接return。
- parse_args也会执行2次
- 第一次parse_args对应第一次执行parse_early_param时,对应的early params
- 第二次parse_args在start_kernel中直接调用,执行解析early=0时对应的params