内核启动过程中需要完成各个部分的初始化,比如中端、页面管理、slab分配器、任务调度器、网络、PCI设备等等的初始化,这些初始化大概可以分为两种:
  • 一种是关键的,必须完成的而且必须以特定的顺序来完成的初始化,这部分的代码往往是直接便如内核的而且是直接调用的
  • 另一种是非关键的的子系统(或者说模块、功能)的初始化,这部分根据配置可以不加载,可以以built-in的方式编到内核的可执行文件中,也可以以模块的方式加载。但是对于这一类来说,它们也需要内核的关键子系统的支持,甚至在它们之间也存在某种依赖或者说顺序关系,因此它们的初始化需要以另一种方式来实现。
对于第二类的子系统,内核在do_initcalls完成它们的初始化,在这之前内核会完成第一类子系统的初始化。do_initcalls的调用时机为start_kernel->rest_init->kernel_init->do_basic_setup->do_initcalls

一、do_initcalls 

1.1 实现方式

该函数的实现体现了linux对第二类子系统进行初始化的方式的设计思想。Linux提供了不同的“初始化等级”,linux会按照等级从高(level 0)到低(level 7)来进行初始化,也就是说会首先初始化等级高的,然后初始化等级低的。其代码逻辑很简单:

	int level;


	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);

