最近打算往Linux kernel command-line parameters新建功能,嵌入到Linux kernel中。为此,我需要了解Linux kernel的初始化过程,并且理解command-line parameters是怎么被解析和形成功能的。
这篇文章以isolcpus为例,介绍isolcpus= 参数的解析及生效过程。由于isolcpus= 涉及到GRUB_CMDLINE_LINUX的解析,因此我还会对start_kernel()中我认为与isolcpus= 相关的部分做解释。
目录
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()
housekeeping_isolcpus_setup()
环境
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有三种备用选项,分别为 nohz,domain,managed_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_line、static_command_line分配空间以及传递boot_command_line内容。saved_command_line、static_command_line都是/init/main.c文件中的全局变量。
saved_command_line的不同之处在于,他还会根据extra_command_line和extra_init_args保留更多的参数,保存没有处理过的命令行参数,是boot_command_line的拷贝;static_command_line是command_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文件中存放相应映射,如下图所示。
在上图中,左边的是 __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, ¶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()
函数中,next_arg()
的作用是提取一对键值对param-val(param指向=号左边的首字符,val指向等号右边的首字符),args指向下一对键值对的首字符。parse_one()
函数的作用是对键值对param-val作运算和操作。
parse_one()函数介绍
下边是parse_one()
的详细介绍。在该函数中,param、val的解释在上一段中,doing
指向"Booting kernel"的首字符,params
指针为__start___param
,num_params
为__stop___param - __start___param
,min_level
和max_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(¶ms[i]))
err = params[i].ops->set(val, ¶ms[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循环函数中并不会发生param
和params[]
的匹配。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表(存储内核符号(如函数和变量名及其在内存中的地址))中也有提及。
下边开始继续介绍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_flags
和housekeeping_mask
是否有内容问题,如果没有,则可以交给/kernel/sched/文件夹中其他结构体或变量使用。
至此,以isolcpus= 参数为例,这篇文章讲解了Linux command-line(cmdline) parameters是怎么被读取和解析的,并且讲解了isolcpus= 参数的生效过程。
希望对你有帮助,如有不足之处,还请多多指正。谢谢!