以isolcpus=参数为例,介绍Linux kernel中command-line(cmdline)的读写、解析、生效过程

最近打算往Linux kernel command-line parameters新建功能,嵌入到Linux kernel中。为此,我需要了解Linux kernel的初始化过程,并且理解command-line parameters是怎么被解析和形成功能的。

这篇文章以isolcpus为例,介绍isolcpus= 参数的解析及生效过程。由于isolcpus= 涉及到GRUB_CMDLINE_LINUX的解析,因此我还会对start_kernel()中我认为与isolcpus= 相关的部分做解释。

目录

1. isolcpus= 介绍

2. start_kernel()介绍

2.1 boot_cpu_init()

2.2 setup_arch(&command_line)

2.3 setup_boot_config(command_line)

2.4 setup_command_line(command_line)

2.5 after_dashes = parse_args()

__param段介绍

parse_one()函数介绍

__setup段参数介绍

unknown_bootoption()

obsolete_checksetup()

housekeeping_isolcpus_setup()

2.6 housekeeping_init()

环境

Linux kernel:5.10.158

系统架构: arm64

1.isolcpus= 介绍

详情isolcpus= 的介绍可以参考Linux的官方文档解析The kernel’s command-line parameters — The Linux Kernel documentation,在这里只进行简单介绍

isolcpus= 的格式为isolcpus= [flag-list,],其中flag-list有三种备用选项,分别为 nohzdomainmanaged_irq

nohz的含义是在该CPU上,当只有一个任务在运行时,定时器不会定期向该CPU发送tick信号,这样做可以提升任务运行效率。

domain的含义是SMP负载均衡和调度算法不会考虑该CPU,若没有用户指定,全局调度器不会将任务向该CPU上边调度;同时该CPU上的任务也不受被全局调度器指挥,而只能受该CPU的调度器指挥。

managed_irq的含义是managed interrupt的CPU掩码将该CPU排除在外。注意,此处的managed interrupt指在isolcpus=生效前,其CPU掩码同时覆盖了受isolcpus=影响的CPU和不受isolcpus=影响的CPU。例如,在一个4核的主机中,若一中断的CPU掩码为1111,且isolcpus=managed_irq,2-3,则生效后其掩码为0011;若中断的CPU掩码为1000,且isolcpus不变,则生效后其掩码仍为000

而flag-list默认带上domain选项,其它两个选项需自行设置。

2. start_kernel()介绍

Linux kernel的启动过程大体可以分为两阶段,第一阶段是bootloader进行系统引导,将Linux kernel的vmlinuz解压缩并且加载进入内存中,第一阶段的详情内容可以参考Linux 内核启动及文件系统加载过程 - 知乎 (zhihu.com)。第二阶段是start_kernel()函数的运行,该函数解析GRUB_COMMAND_LINE并且进行多数功能和模块的初始化。在start_kernel()中,与isolcpus= 相关的函数和路径有这些(详情可以看源代码/init/main.c):

start_kernel()
	->char *command_line//定义变量
	->char *after_dashes//定义变量
	......
	->boot_cpu_init()//初始化并激活boot cpu(即0号CPU)
	......
	->setup_arch(&command_line)//针对系统特定架构进行设置(在这里为arm64)
	->setup_boot_config(command_line)//视CONFIG_BOOT_CONFIG而定
	->setup_command_line(command_line)//为boot_command_line、saved_command_line、static_command_line
									//分配空间以及传递boot_command_line内容
	......
	->after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, NULL, &unknown_bootoption)
		->unknown_bootoption()
			->obsolete_checksetup()
	......
	->housekeeping_init()

2.1 boot_cpu_init()

该函数的功能是初始化boot cpu(即0号CPU),将该CPU的状态设置成online、active、present以及possible。

2.2 setup_arch(&command_line)

由于架构是arm64,所以setup_arch(&command_line)的定义在文件/arch/arm64/kernel/setup.c中。

在setup_arch中有这些比较重要的函数

void __init __no_sanitize_address setup_arch(char **cmdline_p)
{
	......
	//在此处,boot_command_line是一个长为2048字节(arm64)的char数组地址
	*cmdline_p = boot_command_line;
	
	......
	/*
	 * 该函数在对设备树地址的扫描过程中,查找chosen节点,
	 * 找到后在节点内提取bootargs参数,并将其拷贝到cmdline
	 * setup_machine_fdt
	 * ->early_init_dt_scan
	 * ->early_init_dt_scan_nodes
	 * ->of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line)
	 */
	setup_machine_fdt(__fdt_pointer);
	
	......
	/* 捕捉cmdline中的console和 obs_kernel_param的early参数
	 */
	parse_early_param();
	
	......
}

