内核启动参数实现原理

基于上节内核下PCIE的扫描流程内容, 其中有个early_param关键字, 对于启动内核时能够使用诸多参数, 本人早就非常好奇, 趁着这个机会学习一下.

early_param

相关代码:

C 复制代码
#define __initconst __section(.init.rodata)
/*                                                   
 * NOTE: fn is as per module_param, not __setup!     
 * Emits warning if fn returns non-zero.             
 */                                                  
#define early_param(str, fn)                        \
    __setup_param(str, fn, fn, 1)                    
#ifndef MODULE

struct obs_kernel_param {
    const char *str;
    int (*setup_func)(char *);
    int early;
};

/*                                                                  
 * Only for really core code.  See moduleparam.h for the normal way.
 *                                                                  
 * Force the alignment so the compiler doesn't space elements of the
 * obs_kernel_param "array" too far apart in .init.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 }                    
#else /* MODULE */
#define __setup_param(str, unique_id, fn)   /* nothing */
#endif

可见early_param只是给内核用的, modules中不处理.

换句话说, 宏展开之后, 也就是放了一个名字以__setup_str_起始的静态指针常量, 单字节对齐, 置于.init.rodatasection中, 值为参数的名字. 又定义了一个存在于.init.setup中, 以long长度对齐的obs_kernel_param类型结构提, 名字以__setup_开头, 成员为刚刚的静态指针常量, 及函数名等.

那么关键点就是这个.init.rodata区域的值是怎么用作参数的呢?

已经获得的信息:

  1. 两个声明的section分别是.init.rodata.init.setup
  2. 结构体类型为obs_kernel_param.
  3. 变量名以__setup_str_起始.

先简单grep一下相关的变量名发现没找到:

bash 复制代码
[mxd@5 linux-4.19-loongson]$ grep __setup_str_ -rnI arch/loongarch/
[mxd@5 linux-4.19-loongson]$ 

cscope一下结构体obs_kernel_param:

html 复制代码
Cscope tag: obs_kernel_param
   #   line  filename / context / line
   1    241  include/linux/init.h <<GLOBAL>>
             struct obs_kernel_param {
   2    175  init/main.c <<GLOBAL>>
             extern const struct obs_kernel_param __setup_start[], __setup_end[];
   3    256  include/linux/init.h <<__setup_param>>
             static struct obs_kernel_param __setup_##unique_id \
   4    179  init/main.c <<obsolete_checksetup>>
             const struct obs_kernel_param *p;
   5    448  init/main.c <<do_early_param>>
             const struct obs_kernel_param *p;

可见, 第一个是结构提定义, 第二个和第三个是定义了一个全局数组, 第四个和第五个是在函数中调用. 总之在init/main.c中, 进去看看:

C 复制代码
extern const struct obs_kernel_param __setup_start[], __setup_end[];

/* 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;                                                   
}                                                               

其中obsolete_checksetup函数是在参数未知情况下的注册, 本文暂不讨论.

do_early_param中, 就是从先前声明的__setup_start__setup_end中取值, 那么这俩值又是从哪来的呢?

通过cscope并没有找到相应的定义, 这时只有两种可能, 一是该变量由链接脚本提供, 二是在非C源文件中定义, 比如汇编代码. 但无论如何, 这两者均在arch目录下. grep一下看看:

bash 复制代码
[mxd@5 linux-4.19-loongson]$ grep __setup_start -rn arch/loongarch/
arch/loongarch/kernel/vmlinux.lds:60: .init.data : AT(ADDR(.init.data) - 0) { KEEP(*(SORT(___kentry+*))) *(.init.data init.data.*) . = ALIGN(16); __setup_start = .; KEEP(*(.init.setup)) __setup_end = .; 

处理一下核心信息:

C 复制代码
. = ALIGN(16); 
__setup_start = .; 
KEEP(*(.init.setup)) 
__setup_end = .;

也就是在.init.setupsection中的内容, 也就是前面得到的线索1:

  1. 两个声明的section分别是.init.rodata.init.setup

继续分析代码, 从__setup_start开始遍历, 这里也有两种情况:

  1. 如果成员是early类型, 且成员中的变量名与传入的参数名一致
  2. 如果参数名是console, 且成员中的变量名是earlycon时.

这时则会调用该成员的setup_func函数.

举例

earlycon为例:

C 复制代码
/* early_param wrapper for setup_earlycon() */                          
static int __init param_setup_earlycon(char *buf)                       
{                                                                       
    int err;                                                            
                                                                        
    /* Just 'earlycon' is a valid param for devicetree and ACPI SPCR. */
    if (!buf || !buf[0]) {                                              
        if (IS_ENABLED(CONFIG_ACPI_SPCR_TABLE)) {                       
            earlycon_acpi_spcr_enable = true;                           
            return 0;                                                   
        } else if (!buf) {                                              
            return early_init_dt_scan_chosen_stdout();                  
        }                                                               
    }                                                                   
                                                                        
    err = setup_earlycon(buf);                                          
    if (err == -ENOENT || err == -EALREADY)                             
        return 0;                                                       
    return err;                                                         
}
early_param("earlycon", param_setup_earlycon);

根据前面对宏和代码的分析, 上面代码注册了一个obs_kernel_param类型的变量存在.init.setup中: __setup_param_setup_earlycon, 及一个存在于.init.rodata中的字符数组:__setup_str_param_setup_earlycon. 宏展开为:

C 复制代码
static const char __setup_str_param_setup_earlycon[] __section(.init.rodata)
    __aligned(1) = "earlycon";
    
static struct obs_kernel_param __setup_param_setup_earlycon __attribute__((__used__))
__section(.init.setup) __attribute__((aligned((sizeof(long)))))
    = {
        .str        = __setup_str_param_setup_earlycon, 
        .setup_func = param_setup_earlycon, 
        .early      = 1,
       };

所以当do_early_param函数中传入的参数是"earlycon"时, 其对应的early类型已经是1, 满足情景1:

  1. 如果成员是early类型, 且成员中的变量名与传入的参数名一致
  2. 如果参数名是console, 且成员中的变量名是earlycon时.

所以会执行setup_func函数, 也就是param_setup_earlycon函数, 执行的参数是do_early_param函数中传入的val, 这里假设上述代码中第一个分支便成立, 则执行earlycon_acpi_spcr_enable = true;return 0; 这其中的earlycon_acpi_spcr_enable是一个全局变量, 将在其他驱动中被调用, 此文不继续展开.

至此, 内核启动时, 若传入earlycon参数, 将会由此继续注册设备并生效.

参数格式

到现在, 我们已经明白了参数是如何注册和解析的, 那么参数的形式是怎么样的, 还并不清楚. 我们从新回到do_early_param函数. 通过cscope查看其调用过程:

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)                                                             
        return;                                                           
                                                                          
    /* All fall through to do_early_param. */                             
    strlcpy(tmp_cmdline, boot_command_line, COMMAND_LINE_SIZE);           
    parse_early_options(tmp_cmdline);                                     
    done = 1;                                                             
}                                                                         

