zoukankan      html  css  js  c++  java
  • linux kernel的cmdline參数解析原理分析

    利用工作之便,今天研究了kernel下cmdline參数解析过程。记录在此。与大家共享。转载请注明出处。谢谢。



    Kernel 版本:3.4.55

    Kernel启动时会解析cmdline,然后依据这些參数如console root来进行配置执行。


    Cmdline是由bootloader传给kernel。如uboot。将须要传给kernel的參数做成一个tags链表放在ram中,将首地址传给kernel,kernel解析tags来获取cmdline等信息。

    Uboot传參给kernel以及kernel怎样解析tags能够看我的另一篇博文,链接例如以下:

    http://blog.csdn.net/skyflying2012/article/details/35787971


    今天要分析的是kernel在获取到cmdline之后怎样对cmdline进行解析。
    依据我的思路(时间顺序,怎样開始,怎样结束)。首先看kernel下2种參数的注冊。


    第一种是kernel通用參数。如console=ttyS0,115200  root=/rdinit/init等。

    这里以console为例。

    另外一种是kernel下各个driver中须要的參数,在写driver中,假设须要一些启动时可变參数。能够在driver最后增加module_param()来注冊一个參数。kernel启动时由cmdline指定该參数的值。

    这里以drivers/usb/gadget/serial.c中的use_acm參数为例(这个样例有点偏。。由于近期在调试usb虚拟串口)


    一 kernel通用參数

    对于这类通用參数,kernel留出单独一块data段,叫.ini.setup段。在arch/arm/kernel/vmlinux.lds中:

    .init.data : {
      *(.init.data) *(.cpuinit.data) *(.meminit.data) *(.init.rodata) *(.cpuinit.rodata) *(.meminit.rodata) . = ALIGN(32); __dtb_star
     . = ALIGN(16); __setup_start = .; *(.init.setup) __setup_end = .;
      __initcall_start = .; *(.initcallearly.init) __initcall0_start = .; *(.initcall0.init) *(.initcall0s.init) __initcall1_start =
      __con_initcall_start = .; *(.con_initcall.init) __con_initcall_end = .;
      __security_initcall_start = .; *(.security_initcall.init) __security_initcall_end = .;
      . = ALIGN(4); __initramfs_start = .; *(.init.ramfs) . = ALIGN(8); *(.init.ramfs.info)
     }

    能够看到init.setup段起始__setup_start和结束__setup_end

    .init.setup段中存放的就是kernel通用參数和相应处理函数的映射表。

    include/linux/init.h

    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 }
    
    #define __setup(str, fn)                    
        __setup_param(str, fn, fn, 0)
    /* 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)

    能够看出宏定义__setup以及early_param定义了obs_kernel_param结构体。该结构体存放參数和相应处理函数。存放在.init.setup段中。

    能够想象,假设多个文件里调用该宏定义,在链接时就会依据链接顺序将定义的obs_kernel_param放到.init.setup段中。

    console为例。在/kernel/printk.c中,例如以下:

    static int __init console_setup(char *str)
    {
    .......
    }
    __setup("console=", console_setup);

    __setup宏定义展开,例如以下:

    Static struct obs_kernel_param __setup_console_setup 
    __used_section(.init.setup) __attribute__((aligned((sizeof(long)))) = {
    .name = “console=”,
    .setup_func = console_setup,
    .early = 0
    }

    __setup_console_setup编译时就会链接到.init.setup段中,kernel执行时就会依据cmdline中的參数名与.init.setup段中obs_kernel_paramname对照。

    匹配则调用console-setup来解析该參数,console_setup的參数就是cmdlineconsole的值,这是后面參数解析的大体过程了。

     

    二 driver自己定义參数

    对于driver自己定义參数。kernel留出rodata段一部分,叫__param,在arch/arm/kernel/vmlinux.lds中。例如以下:

    __param : AT(ADDR(__param) - 0) { __start___param = .; *(__param) __stop___param = .; }

    该段放在.rodata段中。

    那该段中存放的是什么样的数据呢?

    Driver中使用module_param来注冊參数,跟踪这个宏定义。终于就会找到对__param段的操作函数例如以下:

    /* This is the fundamental function for registering boot/module
       parameters. */
    #define __module_param_call(prefix, name, ops, arg, perm, level)    
        /* Default value instead of permissions? */         
        static int __param_perm_check_##name __attribute__((unused)) =  
        BUILD_BUG_ON_ZERO((perm) < 0 || (perm) > 0777 || ((perm) & 2))  
        + BUILD_BUG_ON_ZERO(sizeof(""prefix) > MAX_PARAM_PREFIX_LEN);   
        static const char __param_str_##name[] = prefix #name;      
        static struct kernel_param __moduleparam_const __param_##name   
        __used                              
        __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) 
        = { __param_str_##name, ops, perm, level, { arg } }
    ........
    #define module_param(name, type, perm)              
        module_param_named(name, name, type, perm)
    
    #define module_param_named(name, value, type, perm)            
        param_check_##type(name, &(value));                
        module_param_cb(name, ¶m_ops_##type, &value, perm);        
        __MODULE_PARM_TYPE(name, #type)
    
    #define module_param_cb(name, ops, arg, perm)                     
        __module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1)

    driver/usb/gadget/serial.c中的use_acm为例,例如以下:

    static bool use_acm = true;
    module_param(use_acm, bool, 0);

    Module_param展开到__module_param_call。例如以下:

    Static bool use_acm = true;
    Param_check_bool(use_acm, &(use_acm));
    __module_param_call(MODULE_PARAM_PREFIX, use_acm, ¶m_ops_bool, &(use_acm, 0, -1));
    __MODULE_PARAM_TYPE(use_acm, bool);

    __module_param_call展开,能够看到是定义了结构体kernel_param,例如以下:

    Static struct kernel_param __moduleparam_const __param_use_acm 
     __used   __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) = {
    .name = MODULE_PARAM_PREFIX#use_acm,
    .ops = ¶m_ops_bool,
    .Perm=0,
    .level = -1.
    .arg = &use_acm
    }

    非常清楚,跟.init.setup段一样,kernel链接时会依据链接顺序将定义的kernel_param放在__param段中。

    Kernel_param3个成员变量须要注意:

    (1)

    ops=param_ops_bool,是kernel_param_ops结构体,定义例如以下:

    struct kernel_param_ops param_ops_bool = {
        .set = param_set_bool,
        .get = param_get_bool,
    };

    2个成员函数分别去设置和获取參数值

    kernel/param.c中能够看到kernel默认支持的driver參数类型有bool byte short ushort int uint long ulong string(字符串) charp(字符串指针)array等。

    对于默认支持的參数类型,param.c中提供了kernel_param_ops来处理相应类型的參数。

    2

    Arg = &use_acm,宏定义展开,能够看到arg中存放use_acm的地址。

    參数设置函数param_set_boolconst char *val, const struct kernel_param *kp

    val值设置到kp->arg地址上。也就是改变了use_acm的值,从而到达传递參数的目的。

    (3)

    .name=MODULE_PARAM_PREFIX#use_acm,定义了该kernel_paramname

    MODULE_PARAM_PREFIX非常重要,定义在include/linux/moduleparam.h中:

    * You can override this manually, but generally this should match the
       module name. */
    #ifdef MODULE
    #define MODULE_PARAM_PREFIX /* empty */
    #else
    #define MODULE_PARAM_PREFIX KBUILD_MODNAME "."
    #endif

    假设我们是模块编译(make modules)。则MODULE_PARAM_PREFIXempty

    在模块传參时,參数名为use_acm,如insmod g_serial.ko use_acm=0

    正常编译kernelMODULE_PARAM_PREFIX为模块名+”.”

    假设我们在传參时不知道自己的模块名是什么。能够在自己的驱动中加打印,将MODULE_PARAM_PREFIX打印出来,来确定自己驱动的模块名。

    所以这里将serial.c编入kernel,依据driver/usb/gadget/Makefile,例如以下:

    g_serial-y          := serial.o
    ....
    obj-$(CONFIG_USB_G_SERIAL)  += g_serial.o

    终于是生成g_serial.o,模块名为g_serial.ko.name = g_serial.use_acm

    kernel传參时,该參数名为g_serial.use_acm

    这样处理防止kernel下众多driver中出现重名的參数。

     

    能够看出,对于module_param注冊的參数。假设是kernel默认支持类型,kernel会提供參数处理函数。

    假设不是kernel支持參数类型。则须要自己去实现param_ops##type了。

    这个能够看drivers/video/uvesafb.c中的scroll參数的注冊(又有点偏。。。

    无意间找到的)。

     

    參数注冊是在kernel编译链接时完毕的(链接器将定义结构体放到.init.setup__param中)

    接下来须要分析kernel启动时怎样对传入的cmdline进行分析。


    三 kernel对cmdline的解析

    依据我之前写的博文可知,start_kernelsetup_arch中解析tags获取cmdline,复制到boot_command_line中。我们接着往下看start_kernel

    调用setup_command_line,将cmdline拷贝2份,放在saved_command_line static_command_line

    以下调用parse_early_param(),例如以下:

    void __init parse_early_options(char *cmdline)
    {
        parse_args("early options", cmdline, NULL, 0, 0, 0, do_early_param);
    }
    
    /* Arch code calls this early on, or if not, just before other parsing. */
    void __init parse_early_param(void)
    {
        static __initdata int done = 0;
        static __initdata char tmp_cmdline[COMMAND_LINE_SIZE];
    
        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;
    }
    Parse_early_param拷贝cmdline到tmp_cmdline中一份。终于调用parse_args,例如以下:
    
    /* Args looks like "foo=bar,bar2 baz=fuz wiz". */
    int parse_args(const char *name,
               char *args,
               const struct kernel_param *params,
               unsigned num,
               s16 min_level,
               s16 max_level,
               int (*unknown)(char *param, char *val))
    {
        char *param, *val;
    
        pr_debug("Parsing ARGS: %s
    ", args);
    
        /* Chew leading spaces */
        args = skip_spaces(args);
    
        while (*args) {
            int ret;
            int irq_was_disabled;
    
            args = next_arg(args, ¶m, &val);
            irq_was_disabled = irqs_disabled();
            ret = parse_one(param, val, params, num,
                    min_level, max_level, unknown);
            if (irq_was_disabled && !irqs_disabled()) {
                printk(KERN_WARNING "parse_args(): option '%s' enabled "
                        "irq's!
    ", param);
            }
            switch (ret) {
            case -ENOENT:
                printk(KERN_ERR "%s: Unknown parameter `%s'
    ",
                       name, param);
                return ret;
            case -ENOSPC:
                printk(KERN_ERR
                       "%s: `%s' too large for parameter `%s'
    ",
                       name, val ?: "", param);
                return ret;
            case 0:
                break;
            default:
                printk(KERN_ERR
                       "%s: `%s' invalid for parameter `%s'
    ",
                       name, val ?: "", param);
                return ret;
            }
        }
    
        /* All parsed OK. */
        return 0;
    }
    .....
    void __init parse_early_options(char *cmdline)
    {
        parse_args("early options", cmdline, NULL, 0, 0, 0, do_early_param);
    }

    Parse_args遍历cmdline。依照空格分割获取參数,对全部參数调用next_arg获取參数名param和參数值val

    console=ttyS0,115200。则param=consoleval=ttyS0,115200。调用parse_one。例如以下:

    static int parse_one(char *param,
                 char *val,
                 const struct kernel_param *params,
                 unsigned num_params,
                 s16 min_level,
                 s16 max_level,
                 int (*handle_unknown)(char *param, char *val))
    {
        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->set != param_set_bool
                    && params[i].ops->set != param_set_bint)
                    return -EINVAL;
                pr_debug("They are equal!  Calling %p
    ",
                       params[i].ops->set);
                mutex_lock(¶m_lock);
                err = params[i].ops->set(val, ¶ms[i]);
                mutex_unlock(¶m_lock);
                return err;
            }
        }
    
        if (handle_unknown) {
            pr_debug("Unknown argument: calling %p
    ", handle_unknown);
            return handle_unknown(param, val);
        }
    
        pr_debug("Unknown argument `%s'
    ", param);
        return -ENOENT;
    }

    由于从parse_early_options传入的num_params=0,所以parse_one是直接走的最后handle_unknown函数。该函数是由parse-early_options传入的do_early_param。例如以下:

    static int __init do_early_param(char *param, char *val)
    {
        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)
                    printk(KERN_WARNING
                           "Malformed early option '%s'
    ", param);
            }
        }
        /* We accept everything at this stage. */
        return 0;
    }

    Do_early_param遍历.init.setup段。假设有obs_kernel_paramearly1。或cmdline中有console參数而且obs_kernel_paramearlycon參数,则会调用该obs_kernel_paramsetup函数来解析參数。

    Do_early_param会对cmdline中优先级较高的參数进行解析。我翻了下kernel源代码找到一个样例。就是arch/arm/kernel/early_printk.c,利用cmdline參数earlyprintk来注冊最早的一个console,有兴趣大家能够參考下。

    假设想kernel启动中尽早打印输出。方便调试。能够注冊strearlyconobs_kernel_param

    在其setup參数处理函数中register_console。注冊一个早期的console。从而是printk信息正常打印。这个在后面我还会总结一篇kernel打印机制来说这个问题。

    do_early_param是为kernel中须要尽早配置的功能(如earlyprintk  earlycon)做cmdline的解析。

    Do_early_param就说道这里,该函数并没有处理我们常常使用的kernel通用參数和driver自己定义參数。接着往下看。代码例如以下:

        setup_arch(&command_line);
        mm_init_owner(&init_mm, &init_task);
        mm_init_cpumask(&init_mm);
        setup_command_line(command_line);
        setup_nr_cpu_ids();
        setup_per_cpu_areas();
        smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
    
        build_all_zonelists(NULL);
        page_alloc_init();
    
        printk(KERN_NOTICE "Kernel command line: %s
    ", boot_command_line);
        parse_early_param();
        parse_args("Booting kernel", static_command_line, __start___param,
               __stop___param - __start___param,
               -1, -1, &unknown_bootoption);

    Parse_early_param结束后,start_kernel调用了parse_args。这次调用,不像parse_early_param中调用parse_args那样kernel_param指针都为NULL。而是指定了.__param段。

    回到上面看parse_args函数,params參数为.__param段起始地址,numkernel_param个数。

    Min_level,max_level都为-1.unknown=unknown_bootoption

    Parse_args还是像之前那样。遍历cmdline,分割获取每一个參数的paramval,对每一个參数调用parse_one

    回看Parse_one函数源代码:

    1parse_one首先会遍历.__param段中全部kernel_param,将其name与參数的param对照,同名则调用该kernel_param成员变量kernel_param_opsset方法来设置參数值。

    联想前面讲driver自己定义參数样例use_acm,cmdline中有參数g_serial.use_acm=0,则在parse_one中遍历匹配在serial.c中注冊的__param_use_acm,调用param_ops_boolset函数,从而设置use_acm=0.

    (2)假设parse_args传给parse_onekernel通用參数,如console=ttyS0,115200。则parse_one前面遍历.__param段不会找到匹配的kernel_param。就走到后面调用handle_unknown。就是parse_args传来的unknown_bootoption。代码例如以下: 

    /*
     * Unknown boot options get handed to init, unless they look like
     * unused parameters (modprobe will find them in /proc/cmdline).
     */
    static int __init unknown_bootoption(char *param, char *val)
    {
        repair_env_string(param, val);
    
        /* Handle obsolete-style parameters */
        if (obsolete_checksetup(param))
            return 0;
    
        /* Unused module parameter. */
        if (strchr(param, '.') && (!val || strchr(param, '.') < val))
            return 0;
    
        if (panic_later)
            return 0;
    
        if (val) {
            /* Environment option */
            unsigned int i;
            for (i = 0; envp_init[i]; i++) {
                if (i == MAX_INIT_ENVS) {
                    panic_later = "Too many boot env vars at `%s'";
                    panic_param = param;
                }
                if (!strncmp(param, envp_init[i], val - param))
                    break;
            }
            envp_init[i] = param;
        } else {</span>
    <span style="font-size:14px;">        /* Command line option */
            unsigned int i;
            for (i = 0; argv_init[i]; i++) {
                if (i == MAX_INIT_ARGS) {
                    panic_later = "Too many boot init vars at `%s'";
                    panic_param = param;
                }
            }
            argv_init[i] = param;
        }
        return 0;
    }

    首先repair_env_string会将param val又一次组合为param=val形式。

    Obsolete_checksetup则遍历-init_setup段全部obs_kernel_param,如有param->strparam匹配,则调用param_>setup进行參数值配置。

    这里须要注意的一点是repair_env_stringparam又一次拼成了param=val形式。后面遍历匹配都是匹配的”param=”而不是“param”

    如之前分析kernel通用參数所举样例,__setup(“console=”, console_setup)

    Console=ttyS0115200,obsolete_checksetup是匹配前面console=,假设匹配,则跳过console=。获取到其值ttyS0,115200。调用其详细的setup函数来解析设置參数值。

    能够想象,parse_one对于parse_args传来的每一个cmdline參数都会将.__param以及-init.setup段遍历匹配,匹配到strname一致,则调用其相应的setsetup函数进行參数值解析或设置。

    Start_kernelParse_args结束,kernelcmdline就解析完毕。

     

    总结下kernel的參数解析:

    1kernel编译链接。利用.__param .init.setup段将kernel所需參数(driver及通用)和相应处理函数的映射表(obs_kernel_param  kernel_param结构体)存放起来。

    2Kernel启动,do_early_param处理kernel早期使用的參数(如earlyprintk earlycon

    (3)parse_argscmdline每一个參数都遍历__param .init.setup进行匹配,匹配成功,则调用相应处理函数进行參数值的解析和设置。


    另一点非常值得思考,kernel下对于这样的映射处理函数表方式还有非常多使用。

    比方之前博文中uboot传參给kernel,kernel对于不同tags的处理函数也是以该种方式来映射的。

    kernel下driver私有结构体的回调处理函数也有这个思想哇!










  • 相关阅读:
    1093 Count PAT's(25 分)
    1089 Insert or Merge(25 分)
    1088 Rational Arithmetic(20 分)
    1081 Rational Sum(20 分)
    1069 The Black Hole of Numbers(20 分)
    1059 Prime Factors(25 分)
    1050 String Subtraction (20)
    根据生日计算员工年龄
    动态获取当前日期和时间
    对计数结果进行4舍5入
  • 原文地址:https://www.cnblogs.com/tlnshuju/p/6851812.html
Copyright © 2011-2022 走看看