setup_machine_fdt(__fdt_pointer)函数中,函数扫描设备树地址,查找chosen节点,并且在节点内提取bootargs参数,将其拷贝到cmdline中。bootargs参数即是我们想要的GRUB_CMDLINE_LINUX的内容。

parse_early_param()调用 strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE),将boot_command_line的内容写入到tmp_cmdline(该变量在parse_early_param(void)中新定义)中,再调用parse_early_options(tmp_cmdline),捕捉tmp_cmdline中的console和 obs_kernel_param的early参数。

可以确定的是,我们想要的CMDLINE_LINUX的内容,存储于变量boot_command_line中,该变量在/init/main.c中被全局定义,是一个该文件的全局变量。

2.3 setup_boot_config(command_line)

CONFIG_BOOT_CONFIG is not set(默认)时,有以下引用:

static void __init setup_boot_config(const char *cmdline)
{
	/* Remove bootconfig data from initrd */
	get_boot_config_from_initrd(NULL, NULL);//从initrd(初始RAM磁盘)获取引导配置信息
}

2.4 setup_command_line(command_line)

该函数使用memblock_alloc()函数,为saved_command_linestatic_command_line分配空间以及传递boot_command_line内容。saved_command_linestatic_command_line都是/init/main.c文件中的全局变量。

saved_command_line的不同之处在于,他还会根据extra_command_lineextra_init_args保留更多的参数,保存没有处理过的命令行参数,是boot_command_line的拷贝;static_command_linecommand_line的拷贝。

2.5 after_dashes = parse_args()

after_dashes = parse_args("Booting kernel",
				  static_command_line, __start___param,
				  __stop___param - __start___param,
				  -1, -1, NULL, &unknown_bootoption);

先来介绍下parse_args()各形参的类型以及位置,还有该函数里边的内容。const char *doing参数做输出用,无实际运行意义,char *args指向static_command_line的首字符。

__start__param__stop__param在/include/linux/moduleparam.h中被定义,定义代码如下:

extern const struct kernel_param __start___param[], __stop___param[];

__start__param__stop__param变量以extern const struct kernel_param []的形式出现,这两个变量被视作数组的首指针。__stop___param - __start___param表明这两个地址之间变量类型为kernel_param数据类型的数目。

__param段介绍

与这两个变量有关的还有kernel的RO_DATA段,叫 __param段。这一段在arm64中相关的文件在/include/asm-generic/vmlinux.lds.h中:

#define RO_DATA(align)							\
	. = ALIGN((align));						\
	// ......
	
	/* Built-in module parameters. */				\
	__param : AT(ADDR(__param) - LOAD_OFFSET) {			\
		__start___param = .;					\
		KEEP(*(__param))					\
		__stop___param = .;					\
	}								\
	
	// ......

这一段用于存放kernel下各个driver需要的参数,在driver文件中可以通过module_param()注册参数,并且kernel启动时由cmdline指定该参数的值。这些参数在被编译和注册后,将被kernel链接并写入vmlinuz中,并且在System.map文件中存放相应映射,如下图所示。

在这里插入图片描述

图 System.map __param段部分参数示意图

在上图中,左边的是 __param类型参数在vmlinuz中的地址,中间的’r’表明该参数的读写类型,右边的内容是参数的完整名称。

详情的 __param段参数解析例子可以参考linux kernel的cmdline参数解析原理分析【转】 - yooooooo - 博客园 (cnblogs.com),这篇文章以drivers/usb/gadget/serial.c的use_acm参数为例,此处不再赘述。

以下是char *parse_args()的详细解释(附解析)

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后边的所有空格,保留字符串,返回一系列连续空格后的第一个字符串地址*/
	args = skip_spaces(args);

	if (*args)
		pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args);

	while (*args) {
		int ret;
		int irq_was_disabled;

		/* 以isolcpus=2-13为例,param指向i,val指向2,args指向13后的最后一个空格的下一个位置
		 * 请注意,在next_args()的运行中,val指针的上一个字符已被替换成 /0 */
		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()函数中,next_arg()的作用是提取一对键值对param-val(param指向=号左边的首字符,val指向等号右边的首字符),args指向下一对键值对的首字符。parse_one()函数的作用是对键值对param-val作运算和操作。

parse_one()函数介绍

下边是parse_one()的详细介绍。在该函数中,param、val的解释在上一段中,doing指向"Booting kernel"的首字符,params指针为__start___paramnum_params__stop___param - __start___parammin_levelmax_level均为1,arg为空,int (*handle_unknown)函数指针为unknown_bootoption()

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++) {
		/* 将param和params[]里边的词一一匹配,
		 * 一旦发生匹配,则不再进行handle_unknown的部分
		 * 发生匹配时,如果一切顺利,将进行动作,
		 * params[i].ops->set
		 */
		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);
			/* 完成一定的动作,完成情况将返回至err中 */
			if (param_check_unsafe(&params[i]))
				err = params[i].ops->set(val, &params[i]);
			else
				err = -EPERM;
			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;
}