因此,一个模块只需要将自己的初始化函数定义为某个等级,do_initcalls在进行该等级的初始化时,就会调用该模块的初始化函数。
为了方便使用,内核使用了如下的宏来定义各个初始化等级:
#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id __used \
	__attribute__((__section__(".initcall" #id ".init"))) = fn


/*
 * Early initcalls run before initializing SMP.
 *
 * Only for built-in code, not modules.
 */
#define early_initcall(fn)		__define_initcall(fn, early)


/*
 * A "pure" initcall has no dependencies on anything else, and purely
 * initializes variables that couldn't be statically initialized.
 *
 * This only exists for built-in code, not for modules.
 * Keep main.c:initcall_level_names[] in sync.
 */
#define pure_initcall(fn)		__define_initcall(fn, 0)


#define core_initcall(fn)		__define_initcall(fn, 1)
#define core_initcall_sync(fn)		__define_initcall(fn, 1s)
#define postcore_initcall(fn)		__define_initcall(fn, 2)
#define postcore_initcall_sync(fn)	__define_initcall(fn, 2s)
#define arch_initcall(fn)		__define_initcall(fn, 3)
#define arch_initcall_sync(fn)		__define_initcall(fn, 3s)
#define subsys_initcall(fn)		__define_initcall(fn, 4)
#define subsys_initcall_sync(fn)	__define_initcall(fn, 4s)
#define fs_initcall(fn)			__define_initcall(fn, 5)
#define fs_initcall_sync(fn)		__define_initcall(fn, 5s)
#define rootfs_initcall(fn)		__define_initcall(fn, rootfs)
#define device_initcall(fn)		__define_initcall(fn, 6)
#define device_initcall_sync(fn)	__define_initcall(fn, 6s)
#define late_initcall(fn)		__define_initcall(fn, 7)
#define late_initcall_sync(fn)		__define_initcall(fn, 7s)


#define __initcall(fn) device_initcall(fn)
上述定义针对的是非模块,即要编译到内核的影响中的子系统,如果是模块,则定义变成了:
#define early_initcall(fn)		module_init(fn)
#define core_initcall(fn)		module_init(fn)
#define postcore_initcall(fn)		module_init(fn)
#define arch_initcall(fn)		module_init(fn)
#define subsys_initcall(fn)		module_init(fn)
#define fs_initcall(fn)			module_init(fn)
#define device_initcall(fn)		module_init(fn)
#define late_initcall(fn)		module_init(fn)


#define security_initcall(fn)		module_init(fn)


/* Each module must use one module_init(). */
#define module_init(initfn)					\
	static inline initcall_t __inittest(void)		\
	{ return initfn; }					\
	int init_module(void) __attribute__((alias(#initfn)));

顺便提一句,module_init在不支持模块的系统中会变成:

#define __initcall(fn) device_initcall(fn)

#define module_init(x)  __initcall(x);

1.2 内存安排

do_initcalls是从特定的内存区域取出初始化函数的指针,然后来调用它的,因而上边定义的各个等级的初始化函数都应该被放在特定的内存区域,每个等级的初始化函数都被放在自己特定的初始化区域中。
Linux提供了一个头文件“vmlinux.lds.h”,该文件定义了一些宏用于辅助写连接脚本,从其中可以看到最终会出现在连接脚本中的各个内存section以及它们的相对位置,定义各个“初始化等级”所在的内存section的相关代码如下:
#define INIT_DATA_SECTION(initsetup_align)				\
	.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {		\
		INIT_DATA						\
		INIT_SETUP(initsetup_align)				\
		INIT_CALLS						\
		CON_INITCALL						\
		SECURITY_INITCALL					\
		INIT_RAM_FS						\
	}
该宏定义了初始化数据段,其中就包括了INIT_CALLS段。
#define INIT_CALLS_LEVEL(level)						\
		VMLINUX_SYMBOL(__initcall##level##_start) = .;		\
		*(.initcall##level##.init)				\
		*(.initcall##level##s.init)				\


#define INIT_CALLS							\
		VMLINUX_SYMBOL(__initcall_start) = .;			\
		*(.initcallearly.init)					\
		INIT_CALLS_LEVEL(0)					\
		INIT_CALLS_LEVEL(1)					\
		INIT_CALLS_LEVEL(2)					\
		INIT_CALLS_LEVEL(3)					\
		INIT_CALLS_LEVEL(4)					\
		INIT_CALLS_LEVEL(5)					\
		INIT_CALLS_LEVEL(rootfs)				\
		INIT_CALLS_LEVEL(6)					\
		INIT_CALLS_LEVEL(7)					\
		VMLINUX_SYMBOL(__initcall_end) = .;
该宏即定义了各个初始化等级所在的section,比如INIT_CALLS_LEVEL(5)会被展开为:
  VMLINUX_SYMBOL(__initcall5_start) =.;
  *(.initcall5.init)
*(.initcall5s .init)
__initcall5_start就是会被do_initcalls使用的一个变量,它被do_initcalls用于定位初始化等级5的起始地址,进而找到定义为初始化等级5的所有初始化函数,并调用它们。

同时从INIT_CALLS中也可以看出,所有等级的初始化函数都被放在从__initcall_start到__initall_end之间的内存区域中。

其它的内存区域的定义也可以在头文件“vmlinux.lds.h”中找到,如果感兴趣的话可以查看该文件。

二、初始化参数

2.1 初始化参数的定义和初始化时机

Linux允许用户在启动时指定一些内核选项(也就是启动参数),这些参数会被传递给相应的处理函数。

有三种方式来定义内核选项:
  1. 使用宏early_param(str, fn),对于模块,该宏被定义为空
  2. 使用宏__setup(str, fn),对于模块,该宏被定义为空
  3. 使用宏module_param(name, type, perm)
其中前两种方式中,str为要定义的选项的名字,其值(即“=”后的部分)将会被传递给fn来处理。第三种方式没有函数选项,这是因为在新的内核中,内核为每一种数据类型提供了一个kernel_param_ops结构,该结构保存了对应该数据类型的set/get/free函数,因而不需要每个模块提供自己的函数,如果需要定义一个内核选项,只需要提供名字,类型以及期望的访问权限即可。

采用第一种方式定义的内核选项会在内核启动的开始阶段被parse_early_param处理。而其它两种方式则会由下边的调用处理:

parse_args("Booting kernel", static_command_line, __start___param,
		   __stop___param - __start___param,
		   -1, -1, &unknown_bootoption);

parse_args的参数的含义:

  • 第一个参数:一个字符串,用于提示正在解析的内核选项
  • 第二个参数:命令行参数
  • 第三个参数:要解析的参数的首地址。要解析的参数就是通过module_param定义的参数,它们会被存放在__param段中,下面会给出其相关定义
  • 第四个参数:要解析多少个参数
  • 第五个参数和第六个参数:参数的最小等级和最大等级,只有落在这个等级范围内的参数才会被解析
  • 最后一个参数是一个函数:用于指定如果不能识别某个参数则应该由谁进行处理,这里使用了unknown_bootoption,它会处理使用__setup(str, fn)定义的参数

实际上parse_early_param最终也会调用parse_args,但在它的调用中,对于不能识别的参数使用的处理函数是do_early_param。

module_param最终会通过下面的宏定义;

#define __module_param_call(prefix, name, ops, arg, perm, level)	\
	/* Default value instead of permissions? */			\
	static int __param_perm_check_##name __attribute__((unused)) =	\
	BUILD_BUG_ON_ZERO((perm) < 0 || (perm) > 0777 || ((perm) & 2))	\
	+ BUILD_BUG_ON_ZERO(sizeof(""prefix) > MAX_PARAM_PREFIX_LEN);	\
	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, ops, perm, level, { arg } }

2.2 初始化参数的内存安排

通过early_param和__setup定义的初始化参数会被放在.init.setup段中,具体的代码可以查看头文件"init.h"。.init.setup段的定义也在头文件“vmlinux.lds.h”中,如下所示

#define INIT_SETUP(initsetup_align)					\
		. = ALIGN(initsetup_align);				\
		VMLINUX_SYMBOL(__setup_start) = .;			\
		*(.init.setup)						\
		VMLINUX_SYMBOL(__setup_end) = .;

从上述内存段的定义可以看出,该宏定义的内存区域起始于__setup_start,结束于__setup_end。

通过module_param定义的参数会被放在.para段中,该段同样可以在头文件“vmlinux.lds.h”中找到,如下所示:

	__param : AT(ADDR(__param) - LOAD_OFFSET) {			\
		VMLINUX_SYMBOL(__start___param) = .;			\
		*(__param)						\
		VMLINUX_SYMBOL(__stop___param) = .;			\
从上述内存段的定义可以看出,该宏定义的内存区域起始于__start___param,结束于__stop___param。

2.3 一些其它宏

#define __exit          __section(.exit.text) __exitused __cold notrace
如果一个函数加了个标记,则它会被放入.exit.text段,该段比较特殊,它被用于模块的退出函数,如果一个模块被编译到了内核映像中而不是以模块的形式编译的则该部分内存可以被链接器忽略,即不把它连接到内核中或者可以在系统启动后将它占用的内存给释放掉;同时如果内核不支持模块卸载,则也可以在链接时或者启动后释放该部分区域所占的内存。

下边这些宏直接给出内核代码中的注释,简单的加了这些标记的内存区域是可释放的(以节省内存)。

/* These are for everybody (although not all archs will actually
   discard it in modules) */
#define __init		__section(.init.text) __cold notrace
#define __initdata	__section(.init.data)
#define __initconst	__constsection(.init.rodata)
#define __exitdata	__section(.exit.data)
#define __exit_call	__used __section(.exitcall.exit)

三、inittab

在内核启动完成后,它会启动第一个进程init进程。inittab是init进程的配置文件,该配置文件的格式如下:

id:runlevels:action:process 其中某些部分可以为空

  • id:1~2个字符,配置行的唯一标识,在配置文件中不能重复。
  • runlevels:配置行适用的运行级别,在这里可填入多个运行级别,比如12345或者35等。
  • Linux有7个运行级别,如下:
    • 0:关机。
    • 1:单用户字符界面。
    • 2:不具备网络文件系统(NFS)功能的多用户字符界面。
    • 3:具有网络功能的多用户字符界面。
    • 4: 保留不用。
    • 5:具有网络功能的图形用户界面。
    • 6:重新启动系统。
  • action:针对该行所指定的process,init进程所采取的动作。init有如下几种行为:
    • respawn:启动并监视第4项指定的process,若process终止则重启它
    • wait:执行第4项指定的process,并等待它执行完毕
    • once:执行第4项指定的process
    • boot:不论在哪个执行等级,系统启动时都会运行第4项指定的process
    • bootwait:不论在哪个执行等级,系统启动时都会运行第4项指定的process,且一直等它执行完备
    • off:关闭任何动作,相当于忽略该配置行
    • ondemand:进入ondemand执行等级时,执行第4项指定的process
    • initdefault:系统启动后进入的执行等级,该行不需要指定process
    • sysinit:不论在哪个执行等级,系统会在执行boot 及bootwait之前执行第4项指定的process
    • powerwait:当系统的供电不足时执行第4项指定的 process,且一直等它执行完毕
    • powerokwait:当系统的供电恢复正常时执行第4项指定的process,且一直等它执行完毕
    • powerfailnow:当系统的供电严重不足时执行第4项指定的process
    • ctrlaltdel:当用户按下【Ctrl+Alt+Del】时执行的操作
    • kbrequest:当用户按下特殊的组合键时执行第4项指定的process,此组合键需在keymaps文件定义
  • process:进程,可以指定其参数
特别需要注意的是action中的 initdefault,它用于指定系统启动后进入的执行等级,因此不需要指定process。


Logo

更多推荐