本文主要记录 Linux kernel 获取 cmdline 、Linux early_param 启动参数解析以及 init_call 过程。基于ARM64架构,Linux kernel 5.10.20。
cmdline 的初始化
cmdline的初始化:start_kernel()
-> setup_arch()
将 commad_line 指向 boot_command_line
。很明显,这个变量将记录 cmdline。
*cmdline_p = boot_command_line;
但是内核从哪里获取 bootloader 传递的 cmdline 以及在哪里对boot_command_line
初始化呢?
boot_command_line 定义在 init/main.c 文件中 char __initdata boot_command_line[COMMAND_LINE_SIZE];
。ARM64 架构下,设备树被来描述系统中所有的硬件信息,并在启动内核时向内核传递设备树文件(bootloader将设备树文件所在的地址保存在X0寄存器),内核解析设备树来对系统中存在的硬件进行初始化等操作。与 X86 架构传递 cmdline的方式不同(X86方式可参考链接[4]),ARM64架构下使用设备树传递 cmdline。
设备树文件中的 chosen 节点通常包含许多信息,启动参数 cmdline 就包含在内,一个简单的例子如下:
chosen {
bootargs = "console=ttyS0,115200 loglevel=8";
initrd-start = <0xc8000000>;
initrd-end = <0xc8200000>;
};
BTW:但是在使用树莓派时,我们可以通过修改/boot目录下的 cmdline.txt 文件直接修改树莓派4b的启动boot comand line。我估计是在树莓派的启动流程[2]中读取了cmdline.txt文件并修改了设备树的chosen节点,替换了启动参数。
设备树解析
在内核的启动流程中,early_init_dt_scan_nodes()
函数解析设备树文件,提取chosen节点中bootargs保存到 boot_command_line 中。
void __init early_init_dt_scan_nodes(void)
{
/* Retrieve various information from the /chosen node */
rc = of_scan_flat_dt(early_init_dt_scan_chosen, boot_command_line);
...
}
函数调用关系总结
函数调用关系如下:
setup_arch() // arch/arm64/kernel/setup.c
*cmdline_p = boot_command_line;
->setup_machine_fdt(__fdt_pointer)
->early_init_dt_scan() // driver/of/fdt.c
->early_init_dt_scan_nodes()
-> early_init_dt_scan_chosen()
Cmdline 参数解析与 early_param
获取到 cmdline 后,Linux kernel 在 start_kernel()
函数中调用parse_early_param()
对系统启动早期所需的配置参数进行解析并调用相应的回调。例如,系统启动时可能需要开启一个 earlycon 打印 log 或从设备树文件中获取指定 mem 大小的配置。
parse_early_options()
函数最终会调用do_early_param()
函数。为了能在do_early_param()
函数中统一处理所有需 early 配置的选项,Linux kernel 设计了类似注册回调的机制。我们可以看到,do_early_param()
函数使用一个循环判断 cmdline 中是否存在 early option,若存在则调用其注册的回调函数p->setup_func(val)
。
void __init parse_early_options(char *cmdline)
{
parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,
do_early_param);
}
static int __init do_early_param(char *param, char *val,
const char *unused, void *arg)
{
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);
}
}
return 0;
}
那么如何注册一个 early option 以及指定回调处理函数呢?Linux kernel 使用宏early_param
(定义在文件 include/linux/init.h ,不具体展开)注册一个 early option 并指定回调。以早期内核从设备树文件获取内存大小为例,回调函数和注册都在 arch/arm64/mm/init.c 文件中,如下所示。
/*
* Limit the memory size that was specified via FDT.
*/
static int __init early_mem(char *p)
{
if (!p)
return 1;
memory_limit = memparse(p, &p) & PAGE_MASK;
pr_notice("Memory limited to %lldMB\n", memory_limit >> 20);
return 0;
}
early_param("mem", early_mem);
添加新 cmdline 参数并解析
仿照 quiet boot comandline 就很容易实现了。若需要添加自己的 cmdline 选项,只需要使用宏early_param
注册,并实现对应的函数即可。如下所示(在文件 init/main.c 中)。
static int __init quiet_kernel(char *str)
{
console_loglevel = CONSOLE_LOGLEVEL_QUIET;
return 0;
}
early_param("quiet", quiet_kernel);
init_call 机制
early_param 相关的是 init_call,对应于在系统启动过程对系统进行初始化的过程。
底层实现上,在内核镜像文件中,自定义一个段,这个段里面专门用来存放这些初始化函数的地址,内核启动时,只需要在这个段地址处取出函数指针,一个个执行即可。
Linux内核提供xxx_initcall_sync(fn)宏定义接口,驱动开发者只需要将驱动程序的 init_func 使用宏将驱动的初始化函数添加到了上述的段中即可,开发者完全不需要关心实现细节。
对于各种各样的驱动而言,可能存在一定的依赖关系,需要遵循先后顺序来进行初始化,考虑到这个,linux也对这一部分做了分级(Level)处理。如下所示,可选择以下宏来定义各 initcall 的level来达到被依赖的先被调用的目的。
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
init_call 的函数调用路径如下:
kernel_init()
-> kernel_init_freeable()
-> do_basic_setup()
-> do_initcalls()
-> do_initcall_level()
-> do_one_initcall()
参考资料
[1]. how does the bootloader pass the kernel command line to the kernel?
[2]. 树莓派启动那些事(四)
[3]. elixir.bootlin
[4]. Last preparations before the kernel entry point
[5]. linux之early_param()和__setup
[6]. linux的initcall机制(针对编译进内核的驱动)
[7]. The initcall mechanism
Comments | NOTHING