考虑到isolcpus参数并不是驱动driver相关的参数,没有存储到 __param段中,所以parse_one()的for循环函数中并不会发生paramparams[]的匹配。kernel在for循环结束后,将运行handle_unknown函数指针所指向的函数,在这里,handle_unknown函数指针指向的函数是unknown_bootoption。在if(handle_unknown)的if模块中,可以看到handle_unknown()函数指针所指向的函数只携带四个形参。

在进行unknown_bootoption的介绍之前,先进行isolcpus参数的存储介绍

__setup段参数介绍

isolcpus参数的定义在/kernel/sched/isolcation.c中:

__setup("isolcpus=", housekeeping_isolcpus_setup);

其中,__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 }

#define __setup(str, fn)						\
	__setup_param(str, fn, fn, 0)

在宏的相关定义中,__setup_param被解释为两个变量,分别是static const char __setup_str_##unique_id[]常量字符数组,__initconst__aligned(1) 是编译器指令,分别用于将这个变量放入初始化常量段,并确保它按字节对齐;以及static struct obs_kernel_param __setup_##unique_id结构体,这个结构体接受三个参数:

struct obs_kernel_param {
	const char *str; 			//变量名称
	int (*setup_func)(char *);	//设置函数
	int early;					//是否需要提前检测,1为需要,0为不需要
};

以isolcpus变量为例,该__setup宏会形成两个变量:

static const char __setup_str_housekeeping_isolcpus_setup[]  __initconst __aligned(1) = "isolcpus=";
static struct obs_kernel_param __setup_housekeeping_isolcpus_setup __used __section(".init.setup") \
	__attribute__((aligned((sizeof(long)))))=
{
    .*str = "isolcpus=";
    .(*setup_func) = &housekeeping_isolcpus_setup;
    .early = 0
}

这两个变量在System.map表(存储内核符号(如函数和变量名及其在内存中的地址))中也有提及。
在这里插入图片描述

图 System.map表对isolcpus参数的捕捉图

下边开始继续介绍unknown_bootoption()函数以及相关调用,调用关系如下所示:

unknown_bootoption()->obsolete_checksetup()->housekeeping_isolcpus_setup()->housekeeping_setup()

unknown_bootoption()obsolete_checksetup()housekeeping_isolcpus_setup()的详细代码介绍如下代码块所示:

具体而言,unknown_bootoption()负责引导进入obsolete_checksetup()函数,进行 __setup 段参数的检查,obsolete_checksetup()函数负责循环,在 _setup段参数中寻找与 isolcpus= 相匹配的地址;housekeeping_isolcpus_setup()负责检查三种flag是否在’='号后边,housekeeping_setup()负责将cpu-list写入到相应变量,供其他函数和模块使用。

unknown_bootoption()

static int __init unknown_bootoption(char *param, char *val,
				     const char *unused, void *arg)
{
	size_t len = strlen(param);
	
    //val指针前边重新添加'='号
	repair_env_string(param, val);

	/* Handle obsolete-style parameters */
    /* __setup段参数的检查,只引用param参数*/
	if (obsolete_checksetup(param))
		return 0;

	//......
}

obsolete_checksetup()

