9、Linux学习笔记:Linux内核cmdline详解(如何通过bootload给kernel传递参数)

刘曾琪
2023-12-01

目录

点击这里查看所有博文

  随着自己工作的进行,接触到的技术栈也越来越多。给我一个很直观的感受就是,某一项技术/经验在刚开始接触的时候都记得很清楚。往往过了几个月都会忘记的差不多了,只有经常会用到的东西才有可能真正记下来。存在很多在特殊情况下有一点用处的技巧,用的不多的技巧可能一个星期就忘了。

  想了很久想通过一些手段把这些事情记录下来。也尝试过在书上记笔记,这也只是一时的,书不在手边的时候那些笔记就和没记一样,不是很方便。

  很多时候我们遇到了问题,一般情况下都是选择在搜索引擎检索相关内容,这样来的也更快一点,除非真的找不到才会去选择翻书。后来就想到了写博客,博客作为自己的一个笔记平台倒是挺合适的。随时可以查阅,不用随身携带。

  同时由于写博客是对外的,既然是对外的就不能随便写,任何人都可以看到。经验对于我来说那就只是经验而已,公布出来说不一定我的一些经验可以帮助到其他的人。遇到和我相同问题时可以少走一些弯路。

  既然决定了要写博客,那就只能认真去写。不管写的好不好,尽力就行。千里之行始于足下,一步一个脚印,慢慢来 ,写的多了慢慢也会变好的。权当是记录自己的成长的一个过程,等到以后再往回看时,就会发现自己以前原来这么菜。

  本系列博客所述资料均来自互联网资料,并不是本人原创(只有博客是自己写的)。出于热心,本人将自己的所学笔记整理并推出相对应的使用教程,方面其他人学习。为国内的物联网事业发展尽自己的一份绵薄之力,没有为自己谋取私利的想法。若出现侵权现象,请告知本人,本人会立即停止更新,并删除相应的文章和代码。

Linux内核cmdline详解(如何通过bootload给kernel传递参数)

  目前各平台默认关闭dump设置,如果要抓dump,需要通过AT+QCFG=”ApRstLevel“,0和AT+QCFG=“ModemRstLevel”,0命令设置使能;有些情况下模块在启动过程中发生dump,而由于此时AT不通无法通过AT设置dump使能,因此需要有手段在系统启动早期可以使能dump,方便问题调试。

  想要做到不修改代码,并且需要在系统启动的早期灵活的配置DUMP功能。可以在bootload启动的过程中通过检测keypad的ctrl+c中断开机过程,随后键盘输入dump命令。随后在kernel加载的过程中通过cmdline传入相关的启动参数,开启模块的dump功能。

cmdline

  cmdline是uboot引导内核启动时传递给内核的参数序列,作用是指导内核启动。内核启动阶段会去内存中读取并解析cmdline,并根据cmdline去指导内核启动。

  cmdline是一系列启动参数组成的参数序列,参数之间用空格隔开依次排列,每个参数中都是以key=value这种键值对的方式进行描述; 。参数中一般包含启动存储介质、文件系统分区及挂载方式和终端串口等参数。

  linux内核的cmdline配置有两种方式,分别是:

  • 通过u-boot bootargs 传递;
  • 通过内核cmdline参数配置。

  chosen 针对第二点描述的直接修改内核参数,可参阅设备树中的bootargs的定义,bootargs参数被包含在chosen节点中。kernel加载时就是读取chosen中的bootargs,该文件中定义的bootargs和bootload传递过来的bootargs数据最终会合并在一起。

	//设备树文件位于\kernel\msm-5.4\arch\arc\boot\dts\nsim_700.dts
	chosen {
		bootargs = "earlycon=arc_uart,mmio32,0xc0fc1000,115200n8 console=ttyARC0,115200n8 print-fatal-signals=1";
	};

  我们常用的方式是在bootload中通过bootargs直接传递。

  在shell中我们可以通过cat /proc/cmdline 来查看 bootload给 kernel 传递的 cmdline参数内容。

