Linux Kernel:内核中的 initcall 机制

本文采用Linux 内核 v3.10 版本 x86-64 架构

一、概述

在 Linux 内核中,我们经常会看到类似下面的代码:

c 复制代码
// file: kernel/sched/core.c
early_initcall(migration_init);

或者

c 复制代码
// file: fs/devpts/inode.c
module_init(init_devpts_fs)

在上面两段代码中的 early_initcall()module_init() 函数,它们使用了内核的 initcall 机制。

总体来说,initcall 机制实际是向 Linux 内核中注册了多组回调函数,这些函数会在系统初始化的时候被调用。而且,不同组内的函数,其调用优先级是不同的,在执行的时候会有先后顺序。

在 Linux 内核中,通过 initcall 机制注册的函数,主要分为 0 ~ 7 共 8 个等级:

c 复制代码
// file: init/main.c
static initcall_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

其中,__initcall_end 仅用来表示数组元素数量,没有对应的函数。

不同等级的名称保存在 initcall_level_names 变量里,等级越小的优先级越高。比如等级为 1 的函数会比等级为 7 的先执行,也就是说通过 core_initcall() 注册的函数比通过 late_initcall() 注册的先执行。

c 复制代码
// file: init/main.c
/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
	"early",
	"core",
	"postcore",
	"arch",
	"subsys",
	"fs",
	"device",
	"late",
};

除了 0 ~ 7 这 8 个等级外,还有几个特殊的等级,如earlyrootfs 等,我们在下文中都可以看到。

二、initcall 机制的实现

2.1 一组宏 / *_initcall()

Linux 内核提供了一组宏用来注册不同等级的 initcall 函数。这些宏位于 include/linux/init.h 文件中:

c 复制代码
// file: include/linux/init.h
/*
 * 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)

可以看到,这些 *_initcall() 宏又扩展为 __define_initcall() 宏。

2.1.1 宏 __define_initcall()

__define_initcall()的作用是注册一个 initcall 函数,该宏定义如下:

c 复制代码
// file: include/linux/init.h
/* initcalls are now grouped by functionality into separate 
 * subsections. Ordering inside the subsections is determined
 * by link order. 
 * For backwards compatibility, initcall() puts the call in 
 * the device init subsection.
 *
 * The `id' arg to __define_initcall() is needed so that multiple initcalls
 * can point at the same handler without causing duplicate-symbol build errors.
 */

