Linux内核的命令行解析机制

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, &param, &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(&params[i]);
			err = params[i].ops->set(val, &params[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_linestatic_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;
}
  1. 首先repair_env_string会将param val重新组合为param=val形式。
  2. obsolete_checksetup则遍历-init_setup段所有obs_kernel_param,如有param->str与param匹配,则调用param_>setup进行参数值配置。
  3. 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

猜你喜欢

转载自blog.csdn.net/fly_wt/article/details/125132038