/ # cat /proc/cmdline
console=ttyMSM0,115200,n8 
androidboot.hardware=qcom 
msm_rtb.filter=0x237 
androidboot.console=ttyMSM0 
lpm_levels.sleep_disabled=1 
firmware_class.path=/lib/firmware/updates 
service_locator.enable=1 
net.ifnames=0 
atlantic_fwd.rx_ring_size=1024 
pci=pcie_bus_perf 
rootfstype=ubifs 
rootflags=bulk_read 
root=ubi0:rootfs 
ubi.mtd=28 ubi.mtd=25 
androidboot.serialno=4792e506 
androidboot.baseband=msm 
data_interface=[0,0] 
ql_pcie_mode=0
ql_crash_mode=0,0

  在本例描述的应用中只要在cmdline中包含ql_crash_mode=0,0字段,kernel启动后就会使用相应的函数设置标志位,使得模块在出现dump现象时,自动进入dump文件上载模式。

参数传递

  关于上述功能开发代码中的处理很简单,只需要在BootLinux函数中修改即可,往BootParamlistPtr.CmdLine变量后面粘贴一个字符串(ql_crash_mode=0,0)。在UpdateCmdLine函数中会对一系列(很多很多)的cmdline进行预处理。

EFI_STATUS BootLinux (BootInfo *Info)
{

	xxxxxxxxxx
	#ifdef QL_DEBUG_MODE
	if ( Info->BootWithDebug == TRUE){
	    AsciiStrCatS (BootParamlistPtr.CmdLine, BOOT_ARGS_SIZE, " debug=1");
	}
	#endif

	#ifdef QL_DUMP_MODE
	if ( Info->BootWithDump == TRUE){
	    AsciiStrCatS (BootParamlistPtr.CmdLine, BOOT_ARGS_SIZE, " ql_crash_mode=0,0");
	}
	#endif
	Status = UpdateCmdLine (BootParamlistPtr.CmdLine, FfbmStr, Recovery,
	                AlarmBoot, Info->VBCmdLine, &BootParamlistPtr.FinalCmdLine,
	                &BootParamlistPtr.FinalBootConfig,
	                &BootParamlistPtr.FinalBootConfigLen,
	                Info->HeaderVersion,
	                (VOID *)BootParamlistPtr.DeviceTreeLoadAddr);

	xxxxxxxxxx
  
	Status = LoadAddrAndDTUpdate (Info, &BootParamlistPtr);
	if (Status != EFI_SUCCESS) {
		return Status;
	}
	xxxxxxxxxx
}

  UpdateCmdLine处理完成后,由LoadAddrAndDTUpdate携带BootParamlistPtr参数经过后续一系列调用后,最终会在UpdateDeviceTree 函数中将cmdline中记录的所有数据全部添加到bootargs的设备节点中。

EFI_STATUS UpdateDeviceTree (VOID *fdt,CONST CHAR8 *cmdline,VOID *ramdisk,UINT32 RamDiskSize,BOOLEAN BootWith32Bit)
{
  xxxxxxxxxxxx
  if (cmdline) {
    /* Adding the cmdline to the chosen node */
    FdtPropUpdateFunc (fdt, offset, (CONST char *)"bootargs",
                      (CONST VOID *)cmdline, fdt_appendprop_string, ret);
    if (ret) {
      DEBUG ((EFI_D_ERROR,
              "ERROR: Cannot update chosen node [bootargs] - 0x%x\n", ret));
      return EFI_LOAD_ERROR;
    }
  }
  xxxxxxxxxxxx
}

参数接收

  bootload传输过来cmdline之后,我们只需要在适当的位置添加内核参数的处理函数。假如bootloader在启动kernel时传递的参数是"ql_crash_mode=xxx",kernel运行的时候就会自动调用set_crash_mode函数,通过s参数将cmdline携带的参数带入函数。

static  int __init set_crash_mode(char *s)
{
	xxxxxxxxxx
}
__setup("ql_crash_mode=", set_crash_mode);

  __setup是一个宏定义,可以简单的理解为在编译的时候在程序中注册了set_crash_mode函数,并将cmdline字符"ql_crash_mode="set_crash_mode绑定在了一起。

  程序在加载内核的时候会自动解析并遍历cmdline命令列表,一旦在cmdline参数列表中找到"ql_crash_mode="字符串,内核就会自动调用set_crash_mode函数。具体怎么实现的在本文的后续章节中会有详细描述,这里不多做解释。

