源码基于:Linux 5.4
0. 前言
Linux 对驱动程序提供静态编译进内核和动态加载两种方式,当采用静态方式时,开发者如果想要在系统中启动这个驱动通常调用类似 xxx_init() 接口。
最直观的做法:开发者试图添加一个驱动初始化程序时,在内核启动 init 程序的某个地方直接添加调用自己驱动程序的 xxx_init() 接口函数,在内核启动时就自然会启动这个驱动程序,类似:
void kernel_init()
{a_init();b_init();...m_init();
}
但是,这种做法在小系统中或许可以,对于 linux 庞大的系统来说,驱动很多,不可能每添加一个驱动就会改动一下 kernel_init() 代码,这将会是一场灾难。
Linux 内核提供了解决方案:
- 在编译的时候,通过使用告知编译器连接,自定义一个专门用来存放这些初始化函数的地址段,将对应的函数入口统一放在一起;
- 驱动程序中调用linux 内核提供的专门的 xxx_init() 接口,由编译器来收集这些入口函数,集中存放在一个地方;
- 内核启动时,统一扫描这段的开始地址,按照顺序执行被添加的驱动初始化程序;
- init 初始化代码,基本上只会执行一次,因此在这类 xxx_init() 代码所在的特殊段在初始化 完成之后会被内存管理器回收,同时节省了这部分的内存;
1. initcall 源码
上文提到过 Linux 对驱动程序提供静态编译进内核和动态加载两种方式,Linux 的 initcall 机制也是根据静态编译和动态加载的两种方式选择不同的编译、运行流程。
include/linux/init.h#ifndef MODULE... //静态加载#else... //动态加载#endif
MODULE 是在编译的时候,通过编译器参数来传入。例如,在编译 ko 时会使用如下两个编译选项,如果是链接到内核,则不会使用:
//MakefileKBUILD_AFLAGS_MODULE := -DMODULE
KBUILD_CFLAGS_MODULE := -DMODULE
通过 MODULE 的配置,选择静态编译还是动态加载。
本文将分开单独剖析这两种情况下的 initcall 机制。
2. 静态编译
2.1 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()。
2.2 initcall 级别
2.3 __define_initcall()
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.htypedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);
2.3.1 __used
include/linux/compiler_attributes.h#define __used __attribute__((__used__))
这是一种 attribute 修饰属性的一种,意思是告诉编译器:这个静态符号在编译的时候,即使没有使用也要保留,不能优化掉。
详细可以查看《__attribute__机制详解》一文。
2.3.1 __attribute__ ((__section__(...)))
__attribute__ 是 GNU C 的一大特色,可以用来修饰对象、函数、结构体类型等等。
这里用来修改 section,意思是将作用的函数放入指定的 section name 对应的段中。
详细可以查看《__attribute__机制详解》一文。
2.3.2 __stringify()
include/linux/stringify.h#define __stringify_1(x...) #x
#define __stringify(x...) __stringify_1(x)
将 __stringify() 中内容字符串化。
2.4 举例理解initcall接口
上面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;
2.5 linux 编译后的initcall 函数
通过 arch64-linux-gnu-nm 或者 aarch64-linux-gnu/bin/nm 命令:
aarch64-linux-gnu/bin/nm -n vmlinux | grep -E -C 2 '_initcall.*(_start|_end)$'
会显示编译之后的 initcall 函数:
System.map:282341:ffffffc012032ee0 D __initcall_start
System.map-282342-ffffffc012032ee0 D __setup_end
System.map-282343-ffffffc012032ee8 d __initcall_224_66_trace_init_flags_sys_exitearly
--
System.map-282363-ffffffc012032f88 d __initcall_131_37_dummy_timer_registerearly
System.map-282364-ffffffc012032f90 d __initcall_312_768_initialize_ptr_randomearly
System.map:282365:ffffffc012032f98 D __initcall0_start
System.map-282366-ffffffc012032f98 d __initcall_241_771_bpf_jit_charge_init0
System.map-282367-ffffffc012032fa0 d __initcall_141_53_init_mmap_min_addr0
System.map-282368-ffffffc012032fa8 d __initcall_209_6528_pci_realloc_setup_params0
System.map-282369-ffffffc012032fb0 d __initcall_339_1143_net_ns_init0
System.map:282370:ffffffc012032fb8 D __initcall1_start
System.map-282371-ffffffc012032fb8 d __initcall_160_1437_fpsimd_init1
System.map-282372-ffffffc012032fc0 d __initcall_181_669_tagged_addr_init1
--
System.map-282427-ffffffc012033178 d __initcall_347_1788_init_default_flow_dissectors1
System.map-282428-ffffffc012033180 d __initcall_360_2821_netlink_proto_init1
System.map:282429:ffffffc012033188 D __initcall2_start
...
__initcall 后面跟 __COUNTER__ 和 __LINE__,接着加上初始化函数 fun,最后是 initcall 的级别。
当然通过命令 readelf 或者 objdump (objdump -h vmlinux.o)都能看到字段:
Sections:
Idx Name Size VMA LMA File off Algn0 .initcall0.init 00000020 0000000000000000 0000000000000000 00000040 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA1 .initcall1.init 000001d0 0000000000000000 0000000000000000 00000060 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA2 .initcall2.init 00000138 0000000000000000 0000000000000000 00000230 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA3 .initcall2s.init 00000008 0000000000000000 0000000000000000 00000368 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA4 .initcall3.init 000000b0 0000000000000000 0000000000000000 00000370 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA5 .initcall3s.init 00000008 0000000000000000 0000000000000000 00000420 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA6 .initcall4.init 000004f0 0000000000000000 0000000000000000 00000428 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA7 .initcall4s.init 00000008 0000000000000000 0000000000000000 00000918 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA8 .initcall5.init 00000168 0000000000000000 0000000000000000 00000920 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA9 .initcall5s.init 00000008 0000000000000000 0000000000000000 00000a88 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA10 .initcall6.init 00001140 0000000000000000 0000000000000000 00000a90 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA11 .initcall7.init 00000140 0000000000000000 0000000000000000 00001bd0 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA12 .initcall7s.init 00000028 0000000000000000 0000000000000000 00001d10 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA13 .con_initcall.init 00000008 0000000000000000 0000000000000000 00001d38 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA14 .initcallearly.init 000000b8 0000000000000000 0000000000000000 00001d40 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA15 .initcallrootfs.init 00000008 0000000000000000 0000000000000000 00001df8 2**3CONTENTS, ALLOC, LOAD, RELOC, DATA
2.6 initcall 的函数如何被调用
init/main.cstart_kernel()---->arch_call_rest_init()---->rest_init()---->kernel_init()---->kernel_init_freeable()---->do_basic_setup()---->do_initcalls()
init/main.cstatic 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 循环是一个指针数组,该数组会被存放在 __initdata 段。
另外,这个指针数组的类型为 initcall_entry_t,其实就是在上文第 2.3 节提到的 initcall_t 函数指针类型。
继续来看下这个指针数组中的元素:__initcall0_start ~ __initcall_end,而这些元素的值在本 c 文件中已经声明:
init/main.cextern 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 中。
2.6.1 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 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。
2.6.2 do_initcall_level()
init/main.cstatic 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_one_initcall() 的参数就是获取到函数的函数指针。
init/main.cint __init_or_module do_one_initcall(initcall_t fn)
{...ret = fn();...return ret;
}
3. 动态加载
当模块以 ko 的形式存在,并被加载重定位到内核,其作用域和静态连接的代码是完全等价的。这种运行方式的优点:
- 可根据系统需要运行动态加载模块,以扩充内核功能,不需要时可以将其卸载,以释放内存空间;
- 当需要修改内核功能时,只需要编译模块,而不必重新编译整个内核;
当然,有些模块是必须要编译到内核,随内核一起运行,从不卸载,例如 vfs、platform_bus 等。
3.1 initcall 接口
当动态加载时,会在Makefile中添加上 MODULE 的定义:
//MakefileKBUILD_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() 共做了两件事情:
- 定义 static initcall_t __inittest() { ... }
- 声明 int init_module();
__inittest 仅仅是为了检测定义的函数是否符合 initcall_t 类型,不过不是 __inittest 类型在编译的时候会报错。所以真正使用的是 init_module() 函数的声明。
注意:
- __copy(initfn):从 initfn 赋值函数属性,从 gcc-9 开始支持;
- __attribute__((alias(#initfn))):为 init_module 创建别名,指向原来的 initfn;
这里alias 是 gcc 的特有属性,将定义 init_module 为函数initfn 的别名。即对于module_init() 作用就是定义一个变量名 init_module,其地址与 initfn 是一样的。
3.2 insmod
编译后的模块 xxx.ko 需要通过 insmod 或 modprobe 将其加载到内核,由于 insmod 是bubybox 提供的用户层命令,所以需要阅读 busybox 源码:
modutils/insmod.cint 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.cint 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_MODULESif (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;}}
# endifimage_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);elsefree(image);return rc;
}
init_module() 定义如下:
modutils/modutils.c
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
最终进行syscall 的系统调用:
kernel/module.cSYSCALL_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.cload_module()---->do_init_module()---->do_one_initcall()
最终 do_one_initcall() 同静态编译:
init/main.cint __init_or_module do_one_initcall(initcall_t fn)
{...ret = fn();...return ret;
}