Linux 内核 | 内核启动 cmdline, early_param 和 initcall


本文主要记录 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

声明:一丁点儿|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Linux 内核 | 内核启动 cmdline, early_param 和 initcall


勿在浮沙筑高台,每天进步一丁点儿!