__setup 的实现

  宏定义__setup被定义在kernel/msm-4.14/include/linux/init.h文件中。

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) \	//early_param 宏注册的内核选项必须要在其他内核选项之前被处理。
	__setup_param(str, fn, fn, 1)
	

  将上述__setup("ql_crash_mode=", set_crash_mode);直接展开,可得到下面的声明。

static  const  char __setup_str_set_crash_mode[]
__initconst __aligned(1)
= "ql_crash_mode=";

static  struct obs_kernel_param __setup_set_crash_mode 
__used __section(.init.setup) __attribute__((aligned((sizeof(long))))) 
= {
	 __setup_str_set_crash_mode, 
	 set_crash_mode, 
	 0 
}

  __section关键字会将被修饰的变量或函数编译到特定的一块位置,不是物理存储器上的特定位置,而是在可执行文件的特定段内。.init.setup就是一个kernel定义的很普通的代码段,里面存放的都是obs_kernel_param类型的结构体变量。

   __attribute__ 关键词和 aligned关键词一起使用,可以指定对象的对齐格式。该结构体变量内部成员的长度全部被强制设定为4字节对齐。

  简单来讲上面的宏展开后,就是定义了一个 obs_kernel_param 结构体变量,它的名字叫做__setup_set_crash_mode ,里面有三个成员,
分别是名为__setup_str_set_crash_mode的字符数组 、函数指针set_crash_mode、 和一个叫做early的变量。在完成编译后该变量被存放进了.init.setup段。

内核加载导入bootargs

  Linux内核在加载(start_kernel)的过程中setup_arch函数会调用到setup_machine_fdt函数传入__atags_pointer参数。__atags_pointer这个全局变量存储的就是r2的寄存器值,是设备树在内存中的起始地址。


//函数位于kernel\msm-5.4\arch\arm\kernel\devtree.c
const  struct machine_desc * __init setup_machine_fdt(void *dt_virt)
{
	xxxxxxxxxxxxxx
	if (!dt_virt || !early_init_dt_verify(dt_virt))
		return  NULL;
	mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
	xxxxxxxxxxxxxx
	early_init_dt_scan_nodes();
}

//函数位于kernel\msm-5.4\arch\arm\kernel\setup.c
void __init setup_arch(char **cmdline_p)
{
	const  struct machine_desc *mdesc = NULL;
	void *atags_vaddr = NULL;
	if (__atags_pointer)
		atags_vaddr = FDT_VIRT_BASE(__atags_pointer);
	setup_processor();
	if (atags_vaddr) {
		mdesc = setup_machine_fdt(atags_vaddr);
		if (mdesc)
			memblock_reserve(__atags_pointer,fdt_totalsize(atags_vaddr));
	}
	xxxxxxxxxxx
	parse_early_param();
	xxxxxxxxxxx
}

