源码基于:Linux 5.4
Linux 对驱动程序提供静态编译进内核和动态加载两种方式,当采用静态方式时,开发者如果想要在系统中启动这个驱动通常调用类似 xxx_init() 接口。
最直观的做法:开发者试图添加一个驱动初始化程序时,在内核启动 init 程序的某个地方直接添加调用自己驱动程序的 xxx_init() 接口函数,在内核启动时就自然会启动这个驱动程序,类似:
void kernel_init()
{
a_init();
b_init();
...
m_init();
}
但是,这种做法在小系统中或许可以,对于 linux 庞大的系统来说,驱动很多,不可能每添加一个驱动就会改动一下 kernel_init() 代码,这将会是一场灾难。
Linux 内核提供了解决方案:
上文提到过 Linux 对驱动程序提供静态编译进内核和动态加载两种方式,Linux 的 initcall 机制也是根据静态编译和动态加载的两种方式选择不同的编译、运行流程。
include/linux/init.h
#ifndef MODULE
... //静态加载
#else
... //动态加载
#endif
MODULE 是在编译的时候,通过编译器参数来传入。例如,在编译 ko 时会使用如下两个编译选项,如果是链接到内核,则不会使用:
//Makefile
KBUILD_AFLAGS_MODULE := -DMODULE
KBUILD_CFLAGS_MODULE := -DMODULE
通过 MODULE 的配置,选择静态编译还是动态加载。
本文将分开单独剖析这两种情况下的 initcall 机制。
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)
#define __initcall(fn) device_initcall(fn)
#define __exitcall(fn) \
static exitcall_t __exitcall_##fn __exit_call = fn
#define console_initcall(fn) ___define_initcall(fn, con, .con_initcall)
对于静态编译 initcall 接口如上,其中 pure_initcall() 只能在静态编译中存在。
当然,对于静态编译的驱动也可以调佣 module_init() 接口:
include/linux/module.h
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
此时的 module_init() 就是 device_initcall()。
initcall 在静态编译时,函数的调用顺序是有级别控制的,详细可以查看下文第 2.5 节和第 2.6 节。
调用的顺序是:
early-->
0-->0s-->
1-->1s-->
2-->2s-->
3-->3s-->
4-->4s-->
5-->5s-->
rootfs-->
6-->6s-->
7-->7s-->
console
include/linux/init.h
#ifdef CONFIG_LTO_CLANG
/*
* With LTO, the compiler doesn't necessarily obey link order for
* initcalls, and the initcall variable needs to be globally unique
* to avoid naming collisions. In order to preserve the correct
* order, we add each variable into its own section and generate a
* linker script (in scripts/link-vmlinux.sh) to ensure the order
* remains correct. We also add a __COUNTER__ prefix to the name,
* so we can retain the order of initcalls within each compilation
* unit, and __LINE__ to make the names more unique.
*/
#define ___lto_initcall(c, l, fn, id, __sec) \
static initcall_t __initcall_##c##_##l##_##fn##id __used \
__attribute__((__section__( #__sec \
__stringify(.init..##c##_##l##_##fn)))) = fn;
#define __lto_initcall(c, l, fn, id, __sec) \
___lto_initcall(c, l, fn, id, __sec)
#define ___define_initcall(fn, id, __sec) \
__lto_initcall(__COUNTER__, __LINE__, fn, id, __sec)
#else
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
#endif
#endif
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
下文会继续细化分析,这里提前提示:
__define_initcall() 其实就是定义了一个 static initcall_t 的函数指针
include/linux/init.h
typedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);
include/linux/compiler_attributes.h
#define __used __attribute__((__used__))
这是一种 attribute 修饰属性的一种,意思是告诉编译器:这个静态符号在编译的时候,即使没有使用也要保留,不能优化掉。
详细可以查看《__attribute__机制详解》一文。
__attribute__ 是 GNU C 的一大特色,可以用来修饰对象、函数、结构体类型等等。
这里用来修改 section,意思是将作用的函数放入指定的 section name 对应的段中。
详细可以查看《__attribute__机制详解》一文。
include/linux/stringify.h
#define __stringify_1(x...) #x
#define __stringify(x...) __stringify_1(x)
将 __stringify() 中内容字符串化。
上面initcall 接口最终有各种宏转换,可能看着还是一头雾水。本小节用实例来剖析这个接口。
假如,我们在驱动使用如下接口:
module_init(hello_init);
那么,在编译的时候编译器会通过 initcall 接口产生:
static initcall_t __initcall_1_23_hello_init6 __attribute__(__used) \
__attribute__((__section__(".initcall6.init..1_23_hello_init"))) = hello_init;
查看编译好的 System.map:
...
ffffffc012032ee0 d __initcall_223_42_trace_init_flags_sys_enterearly
ffffffc012032ee0 D __initcall_start
ffffffc012032ee0 D __setup_end
ffffffc012032ee8 d __initcall_224_66_trace_init_flags_sys_exitearly
ffffffc012032ef0 d __initcall_163_146_cpu_suspend_initearly
ffffffc012032ef8 d __initcall_151_267_asids_initearly
ffffffc012032f00 d __initcall_167_688_spawn_ksoftirqdearly
ffffffc012032f08 d __initcall_343_6656_migration_initearly
...
ffffffc012032f90 d __initcall_312_768_initialize_ptr_randomearly
ffffffc012032f98 D __initcall0_start
ffffffc012032f98 d __initcall_241_771_bpf_jit_charge_init0
ffffffc012032fa0 d __initcall_141_53_init_mmap_min_addr0
ffffffc012032fa8 d __initcall_209_6528_pci_realloc_setup_params0
ffffffc012032fb0 d __initcall_339_1143_net_ns_init0
ffffffc012032fb8 D __initcall1_start
ffffffc012032fb8 d __initcall_160_1437_fpsimd_init1
ffffffc012032fc0 d __initcall_181_669_tagged_addr_init1
...
ffffffc012033178 d __initcall_347_1788_init_default_flow_dissectors1
ffffffc012033180 d __initcall_360_2821_netlink_proto_init1
ffffffc012033188 D __initcall2_start
ffffffc012033188 d __initcall_165_139_debug_monitors_init2
ffffffc012033190 d __initcall_141_333_irq_sysfs_init2
...
ffffffc0120332b8 d __initcall_304_814_kobject_uevent_init2
ffffffc0120332c0 d __initcall_184_1686_msm_rpm_driver_init2s
ffffffc0120332c8 D __initcall3_start
ffffffc0120332c8 d __initcall_173_390_debug_traps_init3
ffffffc0120332d0 d __initcall_161_275_reserve_memblock_reserved_regions3
...
ffffffc012033370 d __initcall_132_5273_gsi_init3
ffffffc012033378 d __initcall_149_547_of_platform_default_populate_init3s
ffffffc012033380 D __initcall4_start
...
ffffffc012033878 D __initcall5_start
...
ffffffc0120339d8 d __initcall_317_1188_xsk_init5
ffffffc0120339e0 d __initcall_211_194_pci_apply_final_quirks5s
ffffffc0120339e8 d __initcall_168_680_populate_rootfsrootfs
ffffffc0120339e8 D __initcallrootfs_start
ffffffc0120339f0 D __initcall6_start
...
ffffffc012034b30 D __initcall7_start
...
ffffffc012034c88 d __initcall_150_554_of_platform_sync_state_init7s
ffffffc012034c90 d __initcall_123_29_alsa_sound_last_init7s
ffffffc012034c98 D __con_initcall_start
ffffffc012034c98 d __initcall_151_246_hvc_console_initcon
ffffffc012034c98 D __initcall_end
ffffffc012034ca0 D __con_initcall_end
从 System.map 得知:
当然通过命令 readelf 或者 objdump (objdump -h vmlinux.o)都能看到各个字段:
Sections:
Idx Name Size VMA LMA File off Algn
0 .initcall0.init 00000020 0000000000000000 0000000000000000 00000040 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
1 .initcall1.init 000001d0 0000000000000000 0000000000000000 00000060 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
2 .initcall2.init 00000138 0000000000000000 0000000000000000 00000230 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
3 .initcall2s.init 00000008 0000000000000000 0000000000000000 00000368 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
4 .initcall3.init 000000b0 0000000000000000 0000000000000000 00000370 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
5 .initcall3s.init 00000008 0000000000000000 0000000000000000 00000420 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
6 .initcall4.init 000004f0 0000000000000000 0000000000000000 00000428 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
7 .initcall4s.init 00000008 0000000000000000 0000000000000000 00000918 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
8 .initcall5.init 00000168 0000000000000000 0000000000000000 00000920 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
9 .initcall5s.init 00000008 0000000000000000 0000000000000000 00000a88 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
10 .initcall6.init 00001140 0000000000000000 0000000000000000 00000a90 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
11 .initcall7.init 00000140 0000000000000000 0000000000000000 00001bd0 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
12 .initcall7s.init 00000028 0000000000000000 0000000000000000 00001d10 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
13 .con_initcall.init 00000008 0000000000000000 0000000000000000 00001d38 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
14 .initcallearly.init 000000b8 0000000000000000 0000000000000000 00001d40 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
15 .initcallrootfs.init 00000008 0000000000000000 0000000000000000 00001df8 2**3
CONTENTS, ALLOC, LOAD, RELOC, DATA
init/main.c
start_kernel()
---->arch_call_rest_init()
---->rest_init()
---->kernel_init()
---->kernel_init_freeable()
---->do_basic_setup()
---->do_initcalls()
init/main.c
static initcall_entry_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
for 循环针对一个指针数组轮询,该数组是一个静态的 initcall_entry_t 类型,这些数据都存放在 __initdata 段。
指针数组的类型为 initcall_entry_t,是 initcall_t 的另一种叫法,在上文第 2.3 节已经说明过 initcall_t 函数指针类型。
继续来看下这个指针数组中的元素:__initcall0_start ~ __initcall_end,而这些元素的值在本 c 文件中已经声明:
init/main.c
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __initcall_end[];
不难看出,数组 initcall_levels 中元素存放的是这些函数指针数组的首地址。
那么这些实际的指针数组是在哪里呢?从上文得知,initcall 函数都会被定义成 static initcall_t 类型,并且保存在 .initcall##level##.init 段中,那么 initcall_levels 与其是怎么关联的呢?
答案在 vmlinux.lds.h 中。
include/asm-generic/vmlinux.lds.h
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
KEEP(*(.initcall##level##s.init)) \
#define INIT_CALLS \
__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) \
__initcall_end = .;
在这里首先定义了__initcall_start
,将其关联到".initcallearly.init"
段。
然后对每个level定义了INIT_CALLS_LEVEL(level)
,将INIT_CALLS_LEVEL(level)
展开之后的结果是定义 __initcall##level##_start
,并将__initcall##level##_start
关联到".initcall##level##.init
"段和".initcall##level##s.init
"段。
__initcall_start = .; \
*(.initcallearly.init) \
__initcall0_start = .; \
*(.initcall0.init) \
*(.initcall0s.init) \
// 省略1、2、3、4、5
__initcallrootfs_start = .; \
*(.initcallrootfs.init) \
*(.initcallrootfss.init) \
__initcall6_start = .; \
*(.initcall6.init) \
*(.initcall6s.init) \
__initcall7_start = .; \
*(.initcall7.init) \
*(.initcall7s.init) \
__initcall_end = .;
上面这些代码段最终在kernel.img中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序。
.init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。
init/main.c
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));
}
do_initcall_level() 函数的参数 level 是之前是initcall_levels 数组的索引,从第0个开始。
这里再用一个 for 循环,跳到 initcall_levels 内部元素 (函数指针数组)进行轮询,fn 初始值为函数指针数组的起始地址,后面 fn++ 相当于函数指针 +1,即跳到下一个函数指针。
即,for 循环中,根据传入的 level,确定需要轮询的 .initcall##level##.init 段的所有函数指针,一直到下一个 .intcall##(level+1)##.init 段。
另外,需要注意 do_one_initcall() 的参数就是获取函数指针的内容,而这个内容就是注册进来的 initcall 的实际初始化函数。
如上面的举例:
module_init(hello_init);
这里最终就看成调用 do_one_initcall(hello_init);
init/main.c
int __init_or_module do_one_initcall(initcall_t fn)
{
...
ret = fn();
...
return ret;
}
当模块以 ko 的形式存在,并被加载重定位到内核,其作用域和静态连接的代码是完全等价的。这种运行方式的优点:
当然,有些模块是必须要编译到内核,随内核一起运行,从不卸载,例如 vfs、platform_bus 等。
当动态加载时,会在Makefile中添加上 MODULE 的定义:
//Makefile
KBUILD_AFLAGS_MODULE := -DMODULE
KBUILD_CFLAGS_MODULE := -DMODULE
而initcall 代码也将从 init.h 转换到 module.h:
include/module.h
#ifndef MODULE
...
#else
#define early_initcall(fn) module_init(fn)
#define core_initcall(fn) module_init(fn)
#define core_initcall_sync(fn) module_init(fn)
#define postcore_initcall(fn) module_init(fn)
#define postcore_initcall_sync(fn) module_init(fn)
#define arch_initcall(fn) module_init(fn)
#define subsys_initcall(fn) module_init(fn)
#define subsys_initcall_sync(fn) module_init(fn)
#define fs_initcall(fn) module_init(fn)
#define fs_initcall_sync(fn) module_init(fn)
#define rootfs_initcall(fn) module_init(fn)
#define device_initcall(fn) module_init(fn)
#define device_initcall_sync(fn) module_init(fn)
#define late_initcall(fn) module_init(fn)
#define late_initcall_sync(fn) module_init(fn)
#define console_initcall(fn) module_init(fn)
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __maybe_unused __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __copy(exitfn) __attribute__((alias(#exitfn)));
#endif
moudle_init() 共做了两件事情:
__inittest 仅仅是为了检测定义的函数是否符合 initcall_t 类型,不过不是 __inittest 类型在编译的时候会报错。所以真正使用的是 init_module() 函数的声明。
注意:
这里alias 是 gcc 的特有属性,将定义 init_module 为函数initfn 的别名。即对于module_init() 作用就是定义一个变量名 init_module,其地址与 initfn 是一样的。
编译后的模块 xxx.ko 需要通过 insmod 或 modprobe 将其加载到内核,由于 insmod 是bubybox 提供的用户层命令,所以需要阅读 busybox 源码:
modutils/insmod.c
int insmod_main(int argc UNUSED_PARAM, char **argv)
{
char *filename;
int rc;
/* Compat note:
* 2.6 style insmod has no options and required filename
* (not module name - .ko can't be omitted).
* 2.4 style insmod can take module name without .o
* and performs module search in default directories
* or in $MODPATH.
*/
IF_FEATURE_2_4_MODULES(
getopt32(argv, INSMOD_OPTS INSMOD_ARGS);
argv += optind - 1;
);
filename = *++argv;
if (!filename)
bb_show_usage();
rc = bb_init_module(filename, parse_cmdline_module_options(argv, /*quote_spaces:*/ 0));
if (rc)
bb_error_msg("can't insert '%s': %s", filename, moderror(rc));
return rc;
}
bb_init_module():
modutils/modutils.c
int FAST_FUNC bb_init_module(const char *filename, const char *options)
{
size_t image_size;
char *image;
int rc;
bool mmaped;
if (!options)
options = "";
//TODO: audit bb_init_module_24 to match error code convention
#if ENABLE_FEATURE_2_4_MODULES
if (get_linux_version_code() < KERNEL_VERSION(2,6,0))
return bb_init_module_24(filename, options);
#endif
/*
* First we try finit_module if available. Some kernels are configured
* to only allow loading of modules off of secure storage (like a read-
* only rootfs) which needs the finit_module call. If it fails, we fall
* back to normal module loading to support compressed modules.
*/
# ifdef __NR_finit_module
{
int fd = open(filename, O_RDONLY | O_CLOEXEC);
if (fd >= 0) {
rc = finit_module(fd, options, 0) != 0;
close(fd);
if (rc == 0)
return rc;
}
}
# endif
image_size = INT_MAX - 4095;
mmaped = 0;
image = try_to_mmap_module(filename, &image_size);
if (image) {
mmaped = 1;
} else {
errno = ENOMEM; /* may be changed by e.g. open errors below */
image = xmalloc_open_zipped_read_close(filename, &image_size);
if (!image)
return -errno;
}
errno = 0;
init_module(image, image_size, options);
rc = errno;
if (mmaped)
munmap(image, image_size);
else
free(image);
return rc;
}
init_module() 定义如下:
modutils/modutils.c
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
最终进行syscall 的系统调用:
kernel/module.c
SYSCALL_DEFINE3(init_module, void __user *, umod,
unsigned long, len, const char __user *, uargs)
{
int err;
struct load_info info = { };
err = may_init_module();
if (err)
return err;
pr_debug("init_module: umod=%p, len=%lu, uargs=%p\n",
umod, len, uargs);
err = copy_module_from_user(umod, len, &info);
if (err)
return err;
return load_module(&info, uargs, 0);
}
其实无论是 insmod 或者是 modprobe,最终都是调用到内核的 load_module()。
下面的流程为:
kernel/module.c
load_module()
---->do_init_module()
---->do_one_initcall()
最终 do_one_initcall() 同静态编译:
init/main.c
int __init_or_module do_one_initcall(initcall_t fn)
{
...
ret = fn();
...
return ret;
}