static bool __init obsolete_checksetup(char *line)
{
	const struct obs_kernel_param *p;
	bool had_early_param = false;

    //针对字符串line,kernel遍历__setup段参数进行比对
	p = __setup_start;
	do {
		int n = strlen(p->str); //strlen(p->str)的值为"isolcpus="的长度,即为9
		if (parameqn(line, p->str, n)) { //line字符串和p->str字符串进行前边n个字符的比对,若全相等,则返回true
			if (p->early) {
				/* 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 = true;
			} else if (!p->setup_func) {
				pr_warn("Parameter %s is obsolete, ignored\n",
					p->str);
				return true;
			} else if (p->setup_func(line + n)) //line+n运算后,指针line+n指向'='后的第一个字符,
				return true;					//在isolcpus参数中,setup_func为函数
            									//static int __init housekeeping_isolcpus_setup(char *str)
		}
		p++;
	} while (p < __setup_end);

	return had_early_param;
}

housekeeping_isolcpus_setup()

static int __init housekeeping_isolcpus_setup(char *str)
{
	unsigned int flags = 0;
	bool illegal = false;
	char *par;
	int len;

	while (isalpha(*str)) {
		if (!strncmp(str, "nohz,", 5)) { //还记得前边提到过isolcpus的flag-list吗?这个函数主要目的就是检测isolcpus=										 //后边是否跟随了那三种flag,若有,则在flags中写入相应标识符
			str += 5;
			flags |= HK_FLAG_TICK;
			continue;
		}

		if (!strncmp(str, "domain,", 7)) {
			str += 7;
			flags |= HK_FLAG_DOMAIN;
			continue;
		}

		if (!strncmp(str, "managed_irq,", 12)) {
			str += 12;
			flags |= HK_FLAG_MANAGED_IRQ;
			continue;
		}

		/*
		 * Skip unknown sub-parameter and validate that it is not
		 * containing an invalid character.
		 */
		for (par = str, len = 0; *str && *str != ','; str++, len++) {
			if (!isalpha(*str) && *str != '_')
				illegal = true;
		}

		if (illegal) {
			pr_warn("isolcpus: Invalid flag %.*s\n", len, par);
			return 0;
		}

		pr_info("isolcpus: Skipped unknown flag %.*s\n", len, par);
		str++;
	}

	/* Default behaviour for isolcpus without flags */
	if (!flags)
		flags |= HK_FLAG_DOMAIN;

    /* 该函数所在文件/kernel/sched/isolation.c 定义变量
     * static cpumask_var_t housekeeping_mask 和 static unsigned int housekeeping_flags
     * flags将写入housekeeping_flags,而str经过进一步解析后写入housekeeping_mask中。
     * 至此,isolcpus= 的参数设置过程已经结束
     * 篇幅有限,不再陈列housekeeping_setup()的内容
     */
	return housekeeping_setup(str, flags); 
}

2.6 housekeeping_init()

void __init housekeeping_init(void)
{
	if (!housekeeping_flags)
		return;

	static_branch_enable(&housekeeping_overridden);

	if (housekeeping_flags & HK_FLAG_TICK)
		sched_tick_offload_init();

	/* We need at least one CPU to handle housekeeping work */
	WARN_ON_ONCE(cpumask_empty(housekeeping_mask));
}

housekeeping_init()函数的任务是检查housekeeping_flagshousekeeping_mask是否有内容问题,如果没有,则可以交给/kernel/sched/文件夹中其他结构体或变量使用。

至此,以isolcpus= 参数为例,这篇文章讲解了Linux command-line(cmdline) parameters是怎么被读取和解析的,并且讲解了isolcpus= 参数的生效过程。

希望对你有帮助,如有不足之处,还请多多指正。谢谢!

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
可以参考以下代码来使用 `saved_command_line` 来解析 `panel_id` 参数: ```c #include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> #include <linux/string.h> static int panel_id = -1; // 初始化为无效值 static int __init my_module_init(void) { pr_info("my_module: panel_id=%d\n", panel_id); // 在这里使用 panel_id 参数进行其它操作 return 0; } static void __exit my_module_exit(void) { pr_info("my_module: exit\n"); } module_init(my_module_init); module_exit(my_module_exit); // 解析 panel_id 参数 static int __init parse_panel_id(char *str) { char *ptr; int id = -1; ptr = strstr(saved_command_line, "panel_id="); if (ptr != NULL) { id = simple_strtol(ptr + strlen("panel_id="), NULL, 10); } if (id >= 0) { panel_id = id; pr_info("my_module: panel_id=%d\n", panel_id); } else { pr_info("my_module: panel_id not found\n"); } return 1; // 返回 1 表示已处理该参数 } // 注册解析函数 static int __init register_parse_panel_id(void) { parse_early_param("panel_id", parse_panel_id); return 0; } early_initcall(register_parse_panel_id); ``` 在该程序,我们使用 `strstr` 函数在 `saved_command_line` 搜索 `panel_id` 参数,并使用 `simple_strtol` 函数将其转换为整数类型。如果解析成功,则将其存储到全局变量 `panel_id` 。在模块初始化函数 `my_module_init` ,我们可以使用 `panel_id` 参数进行其它操作。 需要注意的是,由于 `saved_command_line` 是一个字符串指针,其内容可能会被修改,因此需要在解析参数时使用字符串操作函数(如 `strtok`、`strchr` 等)来确保正确性。同时,由于在使用 `saved_command_line` 时需要在 `early_initcall` 注册解析函数,因此该方法并不适用于需要在模块初始化函数使用参数的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值