linux 内核early_param module_param core_param使用详解

1 early_param 

1.1 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节点,替换了启动参数。

1.2 设备树解析

在内核的启动流程中,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);
    ...
}

1.3 函数调用关系总结

函数调用关系如下:

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()

 

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

1.5 添加新 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);

1.6 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()

2. module_param 

所有用module_param所定义的参数,都在/sys/module/***/parameters 目录下可以找到。例如在模块perfect中定义了一个参数loglevel, 那么相应的在/sys/module/perfect/parameters 下可以找到。

3. core_param

使用core_param定义参数,都在/sys/module/kernel/parameters/目录下可以找到。

参考:

https://www.cnblogs.com/downey-blog/p/10486653.html

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值