#define __define_initcall(fn, id) \
	static initcall_t __initcall_##fn##id __used \
	__attribute__((__section__(".initcall" #id ".init"))) = fn

该宏接收 2 个参数:

  • fn -- 回调函数,该函数在系统初始化时被执行
  • id -- initcall 标识,防止多个同样的 initcalls 指向同一个处理程序时导致构建时出现重复符号的错误

首先,我们来看下函数类型 initcall_t ,其定义如下:

c 复制代码
// file: include/linux/init.h
/*
 * Used for initialization calls..
 */
typedef int (*initcall_t)(void);

initcall_t 类型的函数,返回值为 int 类型且没有参数。

接下来,__initcall_##fn##id 将入参 fnid 以及 字符串__initcall_连接成一个函数名。比如,early_initcall(migration_init) 扩展后的函数名为 __initcall_migration_initearly。其中,## 是符号连接符。

__used 扩展如下:

c 复制代码
// file: include/linux/compiler-gcc4.h
#define __used			__attribute__((__used__))

使用 used 属性修饰的函数,当该函数没有被引用时,GCC 不会发出 defined but not used 的警告,并强制编译器将它们编译到汇编文件。

__attribute__((__section__(".initcall" #id ".init"))) 会指示编译器将该函数放置到 .initcall<id>.init 节。比如,migration_init() 函数会被放置到 .initcallearly.init 节。

接下来,我们看到,在连接脚本 include/asm-generic/vmlinux.lds.h 里,将这些 initcall 相关的节,放置到符号 __initcall_start__initcall_end 之间,其中使用 early_initcall() 定义的函数位于最靠前的位置,即 .initcallearly.init 节。

assembly 复制代码
// file: include/asm-generic/vmlinux.lds.h
#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) = .;

另外,宏 INIT_CALLS 内部又引用了 INIT_CALLS_LEVEL()宏,该宏定义如下:

c 复制代码
// file: include/asm-generic/vmlinux.lds.h
#define INIT_CALLS_LEVEL(level)						\
		VMLINUX_SYMBOL(__initcall##level##_start) = .;		\
		*(.initcall##level##.init)				\
		*(.initcall##level##s.init)				\

INIT_CALLS_LEVEL() 宏只有 1 个参数,即 initcall 函数的级别 -- level 。在宏内部,首先定义了一个该 level 所在节的起始地址,即__initcall##level##_start。根据参数 level 的不同,可能是 __initcall0_start__initcallrootfs_start__initcall7_start 等等。

接下来,是该 level 相关的 2 个节,.initcall##level##.init.initcall##level##s.init。根据 level 值的不同,这 2 个节可以扩展为 .initcall0.init.initcall0s.init 或者 .initcall7.init.initcall7s.init 等等。

INIT_CALLS 扩展后如下所示:

assembly 复制代码
// file: include/asm-generic/vmlinux.lds.h
#define INIT_CALLS							\
		VMLINUX_SYMBOL(__initcall_start) = .;			\
		*(.initcallearly.init)					\
		VMLINUX_SYMBOL(__initcall0_start) = .;		\
		*(.initcall0.init)				\
		*(.initcall0s.init)				\
		VMLINUX_SYMBOL(__initcall1_start) = .;		\
		*(.initcall1.init)				\
		*(.initcall1s.init)				\
		VMLINUX_SYMBOL(__initcall2_start) = .;		\
		*(.initcall2.init)				\
		*(.initcall2s.init)				\
		
		...
		...
		
		VMLINUX_SYMBOL(__initcall5_start) = .;		\
		*(.initcall5.init)				\
		*(.initcall5s.init)				\
		VMLINUX_SYMBOL(__initcallrootfs_start) = .;		\
		*(.initcallrootfs.init)				\
		*(.initcallrootfss.init)				\
		VMLINUX_SYMBOL(__initcall6_start) = .;		\
		*(.initcall6.init)				\
		*(.initcall6s.init)				\
		
		...
		
		VMLINUX_SYMBOL(__initcall_end) = .;

这些节中的数据都会被链接脚本输出到 .init.data 节中:

assembly 复制代码
// file: include/asm-generic/vmlinux.lds.h
#define INIT_DATA_SECTION(initsetup_align)				\
	.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {		\
		...
		INIT_CALLS						\
		...
	}

最后,.init.data 节,会被放置到 __init_begin__init_end 这两个符号之间:

assembly 复制代码
SECTIONS
{
	...
	* Init code and data - will be freed after init */
	. = ALIGN(PAGE_SIZE);
	.init.begin : AT(ADDR(.init.begin) - LOAD_OFFSET) {
		__init_begin = .; /* paired with __init_end */
	}
	...
	INIT_DATA_SECTION(16)
	...
		/* freed after init ends here */
	.init.end : AT(ADDR(.init.end) - LOAD_OFFSET) {
		__init_end = .;
	}
	...
}

这 2 个符号之间的数据,在系统初始化完成之后,会被释放掉。

这些符号声明如下:

c 复制代码
// init/main.c
extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];

可以看到,这些符号都被声明为 initcall_t 数组。其中,__initcall_start__initcall_end 分别表示 initcall 相关节的起始和结束地址;其它符号,分别表示对应 level 的起始地址。

接下来,我们看下这些通过 initcall 机制注册的函数,是什么时候执行的。

2.2 early_initcall()

使用 early_initcall() 注册的函数,在链接后,位于符号 __initcall_start__initcall0_start 之间。这些回调函数在 do_pre_smp_initcalls() 函数中被调用。

c 复制代码
// file: init/main.c
static void __init do_pre_smp_initcalls(void)
{
	initcall_t *fn;

	for (fn = __initcall_start; fn < __initcall0_start; fn++)
		do_one_initcall(*fn);
}

do_pre_smp_initcalls() 中,会遍历 __initcall_start__initcall0_start 之间的函数,并通过 do_one_initcall() 函数来调用它们。

2.2.1 do_one_initcall()

do_one_initcall() 函数定义如下:

c 复制代码
// file: init/main.c
int __init_or_module do_one_initcall(initcall_t fn)
{
	...
	int ret;

	if (initcall_debug)
		ret = do_one_initcall_debug(fn);
	else
		ret = fn();

	...

	return ret;
}

函数内部,根据变量 initcall_debug 来决定是直接调用 fn() 函数还是调用 do_one_initcall_debug() 函数。initcall_debug 是一个 bool 类型变量,用来决定是否需要追踪 initcall 函数的执行。

c 复制代码
// file: init/main.c
bool initcall_debug;
assembly 复制代码
// file: Documentation/kernel-parameters.txt
initcall_debug	[KNL] Trace initcalls as they are executed.  Useful
			for working out where the kernel is dying during
			startup.

do_one_initcall_debug() 函数的名称也能推断出,该函数包含一些调试信息。

c 复制代码
// file: init/main.c
static int __init_or_module do_one_initcall_debug(initcall_t fn)
{
	ktime_t calltime, delta, rettime;
	unsigned long long duration;
	int ret;

	pr_debug("calling  %pF @ %i\n", fn, task_pid_nr(current));
	calltime = ktime_get();
	ret = fn();
	rettime = ktime_get();
	delta = ktime_sub(rettime, calltime);
	duration = (unsigned long long) ktime_to_ns(delta) >> 10;
	pr_debug("initcall %pF returned %d after %lld usecs\n",
		 fn, ret, duration);

	return ret;
}

可以看到, do_one_initcall_debug() 函数中,在 fn() 函数调用前后,通过 ktime_get() 函数分别获取了开始时间和结束时间,然后计算出时间差并打印出来。最后,返回 fn() 函数的执行结果。

2.3 主要 initcalls

early_initcall() 注册的是 early 等级的函数,注册其它等级函数的宏如下所示。

注册 0 ~ 7 等级函数的宏:

  • core_initcall()
  • postcore_initcall()
  • arch_initcall()
  • subsys_initcall()
  • fs_initcall()
  • device_initcall()
  • late_initcall()

注册 rootfs 等级函数的宏:

  • rootfs_initcall()

注册 0s ~ 7s 等级函数的宏:

  • core_initcall_sync()
  • postcore_initcall_sync()
  • arch_initcall_sync()
  • subsys_initcall_sync()
  • fs_initcall_sync()
  • device_initcall_sync()
  • late_initcall_sync()

通过这些宏注册的函数,在链接后,位于 __initcall0_start__initcall_end 之间。

2.3.1 do_initcalls()

上述 initcall 函数在 do_initcalls()中被调用,该函数位于 do_basic_setup() 函数中。

c 复制代码
// file: init/main.c
/*
 * Ok, the machine is now initialized. None of the devices
 * have been touched yet, but the CPU subsystem is up and
 * running, and memory and process management works.
 *
 * Now we can finally start doing some real work..
 */
static void __init do_basic_setup(void)
{
	...
	do_initcalls();
}

do_initcalls()函数定义如下:

c 复制代码
// file: init/main.c
static void __init do_initcalls(void)
{
	int level;

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

initcall_levels是一个数组,该数组一共 9 个成员,其中每个成员又是 initcall_t类型的数组。除了最后一个成员 __initcall_end 仅用来标识结束地址外,前 8 个成员分别对应着不同的 level 的函数集合。

c 复制代码
// file: init/main.c
static initcall_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];

do_initcalls() 函数中,会遍历所有 level,然后调用 do_initcall_level() 函数来执行该 level 下的所有回调函数。

do_initcall_level() 函数定义如下:

c 复制代码
// file: init/main.c 
static void __init do_initcall_level(int level)
{
	...
	initcall_t *fn;

	...

	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
		do_one_initcall(*fn);
}

do_initcall_level() 函数内部,会遍历该 level 下所有的回调函数,并调用 do_one_initcall() 函数来执行回调。

值得注意的是,通过宏 rootfs_initcall() 注册的函数位于 __initcall5_start[] 数组中。

2.4 module_init()

module_init() 扩展为 __initcall()

c 复制代码
// file: include/linux/init.h
/**
 * 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);

__initcall() 又扩展为 device_initcall()

c 复制代码
// file: include/linux/init.h
#define __initcall(fn) device_initcall(fn)

device_initcall()宏我们在上文中已经介绍过了,通过该宏注册的函数其 level 为 6:

c 复制代码
#define device_initcall(fn)		__define_initcall(fn, 6)

2.5 console_initcall()

通过 console_initcall() 注册的函数,会被放置在 .con_initcall.init 节。

c 复制代码
// file: include/linux/init.h
#define console_initcall(fn) \
	static initcall_t __initcall_##fn \
	__used __section(.con_initcall.init) = fn

该节的起始地址是 __con_initcall_start,结束地址是 __con_initcall_end

c 复制代码
// file: include/asm-generic/vmlinux.lds.h
#define CON_INITCALL							\
		VMLINUX_SYMBOL(__con_initcall_start) = .;		\
		*(.con_initcall.init)					\
		VMLINUX_SYMBOL(__con_initcall_end) = .;

__con_initcall_start__con_initcall_end 均被声明为 initcall_t 类型的数组。

c 复制代码
// file: include/linux/init.h
extern initcall_t __con_initcall_start[], __con_initcall_end[];

.con_initcall.init 节的位置紧挨着宏 INIT_CALLS创建的各节, 位于 __initcall_end 之后。

assembly 复制代码
// file: include/asm-generic/vmlinux.lds.h
#define INIT_DATA_SECTION(initsetup_align)				\
	.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {		\
		...
		INIT_CALLS						\
		CON_INITCALL						\
		...
	}

通过宏console_init() 注册的函数,在 console_init() 函数中被调用。

c 复制代码
// file: drivers/tty/tty_io.c
/*
 * Initialize the console device. This is called *early*, so
 * we can't necessarily depend on lots of kernel help here.
 * Just do some early initializations, and do the complex setup
 * later.
 */
void __init console_init(void)
{
	initcall_t *call;

	...

	/*
	 * set up the console device so that later boot sequences can
	 * inform about problems etc..
	 */
	call = __con_initcall_start;
	while (call < __con_initcall_end) {
		(*call)();
		call++;
	}
}

console_init() 函数内部,会遍历 __con_initcall_start__con_initcall_end 之间的所有函数,并调用它们。

2.6 security_initcall()

通过 security_initcall() 注册的函数,会被放置在 .security_initcall.init 节。

c 复制代码
// file: include/linux/init.h
#define security_initcall(fn) \
	static initcall_t __initcall_##fn \
	__used __section(.security_initcall.init) = fn

该节的起始地址是 __security_initcall_start,结束地址是 __security_initcall_end

c 复制代码
// file: include/asm-generic/vmlinux.lds.h
#define SECURITY_INITCALL						\
		VMLINUX_SYMBOL(__security_initcall_start) = .;		\
		*(.security_initcall.init)				\
		VMLINUX_SYMBOL(__security_initcall_end) = .;

__security_initcall_start__security_initcall_end 均被声明为 initcall_t 类型的数组。

c 复制代码
// file: include/linux/init.h
extern initcall_t __security_initcall_start[], __security_initcall_end[];

.security_initcall.init节的位置紧挨着宏 CON_INITCALL创建的节,即 .con_initcall.init

assembly 复制代码
// file: include/asm-generic/vmlinux.lds.h
#define INIT_DATA_SECTION(initsetup_align)				\
	.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {		\
		...
		INIT_CALLS						\
		CON_INITCALL						\
		SECURITY_INITCALL					\
		...
	}

通过宏security_init() 注册的函数,在 security_init() 函数中被调用。

c 复制代码
// file: security/security.c
/**
 * security_init - initializes the security framework
 *
 * This should be called early in the kernel initialization sequence.
 */
int __init security_init(void)
{
	...
	do_security_initcalls();

	return 0;
}

security_init() 函数内部,通过 do_security_initcalls()函数,执行所有的回调函数。

c 复制代码
// file: security/security.c
static void __init do_security_initcalls(void)
{
	initcall_t *call;
	call = __security_initcall_start;
	while (call < __security_initcall_end) {
		(*call) ();
		call++;
	}
}

2.7 各组 initcall 函数的执行顺序

三、参考资料

1、gcc - "##" 连接符

2、gcc - "used" 属性

相关推荐
许野平14 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
hjjdebug41 分钟前
linux 下 signal() 函数的用法,信号类型在哪里定义的?
linux·signal
其乐无涯41 分钟前
服务器技术(一)--Linux基础入门
linux·运维·服务器
Diamond技术流42 分钟前
从0开始学习Linux——网络配置
linux·运维·网络·学习·安全·centos
斑布斑布1 小时前
【linux学习2】linux基本命令行操作总结
linux·运维·服务器·学习
Spring_java_gg1 小时前
如何抵御 Linux 服务器黑客威胁和攻击
linux·服务器·网络·安全·web安全
✿ ༺ ོIT技术༻1 小时前
Linux:认识文件系统
linux·运维·服务器
会掉头发1 小时前
Linux进程通信之共享内存
linux·运维·共享内存·进程通信
我言秋日胜春朝★1 小时前
【Linux】冯诺依曼体系、再谈操作系统
linux·运维·服务器