Linux允许用户把内核配置选项传递给引导程序,可以使用此机制在引导期间调整内核。
parse_args函数用于解析输入字符串,输入字符串是形如 key=value形式的参数。寻找到特定关键字后,启用适当的处理函数。
内核组件使用__setup宏,注册关键字和处理函数。如网络子系统注册的关键字
__setup("netdev=", netdev_boot_setup);
同一个处理函数可以和多个关键字关联。比如函数netdev_boot_setup又被关键字ether=关联。
__setup("ether=", netdev_boot_setup);
当一段代码被编译成模块时,__setup宏被定义成空操作。
start_kernel两次调用parse_args解析引导配置信息的原因是,引导选项分成两类,每次调用都处理其中的一类。
根据上面的描述可以得出结论使用early_param函数定义的初期选项比使用__setup定义的默认选项更早执行。
传递给__setup的输入函数会被放到.init.setup内存区。
#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 }
#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)
#else /* MODULE */
#define __setup_param(str, unique_id, fn) /* nothing */
#define __setup(str, func) /* nothing */
#endif
第一遍只处理设置了early标记的选项。第二遍处理剩余的选项。
extern const struct obs_kernel_param __setup_start[], __setup_end[];
分析上一小节的代码可以看出当我们定义一个选项时,会定义一个struct obs_kernel_param结构。所有选项数据会放在同一个内存段中(.init.setup区),内核启动时使用__setup_start和__setup_end两个指针指向那个内存段的开始结束。
这样做的原因是:
前边已经知道使用"ether="和"netdev="两个关键字注册了函数netdev_boot_setup。
netdev_boot_setup函数将参数解析后放入dev_boot_setup数组中。
dev_boot_setup是一个struct netdev_boot_setup数组。
struct netdev_boot_setup结构中有一个struct ifmap成员,用于存储输入配置的值。
* Device mapping structure. I'd just gone off and designed a
* beautiful scheme using only loadable modules with arguments
* for driver options and along come the PCMCIA people 8)
*
* Ah well. The get() side of this is good for WDSETUP, and it'll
* be handy for debugging things. The set side is fine for now and
* being very small might be worth keeping for clean configuration.
*/
/* for compatibility with glibc net/if.h */
#if __UAPI_DEF_IF_IFMAP
struct ifmap {
unsigned long mem_start;
unsigned long mem_end;
unsigned short base_addr;
unsigned char irq;
unsigned char dma;
unsigned char port;
/* 3 bytes spare */
};
#endif /* __UAPI_DEF_IF_IFMAP */
/*
* This structure holds boot-time configured netdevice settings. They
* are then used in the device probing.
*/
struct netdev_boot_setup {
char name[IFNAMSIZ];
struct ifmap map;
};
static int netdev_boot_setup_add(char *name, struct ifmap *map)
{
struct netdev_boot_setup *s;
int i;
s = dev_boot_setup;
for (i = 0; i < NETDEV_BOOT_SETUP_MAX; i++) {
if (s[i].name[0] == '\0' || s[i].name[0] == ' ') {
memset(s[i].name, 0, sizeof(s[i].name));
strlcpy(s[i].name, name, IFNAMSIZ);
memcpy(&s[i].map, map, sizeof(s[i].map));
break;
}
}
return i >= NETDEV_BOOT_SETUP_MAX ? 0 : 1;
}
/*
* Saves at boot time configured settings for any netdevice.
*/
int __init netdev_boot_setup(char *str)
{
int ints[5];
struct ifmap map;
str = get_options(str, ARRAY_SIZE(ints), ints);
if (!str || !*str)
return 0;
/* Save settings */
memset(&map, 0, sizeof(map));
if (ints[0] > 0)
map.irq = ints[1];
if (ints[0] > 1)
map.base_addr = ints[2];
if (ints[0] > 2)
map.mem_start = ints[3];
if (ints[0] > 3)
map.mem_end = ints[4];
/* Add new entry to the list */
return netdev_boot_setup_add(str, &map);
}
__setup("netdev=", netdev_boot_setup);
引导结束时,网络代码可以使用netdev_boot_setup_check函数根据设备名称,检查给定接口是否在引导期间传入配置信息。
/**
* netdev_boot_setup_check - check boot time settings
* @dev: the netdevice
* * Check boot time settings for the device.
* The found settings are set for the device to be used
* later in the device probing.
* Returns 0 if no settings found, 1 if they are.
*/
int netdev_boot_setup_check(struct net_device *dev)
{
struct netdev_boot_setup *s = dev_boot_setup;
int i;
for (i = 0; i < NETDEV_BOOT_SETUP_MAX; i++) {
if (s[i].name[0] != '\0' && s[i].name[0] != ' ' &&
!strcmp(dev->name, s[i].name)) {
dev->irq = s[i].map.irq;
dev->base_addr = s[i].map.base_addr;
dev->mem_start = s[i].map.mem_start;
dev->mem_end = s[i].map.mem_end;
return 1;
}
}
return 0;
}
为了使内核具有更高的可读性,引入一组宏,比如__init、__exit等
这些宏允许内核决定要把每个模块的那些代码引入到内核镜像中。
同时这些宏至少提供下列两项服务:
Linux内核使用各种不同的宏为函数和数据结构标记特殊属性。有些宏辉通知链接器把带有共同属性的代码或数据结构放到特定的内存区,这样内核可以轻易管理所有具有共同属性的对象。
函数所用的宏:
__init宏
引导期间初始化函数,针对那些引导结束后不在需要使用的函数。
__exit宏
__init的配对宏,当相关的内核组件被关闭的时候调用。通常用于修饰module_exit函数。
core_initcall
postcore_initcall
arch_initcall
subsys_initcall
fs_initcall
device_initcall
last_initcall
以上这些宏标记那些必须在引导期间执行的初始化函数。
初始化数据结构所用的宏:
设备初始化函数使用的初始化宏:
内核引导早期阶段有两个工种
前一小节列出了很多以_initcall结尾的宏,这些宏分别将各自的函数放在各自的内存段中,由do_initcalls函数按照段的优先级分别调用。
do_initcall_level函数将一个段中的所有函数都取出来依次调用。
static void __init do_initcall_level(int level)
{
initcall_entry_t *fn;
strcpy(initcall_command_line, saved_command_line);
parse_args(initcall_level_names[level],
initcall_command_line, __start___param,
__stop___param - __start___param,
level, level,
NULL, &repair_env_string);
trace_initcall_level(initcall_level_names[level]);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));
}
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
module_init和module_exit宏分别标记那些在模块初始化和卸载时执行的函数。
从下边的代码中可以看出当该模块被内核静态编译(不是编译成模块时)被定义为__initcall
#ifndef MODULE
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
#else /* MODULE */
跟踪module_init宏,将其相关的宏整理如下,可以发现module_init修饰的函数被放入了.initcall6的内存段中。
#define device_initcall(fn) __define_initcall(fn, 6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x);
在第五章的设备处理初始化一章介绍的net_dev_init函数,放到了.initcall4中,因此就可以保证net_dev_init函数肯定会先于驱动程序的初始化函数运行。
#define subsys_initcall(fn) __define_initcall(fn, 4)
subsys_initcall(net_dev_init);
内核代码和数据会永久驻留在内存中,所以应该减少内存浪费。初始化代码大部分只会执行一次,因此在使用完成之后可以被释放。
内核早期阶段执行的函数都会标记__init宏。大多数的module_init修饰函数也标记位这个宏。
查看__init的定义可以发现被__init修饰过的函数被放入了.init.text这个section,这个section会由kernel_init函数调用free_initmem进行释放。
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
__exit宏用于将函数放到.exit.text这个section。这个section在链接内核时直接丢掉。
#define __exit __section(.exit.text) __exitused __cold notrace
xxx__initcall宏
free_initmem也会将xxx__initcall各个段的内存丢弃掉。
__exitcall宏
__exitcall修饰的函数放到.text.exit的section链接期间被丢弃。
__devinit宏当内核没有编译为支持热插拔时,引导阶段结束后就不需要__devinit的函数,因此在不支持热插拔时__devinit定义成__init。
__devexit宏当内核没有编译为支持热插拔时,__devexit的函数不会被调用可以被丢弃掉。
CONFIG_MODULEd
当内核支持可加载模块时定义。
CONFIG_HOTPLUG
当内核支持热插拔时定义
MODULE
当改建所属的内核组件编译为模块时定义。