Linux启动时对命令行参数的解析

来源:互联网 发布:网络型寻衅滋事罪 编辑:程序博客网 时间:2024/06/05 14:41

【参考】

<1>Linux启动bootargs参数分析:http://blog.chinaunix.net/u3/99423/showart_2213279.html
<2>Linux 2.6内核启动传递命令行的过程分析:http://soft.zdnet.com.cn/software_zone/2007/1017/561631.shtml

【环境】
    Linux内核版本:V2.6.20
    
【简介】
    在嵌入式系统中,我们经常在bootloader中设置启动命令行参数,以传递给内核,如在u-boot中,大家比较熟悉的启动命令行参数可能为:
        bootargs=root=/dev/ram rw console=tty0,115200 initrd=0x30800000,0x320000 mem=64M
    这个参数告诉了内核串口的配置,根文件系统挂载方式为ramdisk形式,以及其地址等。
    这些参数在内核中是如何得到解析的呢?

【梦开始的地方--追忆篇】
    参看文章《Linux内核启动参数的传递》可以知道,命令参数属于tag值为(define ATAG_CMDLINE 0x54410009 )的tag参数,对该tag解析的结构体定义为:(注意:struct tag结构为为联合体定义形式,以下为了描述清楚而将其简化了表述)
    struct tag {
        struct tag_header hdr;
        struct tag_cmdline cmdline;
    }
    其中:
    struct tag_cmdline {
        char cmdline[1]; /* this is the minimum size */
    };

 

    内核在main()->setup_arch()->parse_tags()->parse_tag()中调用了parse_tag_cmdline()函数,该函数如下:
    
    static int __init parse_tag_cmdline(const struct tag *tag)
    {
        strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);
        return 0;
    }
    呵呵,该函数实际上将tag结构体中传递的参数拷贝到了default_command_line这个全局变量中,这个全局变量的定义在:
    arch/arm/kernel/setup.c#L111
    static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE   

 

    现在,命令行已经被保存了,接下来的工作就是开始解析。Linux的设计者们将命令行的解析搞得挺复杂,将解析过程分为了3部分,弄得人晕头转向,其实啊,估计是前面的设计没做好,功能不够强大,满足不了后来发展的需要了,所有又进行了扩展,但又得兼容以前的设计,修修补补,才弄得如此的繁琐;人非圣贤,一个这么庞大的软件,不可能是一步到位的啦~~

【Part1--命令行解析】
    还是在main()->setup_arch()中,我们观察如下部分:
    void __init setup_arch(char **cmdline_p)
    {
        char *from = default_command_line;
        //......
        
       memcpy(saved_command_line, from, COMMAND_LINE_SIZE);
       saved_command_line[COMMAND_LINE_SIZE-1] = '/0';
       parse_cmdline(cmdline_p, from);

 

       //......
    }
    显而易见了,该部分的解析通过调用函数parse_cmdline()进行,它也在arch/arm/kernel/setup.c中,我们进入她。
    这个函数干了些啥呢?1:她肯定要解析命令行了(废话,要不函数名白起的......);2:她还对所有的原始的命令行进行了预解析,将原始字符串命令行的所有命令的首地址存入了一个char**的数组中,后来的某个解析函数会用到,我们先看她的第一个功能部分。
    
    功能1即从参数表中寻找是否有与命令行中的命令相匹配的,有则调用相应的处理函数,没有则跳到下一个可能是命令语句的地方继续解析,直到这个命令行字符串解析完毕。

    通过在for循环中我们看到,参数表存在了extern struct early_params __early_begin的首地址当中,这个地方存了什么东西呢?
    其实这里用到了linux一贯使用的伎俩,我们稍微跟一下:
    在/arch/arm/kernel/vmlinux.lds.S#L44中有:
    __early_begin = .;
    *(.early_param.init)
    __early_end = .;
    在include/asm-arm/setup.h#L222中有:
    
    /*
    * Early command line parameters.
    */
    struct early_params {
        const char *arg;
        void (*fn)(char **p);
    };

    #define __early_param(name,fn)                                  /
    static struct early_params __early_##fn __attribute_used__      /
    __attribute__((__section__(".early_param.init"))) = { name, fn }

 

    而在整个内核代码中可以找到如下的定义:
    __early_param("initrd=", early_initrd);
    __early_param("mem=", early_mem);
    __early_param("nocache", early_nocache);
    ......
    不熟悉这种机制的朋友们注意了,以上部分,相当于通过宏__early_param,在.early_param.init段中放了3个(当然不止3个,我们仅用了3个举例)early_params 结构体,其arg值分别为:initrd, mem, nocache。

    那么函数中的for循环就好理解了,for循环体部分:
    for (p = &__early_begin; p < &__early_end; p++) {
        int len = strlen(p->arg);

        if (memcmp(from, p->arg, len) == 0) {
            if (to != command_line)
                to -= 1;
            from += len;
            p->fn(&from);

        while (*from != ' ' && *from != '/0')
            from++;
        break;
        }
    }
    遍历了所有的全局early_params变量,寻找命令行中命令名是否有与p->arg匹配的,有则调用相关的处理函数。
    如我们熟悉的一个命令行部分,mem=64M,则在该处该参数得到解析,会调用early_mem()处理函数进行处理。
    
    至于功能2,大家看看就好理解了,即对所有的原始的命令行进行了预解析,将原始字符串命令行的所有命令的首地址存入了一个char**的数组中,该数组为传递进来的参数:char **cmdline_p。

    至此呢,阶段1命令行的参数就完成了。其实呢,在这阶段,解析的参数很有限,如解析的是mem和initrd的部分,大部分参数都没有得到解析的。


 