//函数位于kernel\msm-5.4\init\main.c
asmlinkage __visible void __init start_kernel(void)
{
	xxxxxxxxxxxxx
	setup_arch(&command_line);
	setup_command_line(command_line);
	xxxxxxxxxxxxx
	jump_label_init();
	parse_early_param();
	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);
	xxxxxxxxxxxxx
}

  随后跳转到early_init_dt_scan_nodes函数,调用of_scan_flat_dt函数完成对设备树地址(dt_virt)的扫描,扫描过程中查找chosen节点,找到后在节点内提取bootargs参数。并将其拷贝到cmdline。(chosen节点就表示bootloader传递给kernel的cmdline

//函数位于kernel\msm-5.4\drivers\of\fdt.c

int __init early_init_dt_scan_chosen(unsigned long node, const char *uname, int depth, void *data)
{
	xxxxxxxx
	if (depth != 1 || !cmdline ||
	    (strcmp(uname, "chosen") != 0 && strcmp(uname, "chosen@0") != 0))
		return 0;
	/* Put CONFIG_CMDLINE in if forced or if data had nothing in it to start */
	if (overwrite_incoming_cmdline || !cmdline[0])
		strlcpy(cmdline, config_cmdline, COMMAND_LINE_SIZE);
	/* Retrieve command line unless forcing */
	if (read_dt_cmdline)
		p = of_get_flat_dt_prop(node, "bootargs", &l);
	xxxxxxxx
	if (p != NULL && l > 0) {
		if (concat_cmdline) {
			int cmdline_len;
			int copy_len;
			strlcat(cmdline, " ", COMMAND_LINE_SIZE);
			cmdline_len = strlen(cmdline);
			copy_len = COMMAND_LINE_SIZE - cmdline_len - 1;
			copy_len = min((int)l, copy_len);
			strncpy(cmdline + cmdline_len, p, copy_len);
			cmdline[cmdline_len + copy_len] = '\0';
		} else {
			strlcpy(cmdline, p, min(l, COMMAND_LINE_SIZE));
		}
	}
	pr_debug("Command line is: %s\n", (char*)data);
}

int __init of_scan_flat_dt(int (*it),void *data)
{
	const  void *blob = initial_boot_params;
	const  char *pathp;
	int offset, rc = 0, depth = -1;
	if (!blob)
		return  0;
	for (offset = fdt_next_node(blob, -1, &depth);offset >= 0 && depth >= 0 && !rc;offset = fdt_next_node(blob, offset, &depth))
	{
		pathp = fdt_get_name(blob, offset, NULL);
		if (*pathp == '/')
			pathp = kbasename(pathp);
		rc = it(offset, pathp, depth, data);
	}
	return rc;
}

void __init early_init_dt_scan_nodes(void)
{
	int rc = 0;

	/* Retrieve various information from the /chosen node */
	rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
	if (!rc)
		pr_warn("No chosen node found, continuing without\n");

	/* Initialize {size,address}-cells info */
	of_scan_flat_dt(early_init_dt_scan_root, NULL);

	/* Setup memory, calling early_init_dt_add_memory_arch */
	of_scan_flat_dt(early_init_dt_scan_memory, NULL);
}

  cmdline指向一个全局变量boot_command_line需要注意的是boot_command_line是一个静态数组,我们的代码在arm的环境下是4096。也就是说bootloader传递给kernel的cmdline超过4096就要修改kernel源代码中COMMAND_LINE_SIZE的定义 。一旦出现cmdline长度大于boot_command_line数组长度的情况,轻则会存在参数无法正常传递的现象,严重的话可能会导致kernel dump(具体什么现象我没有测试)。


//定义位于\kernel\msm-5.4\init\main.c
/* Untouched command line saved by arch-specific code. */
char __initdata boot_command_line[COMMAND_LINE_SIZE];

//定义位于kernel\msm-5.4\arch\arm\include\uapi\asm\setup.h
#ifndef _UAPI__ASMARM_SETUP_H
#define _UAPI__ASMARM_SETUP_H
#include  <linux/types.h>
#define COMMAND_LINE_SIZE 4096
xxxxxx
#endif

  linux5.4.180的kennel代码中arm定义为1024字节。

//
//定义位于arch\arm\include\uapi\asm\setup.h
#ifndef _UAPI__ASMARM_SETUP_H
#define _UAPI__ASMARM_SETUP_H
#include  <linux/types.h>
#define COMMAND_LINE_SIZE 1024
xxxxxx
#endif

第一次处理parse_early_param

  在内核加载的过程中会通过setup_arch调用到parse_early_param函数,在随后函数中会调用parse_argsboot_command_line变量进行解析。

//函数位于kernel\msm-5.4\init\main.c
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)//注意这个done flag,在一次启动过程中,该函数可能会被多次调用,但只会执行一次
		return;//因为结尾将done设为1,再次执行时会直接return
	/* All fall through to do_early_param. */
	strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);
	parse_early_options(tmp_cmdline);
	done = 1;
}

