module_param_call宏的定义如下,
#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)
在kernel编译阶段,会把这个宏定义,加载到对应的链表中,KP(KP对应下面的结构kernel_param)中。
它的作用其实对于kernel和bootloader来说,起到关联的作用,bootloader可以通过传递参数到kernel,在kernel的启动过程中需要对cmdline参数进行解析,在解析的过程中,需要用到kernel_param的结构体,
kernel_param结构体的定义是:
struct kernel_param {
const char *name;
unsigned int perm;
param_set_fn set;
param_get_fn get;
void *arg;
};
既,一旦在解析的过程中,发现kernel_param的链表中的一项kernel_param的name与cmdline中的某个字符串xxx匹配,则会调用module_param_call宏中定义set函数,进行我们需要的操作。
如高通平台中,如果在cmdline中加入start_mode的话,在restart.c中定义一个module_param_call(start_mode,xxx,set),在启动的过程中,直接调用set.
下面的这篇文章主要是针对前期的解析cmdline进行分析。
【参考】
<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内核版本:V
2.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
static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE
现在,命令行已经被保存了,接下来的工作就是开始解析。Linux的设计者们将命令行的解析搞得挺复杂,将解析过程分为了
3部分,弄得人晕头转向,其实啊,估计是前面的设计没做好,功能不够强大,满足不了后来发展的需要了,所有又进行了扩展,但又得兼容以前的设计,修修补补,才弄得如此的繁琐;人非圣贤,一个这么庞大的软件,不可能是一步到位的啦~~
【Part1--命令行解析】
备注:先解析这个结构的cmdline
__early_begin = .;
*(.early_param.init)
__early_end = .;
具体如下
还是在
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中有:
/*
* 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--命令行解析】
备注:再解析下面结构的cmdline
__setup_start = .;
*(.init.setup)
__setup_end = .;
具体如下
该部分的解析在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有:
__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);
备注:__setup()可以在其他的函数定义,如printk中定义 __setup("console=", console_setup);,定义后,就会在编译的时候加到
__setup_start = .;
*(.init.setup)
__setup_end = .;
这个结构中,当上述解析发生时,则会调用其中的console_setup函数,进行console的设置。
至此呢,第二阶段的命令行参数解析过程就完成了。看起来已经可以定义很多参数了嘛,应该够用了啊。可是人的欲望是无限的,Linux的开发者还觉得不够用,因此还有第三阶段的参数解析过程。
【Part3--命令行解析】
备注:最后再解析下面结构的cmdline
struct kernel_param {
const char *name;
unsigned int perm;
param_set_fn set;
param_get_fn get;
void *arg;
};
具体如下:
紧接在
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中有:
#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, ¶m, &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, ¶ms[i]);
}
}
比如,如果定义module_param_call(ports, warn_set, NULL, NULL, 0);,如果在cmdline中有ports的cmdline,则会调用warn_set函数
而对于参数表中没有的参数呢,则调用
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中进入解析的参数啦^_^
http://blog.csdn.net/Eric_tao/article/details/5708894 Eric_tao的这篇文章解析的很好,大家可以仔细看看