static struct tag *setup_commandline_tag(struct tag *params, char *cmdline)
{
if (!cmdline)
return params;
void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
ulong addr, ulong *len_ptr, int verify)
{
ulong len = 0, checksum;
ulong initrd_start, initrd_end;
ulong data;
void (*theKernel)(int zero, int arch, uint params);
image_header_t *hdr = &header;
bd_t *bd = gd->bd;
..........................
/************************************************************************
* Match a name / name=value pair
*
* s1 is either a simple 'name', or a 'name=value' pair.
* i2 is the environment index for a 'name2=value2' pair.
* If the names match, return the index for the value2, else NULL.
*/
kernel/params.c
static inline char dash2underscore(char c)
{
if (c == '-')
return '_';
return c;
}
static inline int parameq(const char *input, const char *paramname)
{
unsigned int i;
for (i = 0; dash2underscore(input[i]) == paramname[i]; i++)
if (input[i] == '\0')
return 1;
return 0;
}
我们再来看一下do_early_param函数,它在init/main.c中
static int __init do_early_param(char *param, char *val)
{
const struct obs_kernel_param *p;
这里的__setup_start和_-setup_end分别是.init.setup段的起始和结束的地址
for (p = __setup_start; p < __setup_end; p++) { 如果没有early标志则跳过
if ((p->early && strcmp(param, p->str) == 0) ||
(strcmp(param, "console") == 0 &&
strcmp(p->str, "earlycon") == 0)
) {
if (p->setup_func(val) != 0) 调用处理函数
printk(KERN_WARNING
"Malformed early option '%s'\n", param);
}
}
/* We accept everything at this stage. */
return 0;
}
在do_early_param函数中for循环遍历obs_kernel_param数组,这里首先要说明一下struct obs_kernel_param结构及这个数组的由来。
obs_kernel_param结构定义在include/linux/init.h文件中
218 struct obs_kernel_param {
219 const char *str;
220 int (*setup_func)(char *);
221 int early;
222 };
前两个参数很简单,一个是key,一个是处理函数。最后一个参数其实也就是类似于优先级的一个标志flag,因为传递给内核的参数中,有一些需要比另外的一些更早的解析。(这也就是为什么early_param和setup传递的最后一个参数的不同的原因了,后面会讲到)。
先说一下系统启动时对bootloader传递参数的初始化,即linux启动参数的实现,启动参数的实现,我们知道boot传递给内核的参数都是 "name_varibale=value"这种形式的,如下:
CONFIG_CMDLINE="root=/dev/mtdblock3 init=/linuxrc console=ttySAC0,115200 mem=64M"
那么内核如何知道传递进来的参数该怎么去处理呢?
内核是通过__setup宏或者early_param宏来将参数与参数所处理的函数相关联起来的。我们接下来就来看这个宏以及相关的结构的定义:
include/linux/init.h
230 #define __setup_param(str, unique_id, fn, early) \
231 static const char __setup_str_##unique_id[] __initconst \
232 __aligned(1) = str; \
233 static struct obs_kernel_param __setup_##unique_id \
234 __used __section(.init.setup) \
235 __attribute__((aligned((sizeof(long))))) \
236 = { __setup_str_##unique_id, fn, early }
237
238 #define __setup(str, fn) \
239 __setup_param(str, fn, fn, 0)
240
241 /* NOTE: fn is as per module_param, not __setup! Emits warning if fn
242 * returns non-zero. */
243 #define early_param(str, fn) \
244 __setup_param(str, fn, fn, 1)
可见,__setup宏的作用是使用str值和函数句柄fn初始化一个static结构体 obs_kernel_param。该结构体在链接后存在于.init.setup段。其实该段也就是所有内核参数所在的处。该段的起始地址是__setup_start,结束地址为__setup_end。同样的还有一个early_param宏,也是设置一个内核参数,不过改参数是早期启动时相关的。
看起来很复杂。 首先setup宏第一个参数是一个key。比如"netdev="这样的,而第二个参数是这个key对应的处理函数。这里要注意相同的handler能联系到不同的key。__setup与early_param不同的是,early_param宏注册的内核选项必须要在其他内核选项之前被处理。在函数 start_kernel中,parse_early_param处理early_param定义的参数,parse_args处理__setup定义的参数。
early_param和setup唯一不同的就是传递给__setup_param的最后一个参数,这个参数下面会说明,而接下来 _setup_param定义了一个struct obs_kernel_param类型的结构,然后通过_section宏,使这个变量在链接的时候能够放置在段.init.setup(后面会详细介绍内核中的这些初始化段).
比如说定义一个内核参数来实现对init程序的指定。见init/main.c中
1,所有的系统启动参数都是由形如
static int __init init_setup(char *str)的函数来支持的
static int __init init_setup(char *str)
{
unsigned int i;
execute_command = str;
for (i = 1; i < MAX_INIT_ARGS; i++)
argv_init[i] = NULL;
return 1;
}
__setup("init=", init_setup);
注:(include/linux/init.h):
#define __init __section(.init.text) __cold notrace申明所有的启动参数支持函数都放入.init.text段
2.1,用__setup宏来导出参数的支持函数
__setup("init=", init_setup);
展开后就是如下的形式
static const char __setup_str_init_setup[] __initdata = "init=";
static struct obs_kernel_param __setup_init_setup
__used __section__(".init.setup")
__attribute__((aligned((sizeof(long)))))
= { __setup_str_init_setup, init_setup, 0 };//"init=",init_setup,0
也就是说,启动参数(函数指针)被封装到obs_kernel_param结构中,
所有的内核启动参数形成内核映像.init.setup段中的一个
obs_kernel_param数组,而在do_early_param函数中for循环就是遍历obs_kernel_param数组,首先看是否
设置early,如果有设置并查找到到对就的key,就调用相关early_param函数处理。
用early_param宏来申明需要'早期'处理的启动参数,例如在
arch/arm/kernel/setup.c就有如下的申明:
468 early_param("mem", early_mem);
展开后和__setup是一样的只是early参数不一样,因此会在do_early_param
中被处理
443 static int __init early_mem(char *p)
444 {
445 static int usermem __initdata = 0;
446 unsigned long size, start;
447 char *endp;
448
449 /*
453 */
454 if (usermem == 0) {
455 usermem = 1;
456 meminfo.nr_banks = 0;
457 }
458
459 start = PHYS_OFFSET;
460 size = memparse(p, &endp);
461 if (*endp == '@')
462 start = memparse(endp + 1, NULL);
463
464 arm_add_memory(start, size);
465
466 return 0;
467 }
init/main.c中启动参数申明列表:
early_param("nosmp", nosmp);
early_param("nr_cpus", nrcpus);
early_param("maxcpus", maxcpus);
__setup("reset_devices", set_reset_devices);
early_param("debug", debug_kernel);
early_param("quiet", quiet_kernel);
early_param("loglevel", loglevel);
__setup("init=", init_setup);
__setup("rdinit=", rdinit_setup);
arch/arm/kernel/setup.c中启动参数申明列表:
__setup("fpe=", fpe_setup);
early_param("mem", early_mem);
early_param("elfcorehdr", setup_elfcorehdr);
注意在2.6.36版本中,除在start_kernel中的start_arch函数中第820调用parse_early_param()外,还在start_arch函数后的580行和581行分别执行下面代码再次解释命令行参数
579 printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
580 parse_early_param();
581 parse_args("Booting kernel", static_command_line, __start___param,
582 __stop___param - __start___param,
583 &unknown_bootoption);
对于parse_early_param我们已经很熟悉,在这里不知道为什么它再做的一次,下面看第581行,我们再来看看parse_args函数,见前面的代码分解出命令行的参数后,会调用
ret = parse_one(param, val, params, num, unknown);
相当于:
ret = parse_one(param, val, __start___param,__stop___param - __start___param,&unknown_bootoption);
这里提到了两个参数__start___param和__stop___param,它涉及到kernel_param 结构体和参数的存储
关于内核参数结构体的定义,见include/linux/moduleparam.h
99 #define module_param(name, type, perm) \
100 module_param_named(name, name, type, perm)
113 #define module_param_named(name, value, type, perm) \
114 param_check_##type(name, &(value)); \
115 module_param_cb(name, ¶m_ops_##type, &value, perm); \
116 __MODULE_PARM_TYPE(name, #type)
126 #define module_param_cb(name, ops, arg, perm) \
127 __module_param_call(MODULE_PARAM_PREFIX, \
128 name, ops, arg, __same_type((arg), bool *), perm )
142 #define __module_param_call(prefix, name, ops, arg, isbool, perm) \
143 /* Default value instead of permissions? */ \
144 static int __param_perm_check_##name __attribute__((unused)) = \
145 BUILD_BUG_ON_ZERO((perm) < 0 || (perm) > 0777 || ((perm) & 2)) \
146 + BUILD_BUG_ON_ZERO(sizeof(""prefix) > MAX_PARAM_PREFIX_LEN); \
147 static const char __param_str_##name[] = prefix #name; \
148 static struct kernel_param __moduleparam_const __param_##name \
149 __used \
150 __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \
151 = { __param_str_##name, ops, perm, isbool ? KPARAM_ISBOOL : 0, \
152 { arg } }
这里也就是填充了 struct kernel_param的结构体,并将这个变量标记为__param段,以便于链接器将此变量装载到指定的段,和结构体 obs_kernel_param类似,该宏函数保持所有实例存在于__param段。该段的起始地址是__start___param,结束地址为__stop___param。具体链接脚本在include/asm-generic/vmlinux.lds.h
350 /* Built-in module parameters. */ \
351 __param : AT(ADDR(__param) - LOAD_OFFSET) { \
352 VMLINUX_SYMBOL(__start___param) = .; \
353 *(__param) \
354 VMLINUX_SYMBOL(__stop___param) = .; \
355 . = ALIGN((align)); \
356 VMLINUX_SYMBOL(__end_rodata) = .; \
357 } \
358 . = ALIGN((align));
这里给个例子net/ipv4/netfilter/nf_nat_irc.c
static int warn_set(const char *val, struct kernel_param *kp)
{
printk(KERN_INFO KBUILD_MODNAME
": kernel >= 2.6.10 only uses 'ports' for conntrack modules\n");
return 0;
}
module_param_call(ports, warn_set, NULL, NULL, 0);
处理module_param_call之外,还有core_param也可以定义内核参数,不过内核参数不可以模块化,也不可以使用前缀命名(如“printk.”)。
接下来我们来看struct kernel_param这个结构:
include/linux/moduleparam.h
struct kernel_param;
/* Flag bits for kernel_param.flags */
#define KPARAM_ISBOOL 2
struct kernel_param {
const char *name;
const struct kernel_param_ops *ops;
u16 perm;
u16 flags;
union { 传递给上面kernel_param_ops中两个函数的参数
void *arg;
const struct kparam_string *str;
const struct kparam_array *arr;
};
};
其中,联合体内定义的三个成员,第一个其实是字符类型的封装。
/* Special one for strings we want to copy into */
struct kparam_string {
unsigned int maxlen;
char *string;
};
第二个是数组类型的封装。
/* Special one for arrays */
struct kparam_array
{
unsigned int max;
unsigned int *num;
const struct kernel_param_ops *ops;
unsigned int elemsize;
void *elem;
};
还剩下的常字符串类型成员name为内核参数的名称,而perm为权限????。
同时数组类型的封装kernel_param_ops中还定义了两个方法,以函数指针存在。分别是设置和读取操作。
struct kernel_param_ops {
/* Returns 0, or -errno. arg is in kp->arg. */
int (*set)(const char *val, const struct kernel_param *kp);设置参数的函数
/* Returns length written or -errno. Buffer is 4k (ie. be short!) */
int (*get)(char *buffer, const struct kernel_param *kp);读取参数的函数
/* Optional function to free kp->arg when module unloaded. */
void (*free)(void *arg);
};
现在我们再回到parse_one函数中,我们前面有部分没有分析,现在再来看一下
static int parse_one(char *param,
char *val,
const struct kernel_param *params,
unsigned num_params,
int (*handle_unknown)(char *param, char *val))
{
unsigned int i;
int err;
如果是early_param则直接跳过这步,而非early的,则要通过这步来设置一些内置模块的参数。
/* Find parameter */
for (i = 0; i < num_params; i++) {
if (parameq(param, params[i].name)) { //如果是内置模块的参数
/* Noone handled NULL, so do it here. */
if (!val && params[i].ops->set != param_set_bool)
return -EINVAL;
DEBUGP("They are equal! Calling %p\n",
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) {
DEBUGP("Unknown argument: calling %p\n", handle_unknown);
return handle_unknown(param, val);
}
DEBUGP("Unknown argument `%s'\n", param);
return -ENOENT;
}
parse_one它调用unknown_bootoption函数处理__setup定义的参数,unknown_bootoption函数在init/main.c中定义如下:
static int __init unknown_bootoption(char *param, char *val)
{
/* Change NUL term back to "=", to make "param" the whole string. */
if (val) {
/* param=val or param="val"? */
if (val == param+strlen(param)+1)
val[-1] = '=';
else if (val == param+strlen(param)+2) {
val[-2] = '=';
memmove(val-1, val, strlen(val)+1);
val--;
} else
BUG();
}
/* 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 {
/* 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;
}
在这个函数中它调用了obsolete_checksetup函数,该函数也定义在init/main.c中
static int __init obsolete_checksetup(char *line)
{
const struct obs_kernel_param *p;
int had_early_param = 0;
p = __setup_start;
do {
int n = strlen(p->str);
if (!strncmp(line, p->str, n)) {
if (p->early) {
/* Already done in parse_early_param?
* (Needs exact match on param part).
* Keep iterating, as we can have early
* params and __setups of same names 8( */
if (line[n] == '\0' || line[n] == '=')
had_early_param = 1;
} else if (!p->setup_func) {
printk(KERN_WARNING "Parameter %s is obsolete,"
" ignored\n", p->str);
return 1;
} else if (p->setup_func(line + n)) //调用支持函数
return 1;
}
p++;
} while (p < __setup_end);
return had_early_param;
}
在这里parse_early_param()主要的作用是处理内核命令行(boot_command_line)的内核参数。也就是处理在内核命令行中有定义的早期参数值(early=1),特别的还包括内核参数console和earlycon。都和输出流有关,内核启动时的打印信息就要求该设备的正确配置。
总结一下:
内核分三类参数的传递与设置
1、内置模块的参数设置
2、高优先级命令行参数设置
3、一般命令行参数设置
三种参娄的设置都由parse_args函数来实现,对第一种内置模块的参数设置在parse_args函数中调用parse_one函数遍历__start___param和__stop___param,如果查找到对应的key,就调用相关参数设置函数处理。
对第二种高优先级命令行参数设置通过parse_args函数传递调用函数do_early_param到parse_one函数中由do_early_param实现遍历obs_kernel_param数组(__setup_start,__setup_end),首先看是否设置early,如果有设置并查找到对应的key,就调用相关early_param函数处理。
对第三种一般命令行参数设置通过parse_args函数传递调用函数unknown_bootoption到parse_one函数中由unknown_bootoption实现遍历 obs_kernel_param数组(__setup_start,__setup_end),首先看是否设置early,如果查找到对应的key,就调用相关__setup函数处理。
对2.6.36以前版本没用 parse_early_param而是用parse_cmdline函数,我们来看看这个函数
static void __init parse_cmdline(char **cmdline_p, char *from)
{
char c = ' ', *to = command_line;
int len = 0;
for (;;) {
if (c == ' ') {
//寻找c=空格 的条件,空格表示一个新的命令行选项,假设:mem=xxx noinitrd root=yyy init=/linuxrc console=ttySAC0,这个扫描是一个一个的扫描命令行里的参数的。
extern struct early_params __early_begin, __early_end; //这些变量在lds中
struct early_params *p;
for (p = &__early_begin; p < &__early_end; p++) { //扫描这个区间的所有early_params结构。
int len = strlen(p->arg); //测量这个字符串的长度。比如"mem="长度是4
if (memcmp(from, p->arg, len) == 0) { //这里也pass,这里不pass就意味着不能解析。
if (to != command_line) //防止得到两个空格
to -= 1;
from += len; //跳过这个选项,得到具体数据,现在from指向“xxx noinitrd...“
p->fn(&from); //调用这个函数处理这个xxx
while (*from != ' ' && *from != '\0') //跳过xxx部分,因为这是mem=xxx已经处理完了,可以扔掉了。
from++;
break; // 终止这次处理,针对mem=xxx的处理,现在from指向“ noinitrd ...“,注意最前面的空格。
}
}
}
c = *from++; //这次c又得到的是空格。第2次,取到的是noinitrd的n
if (!c) //如果到了uboot命令行参数的结尾,或者 命令行参数太长,都会结束扫描
break;
if (COMMAND_LINE_SIZE <= ++len)
break;
*to++ = c; //保存空格,第2此保存了n,依次类推。
}
*to = '\0';
*cmdline_p = command_line; //给cmdline_p赋值,这个指针是start_kernel里的。
}
struct early_params {
const char *arg; //字符串指针
void (*fn)(char **p); //私有的函数
};
在System.map中
c0027cdc T __early_begin
c0027cdc t __early_early_mem
c0027cdc T __setup_end
c0027ce4 t __early_early_initrd
c0027cec t __early_early_vmalloc
c0027cf4 t __early_early_ecc
c0027cfc t __early_early_nowrite
c0027d04 t __early_early_nocache
c0027d0c t __early_early_cachepolicy
c0027d14 t __early_uart_parent_setup
c0027d1c t __early_jtag_wfi_setup
c0027d24 t __early_system_rev_setup
c0027d2c T __early_end
比如arch/arm/kernel/setup.c中
static void __init early_mem(char **p)
{
static int usermem __initdata = 0;
unsigned long size, start;
/*
* If the user specifies memory size, we
* blow away any automatically generated
* size.
*/
if (usermem == 0) {
usermem = 1;
meminfo.nr_banks = 0;
}
start = PHYS_OFFSET;
size = memparse(*p, p);
if (**p == '@')
start = memparse(*p + 1, p);
arm_add_memory(start, size);
}
__early_param("mem=", early_mem);
可以发现能够在这里处理的命令行参数有mem initrd ecc cachepolicy nowrite nocache这六个。
parse_cmdline做了三件事,首先它解析了from所指向的完整的内核参数中关于内存的部分,其次它将没有解析的部分复制到command_line中,最后它将start_kernel()传进来的内核参数指针指向command_line。
内核参数中的 mem=xxxM@ xxx将会被parse_cmdline解析,并根据结果设置meminfo,而其余部分则被复制到command_line中就是说,能解析的都解析了,不能解析的留下来,等着以后解析,保存在command_line中,可以发现uboot的命令行参数具有最高的优先级,如果指定 mem=xxxM@yyy的话,将覆盖掉标记列表的mem32配置,如果多次使用mem= mem=的话,将得到多个内存bank,通过代码来看是这样,没有验证过。
最后。通过上面的分析,我们可以自定义通过命令行传入一个参数设置,这里以uboot向内核传递MAC地址为例说明添加过程
我们使用的系统中的CS8900a没有外接eeprom,所以在默认的情况,Linux下的CS8900a的驱动使用的是一个伪MAC地址。在单一的系统中,这是没有问题的,但是当我们在同一个子网中使用或测试多个设备是,就会产生冲突了。所以我们需要能够方便的改变网卡的MAC地址,而不是将MAC地址硬编码进内核中,每次修改都得重新编译内核。
一、添加内核参数的处理函数
向Linux驱动传递参数的方式有两种,一为在系统启动的时候由bootloader传入,还有一种是将驱动编译成模块,将参数作为模块加载的参数传入。
这里使用的是由bootloader传入的方式。内核通过setup接口接受Bootloader传入的参数。方式如下:
static int __init param_mac_setup(char *str)
{
……
}
__setup("mac=", param_mac_setup);
这样,当在Bootloader中指定“mac=00:2E:79:38:6D:4E”,系统在加载这个模块的时候,就会执行相应的param_mac_setup()函数,而传入给它的参数就是等号后面的物理地址 “00:2E:79:38:6D:4E”。这样,该函数就可以对它进行相应的处理。
二、将MAC地址参数传入命令行参数
为了传入命令行参数,uboot所作的是:
char *commandline = getenv ("bootargs");
setup_commandline_tag (bd, commandline);
现在想要把MAC地址也加入到命令行参数中,只需要修改配置文件include/configs/smdk2410.h中的CONFIG_BOOTARGS:
#define CONFIG_BOOTARGS "root=ramfs devfs=mount console=ttySA0,9600"
在后面添加mac参数如下:
#define CONFIG_BOOTARGS "root=ramfs devfs=mount console=ttySA0,9600 mac=00:2E:79:38:6D:4E"
这样就不需要每次在命令行后面手工追加MAC地址参数了