//函数位于kernel\msm-5.4\arch\arm\kernel\setup.c
void __init setup_arch(char **cmdline_p)
{
	const  struct machine_desc *mdesc = NULL;
	void *atags_vaddr = NULL;
	if (__atags_pointer)
		atags_vaddr = FDT_VIRT_BASE(__atags_pointer);
	setup_processor();
	if (atags_vaddr) {
		mdesc = setup_machine_fdt(atags_vaddr);
		if (mdesc)
			memblock_reserve(__atags_pointer,fdt_totalsize(atags_vaddr));
	}
	xxxxxxxxxxx
	parse_early_param();
	xxxxxxxxxxx
}

//函数位于kernel\msm-5.4\init\main.c
asmlinkage __visible void __init start_kernel(void)
{
	xxxxxxxxxxxxx
	setup_arch(&command_line);
	setup_command_line(command_line);
	xxxxxxxxxxxxx
	jump_label_init();
	parse_early_param();
	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);
	xxxxxxxxxxxxx
}

parse_args解析

  parse_args函数中会使用next_arg以此提取cmdline中的成员(以空格分割),在next_arg会对cmdline进行参数和值分离(以等号=分割)。由于从parse_early_options传入的num_params=0,所以parse_one是直接走的最后handle_unknown函数,即parse-early_options传入的do_early_param。

//函数位于kernel\msm-5.4\kernel\params.c

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);
			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;
}


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))
{
	xxxxx
	while (*args) {
		xxxxx
		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);
		xxxxx
	}
	return err;
}

//函数位于kernel\msm-5.4\lib\cmdline.c
char *next_arg(char *args, char **param, char **val)
{
	unsigned  int i, equals = 0;
	int in_quote = 0, quoted = 0;
	char *next;
	if (*args == '"') {
		args++;
		in_quote = 1;
		quoted = 1;
	}
	for (i = 0; args[i]; i++) {
		if (isspace(args[i]) && !in_quote)
			break;
		if (equals == 0) {
			if (args[i] == '=')
				equals = i;
		}
		if (args[i] == '"')
			in_quote = !in_quote;
	}
	*param = args;
	if (!equals)
		*val = NULL;
	else {
		args[equals] = '\0';
		*val = args + equals + 1;
		/* Don't include quotes in value. */
		if (**val == '"') {
			(*val)++;
			if (args[i-1] == '"')
				args[i-1] = '\0';
		}
	}
	if (quoted && args[i-1] == '"')
		args[i-1] = '\0';
	if (args[i]) {
		args[i] = '\0';
		next = args + i + 1;
	} else
		next = args + i;
	/* Chew up trailing spaces. */
	return  skip_spaces(next);
}

  handle_unknown在本例中就是调用parse_args时传入的_do_early_param函数。

early为1的调用setup函数调用

  调用cmdline处理函数和我们平时常用的调用方式不一样。通过读取特定代码段的内容解析出来,然后通过函数指针的方式去调用处理函数。在do_early_param函数中会对.init.setup段进行一个遍历,__setup_start是该段的起始地址,__setup_end是该段的结束地址。

//段定义位于kernel\msm-5.4\include\asm-generic\vmlinux.lds.h

#define INIT_SETUP(initsetup_align) \
. =  ALIGN(initsetup_align); \
__setup_start = .; \
KEEP(*(.init.setup)) \
__setup_end = .;

  *init.setup段中存储了所有obs_kernel_param的变量,在知道段起始地址的情况下,就可以直接在地址内读取到结构体的数据。使用for循环提取obs_kernel_param长度的数据,p->str就是当前结构体中记录的cmdline对应的参数名,p->setup_func就是cmdline处理函数。

  遍历过程中,如果有obs_kernel_param的early为1,或cmdline中有console参数并且obs_kernel_param有earlycon参数,则会调用该obs_kernel_param的setup函数来解析参数。

  do_early_param是为kernel中需要尽早配置的功能(如earlyprintk earlycon)做cmdline的解析并完成处理函数的调用。