除此之外还有一个early_platform_driver_register_all函数会调用parse_early_options, 经过查看这是特殊行为, 不继续展开了.

所以核心代码是从boot_command_line复制一份到tmp_cmdline并解析. 而boot_command_line通过early_memremap_ro(fw_arg1, COMMAND_LINE_SIZE)而来, 细追了一下发现是从一个物理地址传来, 也就是bootloader传递进来的, 暂不讨论.

解析过程其中涉及许多字符串, 锁, 及中断的解析, 不详细在本文展开, 在此仅作简单说明, 大致解析的格式按照如下顺序进行:

stateDiagram-v2 state 传入cmdline给parse_early_options { parse_early_options --> parse_args parse_args --> skip_spaces去除空格 skip_spaces去除空格 --> next_arg处理cmdline 去除双引号和空格,\n在'='位置处增加'\0'作为分隔符区分参数名和参数值,\n在空格处标记为参数值的结束位置,\n举例earlycon=uart,mmio,0x1fe001e0将会\n被拆分为earlycon\0和uart,mmio,0x1fe001e0\n前者作为参数名,后者作为参数值\n而类似single的参数将只有参数名,\n没有参数值 --> irqs_disabled关闭中断 irqs_disabled关闭中断 --> parse_one parse_one --> do_early_param传入对应的参数和值,\n进而执行相应的setup_func函数,\n如第一章内容 -- next_arg处理cmdline --> 去除双引号和空格,\n在'='位置处增加'\0'作为分隔符区分参数名和参数值,\n在空格处标记为参数值的结束位置,\n举例earlycon=uart,mmio,0x1fe001e0将会\n被拆分为earlycon\0和uart,mmio,0x1fe001e0\n前者作为参数名,后者作为参数值\n而类似single的参数将只有参数名,\n没有参数值 }

所以参数的格式将为:arg1=xxxarg, 千万不可使用空格随意拆分.

日常使用

除了根据代码学习增加参数的方式, 还要会看已经传递的参数:

bash 复制代码
mxd@mxd:~$ cat /proc/cmdline 
BOOT_IMAGE=/vmlinuz-linux root=UUID=a67aaec3-baec-4239-af30-f0a8ed5346ed rw loglevel=3 quiet

详细的功能即可在代码实现层进一步了解

参考文献

  1. 内核源码
相关推荐
soragui1 分钟前
【ChatGPT】OpenAI 如何使用流模式进行回答
linux·运维·游戏
白云coy1 小时前
Redis 安装部署[主从、哨兵、集群](linux版)
linux·redis
Logintern091 小时前
Linux如何设置redis可以外网访问—执行使用指定配置文件启动redis
linux·运维·redis
娶不到胡一菲的汪大东1 小时前
Linux之ARM(MX6U)裸机篇----1.开发环境搭建
linux·运维·服务器
fat house cat_1 小时前
Linux环境下使用tomcat+nginx部署若依项目
linux·nginx·tomcat
shada1 小时前
Ubuntu 24.04 APT源配置详解
linux·ubuntu
monstercl1 小时前
Ubuntu16.04手动升级内核到5.15
linux
vvw&1 小时前
如何在 Ubuntu 22.04 上安装和使用 Composer
linux·运维·服务器·前端·ubuntu·php·composer
F-2H1 小时前
C语言:指针3(函数指针与指针函数)
linux·c语言·开发语言·c++