在驱动程序中,我经常看到使用这三种类型的 init 函数。
module_init()
core_initcall()
early_initcall()
- 我应该在什么情况下使用它们?
- 另外,还有其他的初始化方式吗?
在驱动程序中,我经常看到使用这三种类型的 init 函数。
module_init()
core_initcall()
early_initcall()
它们确定内置模块的初始化顺序。驱动程序将在大多数时间使用device_initcall
(或;见下文)。module_init
早期初始化 ( early_initcall
) 通常由特定于体系结构的代码用于在初始化任何实际驱动程序之前初始化硬件子系统(电源管理、DMA 等)。
看init/main.c
。arch/<arch>/boot
在和中的代码完成一些特定于体系结构的初始化之后,将调用arch/<arch>/kernel
可移植函数。start_kernel
最终,在同一个文件中,do_basic_setup
调用:
/*
* 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)
{
cpuset_init_smp();
usermodehelper_init();
shmem_init();
driver_init();
init_irq_proc();
do_ctors();
usermodehelper_enable();
do_initcalls();
}
以调用结束do_initcalls
:
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
/* 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",
};
static void __init do_initcall_level(int level)
{
extern const struct kernel_param __start___param[], __stop___param[];
initcall_t *fn;
strcpy(static_command_line, saved_command_line);
parse_args(initcall_level_names[level],
static_command_line, __start___param,
__stop___param - __start___param,
level, level,
&repair_env_string);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
您可以看到上面的名称及其相关索引:early
是 0、core
是 1 等。这些__initcall*_start
条目中的每一个都指向一个函数指针数组,这些指针一个接一个地被调用。这些函数指针是实际的模块和内置初始化函数,您使用 , 等指定的module_init
函数early_initcall
。
是什么决定了哪个函数指针进入哪个__initcall*_start
数组?链接器使用module_init
和*_initcall
宏的提示来执行此操作。对于内置模块,这些宏将函数指针分配给特定的 ELF 部分。
module_init
考虑一个内置模块(用y
in配置.config
),module_init
只需像这样扩展(include/linux/init.h
):
#define module_init(x) __initcall(x);
然后我们遵循这个:
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
所以,现在,module_init(my_func)
意味着__define_initcall(my_func, 6)
。这是_define_initcall
:
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
这意味着,到目前为止,我们有:
static initcall_t __initcall_my_func6 __used
__attribute__((__section__(".initcall6.init"))) = my_func;
哇,有很多 GCC 的东西,但这仅意味着创建了一个新符号__initcall_my_func6
,它放在名为 的 ELF 部分中.initcall6.init
,并且如您所见,指向指定的函数 ( my_func
)。将所有函数添加到该部分最终会创建完整的函数指针数组,全部存储在.initcall6.init
ELF 部分中。
再看一下这个块:
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
让我们以第 6 级为例,它代表所有用 . 初始化的内置模块module_init
。它从 开始__initcall6_start
,它的值是在该.initcall6.init
段中注册的第一个函数指针的地址,并在__initcall7_start
(排除)结束,每次以*fn
(这是一个initcall_t
,这是一个void*
,这是 32 位或 64 位)的大小递增位取决于架构)。
do_one_initcall
将简单地调用当前条目指向的函数。
在特定的初始化部分中,决定为什么在另一个初始化函数之前调用初始化函数的原因只是 Makefile 中文件的顺序,因为链接器将__initcall_*
在它们各自的 ELF 初始化中一个接一个地连接符号。部分。
这个事实实际上在内核中使用,例如设备驱动程序(drivers/Makefile
):
# GPIO must come after pinctrl as gpios may need to mux pins etc
obj-y += pinctrl/
obj-y += gpio/
tl;dr:Linux 内核初始化机制非常漂亮,尽管它突出了 GCC 依赖。
module_init
用于标记要用作 Linux 设备驱动程序入口点的函数。
它被称为
do_initcalls()
(对于内置驱动程序)*.ko
模块)每个驱动模块只能有1个。module_init()
这些*_initcall()
函数通常用于设置函数指针以初始化各种子系统。
do_initcalls()
Linux 内核源代码中包含对各种 initcall 列表的调用以及在 Linux 内核启动期间调用它们的相对顺序。
early_initcall()
core_initcall()
postcore_initcall()
arch_initcall()
subsys_initcall()
fs_initcall()
device_initcall()
late_initcall()
modprobe
或模块insmod
。*.ko
module_init()
在设备驱动程序中使用相当于注册一个device_initcall()
.
请记住,在编译期间,在*.o
Linux 内核中链接各种驱动程序目标文件()的顺序很重要;它决定了它们在运行时被调用的顺序。
*_initcall
相同级别的函数
将在启动期间按照它们链接的顺序被调用。
例如,更改 SCSI 驱动程序的链接顺序drivers/scsi/Makefile
将更改检测 SCSI 控制器的顺序,从而更改磁盘的编号。
似乎没有人关注如何配置链接描述文件以提供用于初始化内核代码的函数指针,因此让我们尝试看看 Linux 内核如何为 init 调用创建链接描述文件。
因为上述很好的答案表明Linux C代码如何创建和管理所有initcall,例如如何将函数定义为initcall,访问定义函数的全局变量,以及在初始化时实际调用定义的initcall的函数阶段,我不想再次访问它们。
因此,在这里,我们想重点关注名为initcall_levels[]的全局数组变量的每个元素是如何定义的,它是什么意思,initcall_levels 数组的每个元素指向的内存中包含什么等。
首先,让我们尝试了解变量在 Linux 内核存储库中的定义位置。查看 init/main.c 文件时,您会发现 initcall_levels 数组的所有元素都没有在 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[];
但是,您会发现这些变量在 Linux 存储库的任何 C 源代码中都没有声明,那么这些变量是从哪里来的呢?来自链接描述文件!
Linux 提供了许多帮助程序函数来帮助程序员生成特定于体系结构的链接器脚本文件,它们定义在 linux/include/asm-generic/vmlinux.lds.h 文件中,该文件还为 initcalls 提供了帮助程序。
#define __VMLINUX_SYMBOL(x) _##x
#define __VMLINUX_SYMBOL_STR(x) "_" #x
#else
#define __VMLINUX_SYMBOL(x) x
#define __VMLINUX_SYMBOL_STR(x) #x
#endif
/* Indirect, so macros are expanded before pasting. */
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)
#define INIT_CALLS_LEVEL(level) \
VMLINUX_SYMBOL(__initcall##level##_start) = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
KEEP(*(.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) = .;
我们可以很容易地发现为 initcalls 定义了几个宏。最重要的宏是INIT_CALLS,它发出链接描述文件语法,定义可以在纯 C 代码和输入部分中访问的链接描述文件符号。
详细地说,INIT_CALLS_LEVEL(x) 宏的每次调用都定义了一个名为 __initcall##level_##start 的新符号(请参阅 CPP 中的 ## 连接操作);此符号由VMLINUX_SYMBOL(__initcall##level##_start) = .; . 例如INIT_CALLS_LEVEL(1)宏定义了名为__initcall1_start的链接描述文件符号。
因此,符号 __initcall0_start 到 __initcall7_start 在链接描述文件中定义,并且可以在 C 代码中通过使用 extern 关键字声明来引用。
此外, INIT_CALLS_LEVEL 宏定义了称为 .initcallN.init 的新部分,这里 N 是 0 到 7。生成的部分包含使用提供的宏定义的所有函数,例如 __define_initcall 由部分属性指定。
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
创建的符号和节应由链接描述文件正确配置,使其位于一个节中,即 .init.data 节。为此,使用了 INIT_DATA_SECTION 宏;我们可以发现它调用了我们看过的 INIT_CALLS 宏。
#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 宏,Linux 链接器将__initcall0_start定位到__initcall7_start符号和.init.data节中的 .initcall0.init到.initcall7.init节,它们是背靠背定位的。这里请注意,每个符号不包含任何数据,而是用于定位生成的部分的开始和结束位置。
然后让我们尝试看看编译的Linux内核是否正确包含了生成的符号、节和函数。编译完 Linux 内核后,通过使用 nm 工具,我们可以检索到编译后的 Linux 映像 vmlinux 中定义的所有符号。
//ordering nm result numerical order
$nm -n vmlinux > symbol
$vi symbol
ffffffff828ab1c8 T __initcall0_start
ffffffff828ab1c8 t __initcall_ipc_ns_init0
ffffffff828ab1d0 t __initcall_init_mmap_min_addr0
ffffffff828ab1d8 t __initcall_evm_display_config0
ffffffff828ab1e0 t __initcall_init_cpufreq_transition_notifier_list0
ffffffff828ab1e8 t __initcall_jit_init0
ffffffff828ab1f0 t __initcall_net_ns_init0
ffffffff828ab1f8 T __initcall1_start
ffffffff828ab1f8 t __initcall_xen_pvh_gnttab_setup1
ffffffff828ab200 t __initcall_e820__register_nvs_regions1
ffffffff828ab208 t __initcall_cpufreq_register_tsc_scaling1
......
ffffffff828ab3a8 t __initcall___gnttab_init1s
ffffffff828ab3b0 T __initcall2_start
ffffffff828ab3b0 t __initcall_irq_sysfs_init2
ffffffff828ab3b8 t __initcall_audit_init2
ffffffff828ab3c0 t __initcall_bdi_class_init2
如上所示,在 __initcall0_start 和 __initcall2_start 符号之间,所有用 pure_initcall 宏定义的函数都位于。例如,我们看一下 ipc/shim.c 文件中定义的 ipc_ns_init 函数
static int __init ipc_ns_init(void)
{
const int err = shm_init_ns(&init_ipc_ns);
WARN(err, "ipc: sysv shm_init_ns failed: %d\n", err);
return err;
}
pure_initcall(ipc_ns_init);
如上所示,pure_initcall 宏用于将 ipc_ns_init 函数放在 __initcall0_start 符号所在的 .initcall0.init 部分中。因此,如下面的代码所示,.initcallN.init 部分中的所有函数都是按顺序依次调用的。
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);