struct  obs_kernel_param {
	const  char *str;
	int (*setup_func)(char  *);
	int early;
};
//函数位于\kernel\msm-5.4\init\main.c
/* 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;
}

early为0的调用setup函数调用

  而obs_kernel_param的early为0的,则在start_kernel的后续流程中parse_args继续完成解析。此时是start_kernel第2次执行parse_args

//函数位于kernel\msm-5.4\init\main.c
asmlinkage __visible void __init start_kernel(void)
{
	xxxxxxxxxxxxx
	setup_arch(&command_line);
	setup_command_line(command_line);
	xxxxxxxxxxxxx
	jump_label_init();
	parse_early_param();
	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);
	xxxxxxxxxxxxx
}

  在第二次执行parse_args时,其形参parse_args不再是NULL,而是指定了.__param段,该段中存储的时一些模块的注册信息,主要是针对加载驱动的命令行参数的,将params[i].name与param对比,同名则调用该kernel_param成员变量kernel_param_ops的set方法来设置参数值,这里不多做解释。

//代码位于kernel\msm-5.4\drivers\net\wireless\ath\wil6210\main.c
unsigned  int mtu_max = TXRX_BUF_LEN_DEFAULT - WIL_MAX_MPDU_OVERHEAD;
static  int  mtu_max_set(const  char *val, const  struct kernel_param *kp)
{
	int ret;
	/* sets mtu_max directly. no need to restore it in case of
	* illegal value since we assume this will fail insmod
	*/
	ret = param_set_uint(val, kp);
	if (ret)
		return ret;
	if (mtu_max < 68 || mtu_max > WIL_MAX_ETH_MTU)
		ret = -EINVAL;
	return ret;
}
static  const  struct kernel_param_ops mtu_max_ops = {
	.set = mtu_max_set,
	.get = param_get_uint,
};
module_param_cb(mtu_max, &mtu_max_ops, &mtu_max, 0444);
MODULE_PARM_DESC(mtu_max, " Max MTU value.");

  如果parse_args传给parse_one是kernel通用参数,如console=ttyS0,115200。则parse_one前面遍历.__param段不会找到匹配的kernel_param。就走到后面调用handle_unknown。就是parse_args传来的unknown_bootoption

//函数位于kernel\msm-5.4\kernel\params.c

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);
			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;
}

  在unknown_bootoption中,obsolete_checksetup(该函数是最终解析early=0类型param的)则遍历*init.setup段所有obs_kernel_param,如有param->str与param匹配,则调用param_>setup进行参数值配置。

static  bool __init obsolete_checksetup(char *line)
{
	const  struct obs_kernel_param *p;
	bool had_early_param = false;
	p = __setup_start;
	do {
		int n = strlen(p->str);
		if (parameqn(line, p->str, n)) {
			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))
					return  true;
			}
		p++;
	} while (p < __setup_end);
	return had_early_param;
}

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))
		return  0;
	xxxxxxxxxxxxxx
}

  这样一来整个cmdline的流程全部梳理清楚了,从参数传递,到参数解析,到处理函数调用。

总结

  • 两种方式添加cmdline
    • 修改设备树
    • bootloader传递
  • cmdline中会有三种类型
    • 需要尽早配置的功能(early=1)
    • 加载驱动的命令行参数(param段)
    • 一般的通用内核参数(early=0)
  • parse_early_param函数会被调用多次,但是只有第一次会起作用
  • parse_one也会被调用多次,第一次处理early=0,第二次处理early=1和param段注册的参数

  本篇内容通过一个开发需求,选择一个cmdline实例,使用代码走读的方式。对Linux内核的cmdline机制的流程全部梳理了一遍,从参数传递,到参数解析,到处理函数的调用。通过本篇文章,按照文中所诉的流程,可以简单的了解到bootload在引导linux内核时的参数是怎么传递给kernel处理的。

  那么本篇博客就到此结束了,这里只是记录了一些我个人的学习笔记,其中存在大量我自己的理解。文中所述不一定是完全正确的,可能有的地方我自己也理解错了。如果有些错的地方,欢迎大家批评指正。如有问题直接在对应的博客评论区指出即可,不需要私聊我。我们交流的内容留下来也有助于其他人查看,说不一定也有其他人遇到了同样的问题呢。

 类似资料: