前言
做驱动开发前,一定要知道关于Linux的驱动模块化机制(module)运行方式以及加载时机。看到几篇博客写module_init机制挺好借此总结一下分享给大家。
initcall机制的由来
驱动初始化最原始的做法:开发者试图添加一个驱动初始化程序时,在内核启动 init 程序的某个地方直接添加调用自己驱动程序的 xxx_init() 接口函数,在内核启动时就自然会启动这个驱动程序,类似:
void kernel_init()
{
a_init();
b_init();
...
m_init();
}
但是,这种做法在RTOS系统中或许可以,对于 Linux 庞大的系统来说,驱动很多,不可能每添加一个驱动就会改动一下 kernel_init() 代码,这将会是一场灾难。
Linux 内核提供了解决方案:
-
在编译Linux内核的时候,通过使用告知编译器连接,自定义一个专门用来存放这些初始化函数的地址段,将对应的函数入口统一放在一起;
-
驱动程序中调用linux 内核提供的专门的 xxx_init() 接口,由编译器来收集这些入口函数,集中存放在一个地方;
-
内核启动时,统一扫描这段的开始地址,按照顺序执行被添加的驱动初始化程序;
-
init 初始化代码,基本上只会执行一次,因此在这类 xxx_init() 代码所在的特殊段在初始化 完成之后会被内存管理器回收,同时节省了这部分的内存;
本文来探索一下Linux内核源码是如何实现的解决方案,本篇内核代码版本5.10
module_init机制
先看一个module_init机制里最简单的模块例子如下:
#include <linux/module.h>
#include <linux/init.h>
static int hello_init(void)
{
printk(KERN_INFO "Hello World\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_INFO "Bye Bye World\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("pan");
MODULE_DESCRIPTION("hello world ver " VERSION);
MODULE_VERSION(VERSION);
MODULE_LICENSE("GPL");
模块代码有两种运行方式,一个是静态编译连接进内核,在系统启动过程中进行初始化;另外一个是编译成可动态加载的module,通过insmod动态加载重定位到内核。这两种方式可以在Makefile中通过obj-y或obj-m选项进行选择。
那么同样一份C代码如何实现这两种方式的呢?
答案就在于MODULE宏!下面我们一起来分析MODULE宏。(这里所用的Linux内核版本为5.10)定位到Linux内核源码中的 include/linux/module.h,可以看到有如下代码:
#ifndef MODULE
#define module_init(x) __initcall(x);
#else /* MODULE */
...
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) \
__attribute__((alias(#initfn))); \
__CFI_ADDRESSABLE(init_module)
#endif
显然,MODULE宏是由Makefile控制的。上面部分用于将模块静态编译连接进内核,下面部分用于编译可动态加载的模块。本文将分开单独剖析这两种情况下的 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() 接口:
#define module_init(x) __initcall(x);
--> #define __initcall(fn) device_initcall(fn)
--> #define device_initcall(fn) __define_initcall(fn, 6)
--> #define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
module_init() 就是 device_initcall()
initcall 级别
early-->
0-->0s-->
1-->1s-->
2-->2s-->
3-->3s-->
4-->4s-->
5-->5s-->
rootfs-->
6-->6s-->
7-->7s-->
console
举例理解initcall接口
我们在将 module_init(hello_init) 展开为:
static initcall_t __initcall_hello_init6 __used \
__attribute__((__section__(".initcall6.init"))) = hello_init
这里的 initcall_t 是函数指针类型,如下:
typedef int (*initcall_t)(void);
GNU编译工具链支持用户自定义section,所以我们阅读Linux源码时,会发现大量使用如下一类用法:
__attribute__((__section__("section-name")))
之前有总结过一篇__attribute__的使用用法的一篇,没看到的同学可以看看。
__ attribute__用来指定变量或结构位域的特殊属性,其后的双括弧中的内容是属性说明,它的语法格式为:attribute ((attribute-list))。它有位置的约束,通常放于声明的尾部且“ ;” 之前。
这里的attribute-list为__section__(“.initcall6.init”)。通常,编译器将生成的代码存放在.text段中。但有时可能需要其他的段,或者需要将某些函数、变量存放在特殊的段中,section属性就是用来指定将一个函数、变量存放在特定的段中。
所以这里的意思就是:定义一个名为 __initcall_hello_init6 的函数指针变量,并初始化为 hello_init(指向hello_init); 并且该函数指针变量存放于 .initcall6.init 代码段中。
接下来,我们通过查看链接脚本(arch/$(ARCH)/kernel/vmlinux.lds.S)来了解 .initcall6.init 段。
可以看到,.init段中包含 INIT_CALLS,它定义在include/asm-generic/vmlinux.lds.h。INIT_CALLS 展开后可得:
#define INIT_CALLS_LEVEL(level) \
__initcall##level##_start = .; \
KEEP(*(.initcall##level##.init)) \
__initcall##level##s_start = .; \
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"段。
进而展开得到
#define INIT_CALLS \
__initcall_start = .; \
KEEP(*(.initcallearly.init)) \
__initcall0_start = .; \
*(.initcall0.init) \
__initcall0s_start = .; \
*(.initcall0s.init) \
__initcall1_start = .; \
*(.initcall1.init) \
__initcall1s_start = .; \
*(.initcall11s.init) \
__initcall2_start = .; \
*(.initcall2.init) \
__initcall2s_start = .; \
*(.initcall2s.init) \
__initcall3_start = .; \
*(.initcall3.init) \
__initcall3s_start = .; \
*(..initcall3s.init) \
__initcall4_start = .; \
*(.initcall4.init) \
__initcall4s_start = .; \
*(.initcall4s.init) \
__initcall5_start = .; \
*(.initcall5.init) \
__initcall5s_start = .; \
*(.initcall5s.init) \
__initcallrootfs_start = .; \
*(.initcallrootfs.init) \
__initcallrootfss_start = .; \
*(.initcallrootfss.init) \
__initcall6_start = .; \
*(..initcall6.init) \
__initcall6s_start = .; \
*(.initcall6s.init) \
__initcall7_start = .; \
*(.initcall7.init) \
__initcall7s_start = .; \
*(.initcall7s.init) \
__initcall_end = .;
上面这些代码段最终在kernel.img中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序(__initcall_hello_init6 位于 .initcall6.init 段中)。.init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。这一点从内核启动信息可以看到
Freeing unused kernel memory: 124K (80312000 - 80331000)
linux 编译后的initcall 函数
查看编译好的 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 得知:
- __initcall_start 就是第一个 early 级别的 initcall 函数指针,同理 __initcall0_start 就是第一个 level 0 级别的initcall 函数指针,以此类推;
- rootfs 级别的 initcall 函数是插在 level 5s 之后,level 6 级别之前;
- console 级别的函数在 level 7s 之后,__initcall_end 之前;
initcall调用流程
那么存放于 .initcall6.init 段中的 __initcall_hello_init6 是怎么样被调用的呢?我们看文件 init/main.c,代码梳理如下:
start_kernel
--> arch_call_rest_init
--> rest_init
--> kernel_thread(kernel_init, NULL, CLONE_FS);
--> kernel_init
--> kernel_init_freeable
--> do_basic_setup
--> do_initcalls
--> do_initcall_level(level, command_line);
--> do_one_initcall(initcall_t fn)
kernel_init 这个函数是作为一个内核线程被调用的(该线程最后会启动第一个用户进程init)。
我们着重关注 do_initcalls 函数,如下:
static void __init do_initcalls(void)
{
int level;
char *command_line;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
/* Parser modifies command_line, restore it each time */
strcpy(command_line, saved_command_line);
do_initcall_level(level, command_line);
}
}
函数 do_initcall_level 如下:
static void __init do_initcall_level(int level,char *command_line)
{
// 省略
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
initcall_levels 的定义如下:
typedef initcall_t initcall_entry_t;
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_levels[]中的成员来自于include/asm-generic/vmlinux.lds.h文件里的INIT_CALLS 的展开,如“__initcall0_start = .;”,这里的__initcall0_start是一个变量,它跟代码里面定义的变量的作用是一样的,之所以代码里面能够使用__initcall0_start,是因为init/main.c文件中通过 extern 的方法将这些变量引入,如下:
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[]
到这里基本上就明白了,在 do_initcalls 函数中会遍历 initcalls 段中的每一个函数指针,然后执行这个函数指针。因为编译器根据链接脚本的要求将各个函数指针链接到了指定的位置,所以可以放心地用do_one_initcall(*fn)来执行相关初始化函数。
我们例子中的 module_init(hello_init) 是 level6 的 initcalls 段,比较靠后调用__initcall_hello_init6函数,如果是静态编译进内核,则这些函数指针会按照编译先后顺序插入到 initcall6.init 段中,然后等待 do_initcalls 函数调用。
动态加载进内核
动态加载就是编译成可动态加载的module文件(ko文件),通过insmod命令进行动态加载重定位到内核。其作用域和静态链接的代码是完全等价的。所以这种运行方式的优点显而易见:
1、节省运行内存:可根据系统需要运行动态加载模块,以扩充内核功能,不需要时将其卸载,以释放内存空间;
2、调试驱动方便:当需要修改驱动时,只需编译相应模块,而不必重新编译整个内核
因为这样的优点,在进行设备驱动开发时,基本上都是将其编译成可动态加载的模块。但是需要注意,有些模块必须要编译到内核,随内核一起运行,从不卸载,如 vfs、platform_bus等。
源码实现
#else
...
#define module_init(initfn) \
static inline initcall_t __maybe_unused __inittest(void) \
{ return initfn; } \
int init_module(void) __copy(initfn) \
__attribute__((alias(#initfn))); \
__CFI_ADDRESSABLE(init_module)
#endif
module_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(hello_init) 作用就是定义一个变量名 init_module,其地址与 hello_init 是一样的。
编译后所得的 HelloWorld.ko 需要通过insmod 将其加载进内核,由于 insmod 是 busybox 提供的用户层命令,所以我们需要阅读 busybox 源码。代码梳理调用如下:(文件 busybox/modutils/ insmod.c)
insmod_main
--> bb_init_module
--> init_module
而 init_module 定义如下:(文件 busybox/modutils/modutils.c)
#define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)
因此,该系统调用对应内核层的 sys_init_module 函数
关于系统调用,文件(include/linux/syscalls.h)中,有
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
从而形成 sys_init_module 函数。
SYSCALL_DEFINE3实现是在Linux内核源代码(kernel/module.c)下面,代码梳理调用如下:
SYSCALL_DEFINE3(init_module, ...)
-->load_module
--> do_init_module(mod)
--> do_one_initcall(mod->init);
从而执行了mod->init,即module_init(xx)中的xx初始化函数。
参考文章:
- 1、https://blog.csdn.net/weixin_37571125/article/details/78665184
- 2、https://www.cnblogs.com/downey-blog/p/10486653.html
- 3、https://www.cnblogs.com/schips/p/linux_kernel_initcall_and_module_init.html
- 4、https://justinwei.blog.csdn.net/article/details/134183274
本文由mdnice多平台发布