【Part2--命令行解析】
    该部分的解析在main()->parse_early_param(),我们进入她瞧瞧。发现函数不长,这真少见,所以干脆贴出来了:
    
    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, saved_command_line, COMMAND_LINE_SIZE);
            parse_args("early options", tmp_cmdline, NULL, 0, do_early_param);
            done = 1;
    }
    这部分很简单,将全局变量saved_command_line拷贝到tmp_cmdline,然后调用parse_args()进行解析。(什么?saved_command_line是什么?没这么健忘吧,自己往前看找去,在setup_arch()函数中......),这里值得注意的是,该函数进行解析的还是原始的字符串命令行。
    我们继续进入parse_args()函数:
    ......经过一连串的尾行,我们终于发现解析过程是在do_early_param()这个函数中进行的(parse_args()->parse_arg()->do_early_param()),跟进去看看:
    
    static int __init do_early_param(char *param, char *val)
    {
        struct obs_kernel_param *p;

        for (p = __setup_start; p < __setup_end; p++) {
            if (p->early && strcmp(param, p->str) == 0) {
            if (p->setup_func(val) != 0)
                printk(KERN_WARNING
                  "Malformed early option '%s'/n", param);
            }
        }
        /* We accept everything at this stage. */
        return 0;
    }
    在这里我们找到了:extern struct obs_kernel_param __setup_start[], __setup_end[];看到她我们又乐了,故伎重演嘛,和前面描述参数表定义形式一模一样,不过我们还是跟一下:
    在arch/arm/kernel/vmlinux.lds.S#L41有:  
    __setup_start = .;
    *(.init.setup)
    __setup_end = .;
    在include/linux/init.h#L143有:
    struct obs_kernel_param {
        const char *str;
        int (*setup_func)(char *);
        int early;
    };
    #define __setup_param(str, unique_id, fn, early)                        /
        static char __setup_str_##unique_id[] __initdata = str; /
        static struct obs_kernel_param __setup_##unique_id      /
                __attribute_used__                              /
                __attribute__((__section__(".init.setup")))     /
                __attribute__((aligned((sizeof(long)))))        /
                = { __setup_str_##unique_id, fn, early }

    
    #define __setup_null_param(str, unique_id)                      /
        __setup_param(str, unique_id, NULL, 0)

    #define __setup(str, fn)                                        /
        __setup_param(str, fn, fn, 0)

    #define __obsolete_setup(str)                                   /
        __setup_null_param(str, __LINE__)

    #define early_param(str, fn)                                    /
        __setup_param(str, fn, fn, 1)
    
    这一类型的参数定义非常的丰富,其可使用4种宏来定义:__setup_null_param,__setup,__obsolete_setup,early_param(一般不直接使用宏__setup_param)。
    如我们很常见的参数:console=ttyS0,115200,在该处就能得到解析,其注册的地方为:printk.c
        __setup("console=", console_setup);

    至此呢,第二阶段的命令行参数解析过程就完成了。看起来已经可以定义很多参数了嘛,应该够用了啊。可是人的欲望是无限的,Linux的开发者还觉得不够用,因此还有第三阶段的参数解析过程。

 

【Part3--命令行解析】

 

    紧接在main()->parse_early_param()后面,main()->parse_args即是第三阶段的解析:
    
    parse_args("Booting kernel", command_line, __start___param,
      __stop___param - __start___param,
      &unknown_bootoption);
    
    注意了,该阶段进行解析的对象为command_line,她是什么呢?其实是在第一阶段解析时生成的,还记得吗?__start___param和__stop___param 又是什么呢?呵呵,她终于换招了,要不每次都使用相同招式,一下就被我们看穿了,多没意思是吧~~我们慢慢看这次出的是什么招.....
    
    她们的定义如下:extern struct kernel_param __start___param[], __stop___param[];
    
    kernel_param结构体的定义是:
    struct kernel_param {
        const char *name;
        unsigned int perm;
        param_set_fn set;
        param_get_fn get;
        void *arg;
    };

    在/include/asm-generic/vmlinux.lds.h中有:
    
        __param : AT(ADDR(__param) - LOAD_OFFSET) {                     /
                VMLINUX_SYMBOL(__start___param) = .;                    /
                *(__param)                                              /
                VMLINUX_SYMBOL(__stop___param) = .;                     /
                VMLINUX_SYMBOL(__end_rodata) = .;                       /
        }    
    
    在/include/linux/moduleparam.h#L65中有:
    
    #define __module_param_call(prefix, name, set, get, arg, perm)          /
        /* Default value instead of permissions? */                     /
        static int __param_perm_check_##name __attribute__((unused)) =  /
        BUILD_BUG_ON_ZERO((perm) < 0 || (perm) > 0777 || ((perm) & 2)); /
        static char __param_str_##name[] = prefix #name;                /
        static struct kernel_param const __param_##name                 /
        __attribute_used__                                              /
        __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) /
        = { __param_str_##name, perm, set, get, arg }

    #define module_param_call(name, set, get, arg, perm)                          /
        __module_param_call(MODULE_PARAM_PREFIX, name, set, get, arg, perm)

    后面还有针对module_param_named,module_param_string的宏定义......
    
    很复杂是吧?看不懂是吧?很高兴的告诉你,我也看不懂,或者我宁愿看不懂也不告诉你!不过这不影响我们对这一步的理解,内核中这些参数是怎么定义的,我们随便找找:
    在net/ipv4/netfilter/nf_nat_ftp.c#L176,有:module_param_call(ports, warn_set, NULL, NULL, 0);
    在drivers/usb/core/usb.c中有:__module_param_call("", nousb, param_set_bool, param_get_bool, &nousb, 0444);

    关于__start___param部分先停一停,我们先回到解析的过程中,回到main()->parse_args()函数:
    int parse_args(const char *name,char *args,struct kernel_param *params,unsigned num,int (*unknown)(char *param, char *val))
    {
        char *param, *val;
        //......
        while (*args) {
            //......
            args = next_arg(args, &param, &val);    //解析获得参数名,以及参数值
            //......
            ret = parse_one(param, val, params, num, unknown);    //调用该函数继续解析
            //......
        }
    }
    进入parse_one()函数:
    不难理解,其第一步也是在参数表中寻找,看参数名是否有匹配的,有的话则调用set函数,如下所示:
    
    /* Find parameter */
    for (i = 0; i < num_params; i++) {
        if (parameq(param, params[i].name)) {
            DEBUGP("They are equal!  Calling %p/n",
              params[i].set);
            return params[i].set(val, &params[i]);
        }
    }
    
    而对于参数表中没有的参数呢,则调用handle_unknown()函数进行处理,这个函数是传进来的参数,她在init/main.c中:
    static int __init unknown_bootoption(char *param, char *val);
    对于这个函数的作用呢,参考文献<1>中的描述:

 

    //---------------------//参考文献<1>

    obsolette_checksetup函数,这个函数内部的处理和parse_early_param()类似,所以这里就不详细解释了。
    if (obsolete_checksetup(param))
        return 0;
 
    对于既不是param,在handle_unknown中又不是setup形式的参数字符串,但设置了参数值。就将其放置在系统启动后的环境变量全局数组                envp_init[]中的同名参数或空环境变量中。
    对于没有设置参数值的参数字符串就将其传给argv_init[]中同名参数或空参数。

 

    //---------------------//

    至此呢,所有的解析过程就完毕了,真的是挺长挺复杂的。

    Linux中有很多定义好的参数可以设置,当然我们也可以自己加入参数,让其成为可设置的启动参数。当前版本的Linux推荐的是使用阶段3中的方式对参数进行解析,加入在这个阶段进行解析的参数的方式,可参考文献<2>中的描述。当然,我们也能加入能在阶段1以及阶段2中进入解析的参数啦^_^
    

 

 


 


 


 


 


 


    

原创粉丝点击