linux内核组件初始化体系

先来看下内核初始化时调用的一些函数:





这里主要的初始化有三类:

1 boot比如grub,u-boot传递给内核的参数,内核的处理。这里是调用parse_args.
2 中断和时钟的初始化。
3 初始化的函数,这里主要是通过do_initcalls标记的驱动初始化函数。一般这里的初始化函数完成后,会调用free_init_mem释放掉这块的空间。

我们这里主要关注第1和第3类。

首先来看第一类的初始化,我们知道boot传递给内核的参数都是 "name_varibale=value"这种形式的,那么内核如何知道传递进来的参数该怎么去处理呢?

内核是通过__setup宏或者early_param宏来将参数与参数所处理的函数相关联起来的。我们接下来就来看这个宏以及相关的结构的定义:

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

#define early_param(str, fn)					\
	__setup_param(str, fn, fn, 1)

#define __setup_param(str, unique_id, fn, early)			\
	static char __setup_str_##unique_id[] __initdata __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 }


看起来很复杂。 首先setup宏第一个参数是一个key。比如"netdev="这样的,而第二个参数是这个key对应的处理函数。这里要注意相同的handler能联系到不同的key。early_param和setup唯一不同的就是传递给__setup_param的最后一个参数,这个参数我么下面会说明

而接下来_setup_param定义了一个struct obs_kernel_param类型的结构,然后通过_section宏,使这个变量在链接的时候能够放置在段.init.setup(我们后面会详细介绍内核中的这些初始化段).

我们接下来来看struct obs_kernel_param结构:

struct obs_kernel_param {
	const char *str;
	int (*setup_func)(char *);
	int early;
};


前两个参数很简单,一个是key,一个是handler。最后一个参数其实也就是类似于优先级的一个flag,因为传递给内核的参数中,有一些需要比另外的一些更早的解析。(这也就是为什么early_param和setup传递的最后一个参数的不同的原因了。


接下来我们来看内核解析boot传递给内核的参数的步骤:

先看下面的图:



可以看到内核首先通过parse_early_param来解析优先级更高的,也就是需要被更早解析的命令行参数,然后通过parse_ares来解析一般的命令行参数.

下面这两个调用就是parse_early_param和parse_ares调用传递给parse_args的不同的参数.

parse_args("early options", tmp_cmdline, NULL, 0, do_early_param);
parse_args("Booting kernel", static_command_line, __start___param,
		   __stop___param - __start___param,
		   &unknown_bootoption);



我们先来看do_early_param函数:


static int __init do_early_param(char *param, char *val)
{
	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;
}



接下来下来看下,一些内置模块的参数处理的问题。传递给模块的参数都是通过module_param来实现的:

#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_call(name, param_set_##type, param_get_##type, &value, perm); \
	__MODULE_PARM_TYPE(name, #type)

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

#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 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, perm, set, get, { arg } }


这里也就是填充了 struct kernel_param的结构体,并将这个变量标记为__param段,以便于链接器将此变量装载到指定的段。

接下来我们来看struct kernel_param这个结构:

struct kernel_param {
	const char *name;
	unsigned int perm;
	param_set_fn set;///设置参数的函数,
	param_get_fn get;///读取参数的函数
	union {///传递给上面两个函数的参数。
		void *arg;
		const struct kparam_string *str;
		const struct kparam_array *arr;
	};
};




接下来来看parse_one函数,其中early和一般的pase都是通过这个函数来解析:

static int parse_one(char *param,
		     char *val,
		     struct kernel_param *params, 
		     unsigned num_params,
		     int (*handle_unknown)(char *param, char *val))
{
	unsigned int i;
///如果是early_param则直接跳过这步,而非early的,则要通过这步来设置一些内置模块的参数。
	/* 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]);
		}
	}

	if (handle_unknown) {
		DEBUGP("Unknown argument: calling %p\n", handle_unknown);
		return handle_unknown(param, val);
	}

	DEBUGP("Unknown argument `%s'\n", param);
	return -ENOENT;
}


这里之所以把所有的内核初始化参数都放在一个段里,主要是为了初始化成功后,释放这些空间。


我们下来看一下内核编译完毕后的一些段的位置:




右边就是每个段定义的相关的宏。在__init_start和__init_end之间的段严格按照顺序进行初始化,比如core_initcall宏修饰的函数就要比arch_initcall宏定义的函数优先级高,所以就更早的调用。这个特性就可以使我们将一些优先级比较高的初始化函数放入相应比较高的段。。


这里要注意,每个init宏,基本都会有个对应的exit宏,比如__initial和__exit等等。。

内核中修饰的宏很多,我们下面只介绍一些常用的:

__devinit 用来标记一个设备的初始化函数,比如pci设备的probe函数,就是用这个宏来修饰。

__initcall这个已被废弃,现在实现为__devinit的别名。

剩下的宏需要去看内核的相关文档。。


最后还要注意几点:

1 当模块被静态的加入到内核中时,module_init标记的函数只会被执行一次,因此当初始化成功后,内核会释放掉这块的内存。

2 当模块静态假如内核,module_exit在链接时就会被删除掉。


这里的优化还有很多,比如热拔插如果不支持的话,很多设备的初始化函数都会在执行完一遍后,被丢弃掉。


这里可以看到,如果模块被静态的编译进内核时,内核所做的内存优化会更多,虽然损失了更多的灵活性。。




你可能感兴趣的:(linux)