module_init底层原理
可以结合我录制的视频讲解看哦:module_init底层实现 | 驱动加载 |【源码解析】
module_init 其实是一个宏,它的作用是:告诉内核,该驱动程序的入口函数地址
实际上驱动的加载分为两种:静态加载、动态加载
-
静态加载就是把驱动程序直接编译到内核里,系统启动后可以直接调用。静态加载的缺点是调试起来比较麻烦,每次修改一个地方都要重新编译下载内核,效率较低。
-
动态加载利用了Linux的module特性,可以在系统启动后用insmod命令把驱动程序(.ko文件)加载上去,在不需要的时候用rmmod命令来卸载。
在 “linux/init.h” 里我们可以看到
#ifndef MODULE
......
......
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
#else /* MODULE */
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));
#define module_exit(exitfn) \
static inline exitcall_t __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __attribute__((alias(#exitfn)));
......
......
#endif
静态加载
在 “linux/init.h” 中找到如下代码
#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_##drv_init##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
typedef int (*initcall_t)(void);
所以,静态加载宏展开后如下
module_init(drv_init)
--->__initcall(drv_init)
--->device_initcall(drv_init)
--->__define_initcall(drv_init, 6)
--->static initcall_t __initcall_drv_init6 __used \
__attribute__((__section__(".initcall6.init"))) = drv_init; \
LTO_REFERENCE_INITCALL(__initcall_drv_init6)
-
static initcall_t __initcall_drv_init6
相当于定义了一个静态变量,该变量名是 __initcall_drv_init6 ,类型是 initcall_t 函数指针
-
__used __attribute__((__section__(".initcall6.init"))) = drv_init;
__used
:表示该函数仅用于声明,不实际调用(GCC编译器提供的一种属性)__attribute__
:用于向编译器传递特定的属性或约束(GCC编译器提供的一种属性)__section__((section-name))
: 将函数或变量放置在指定的内存段中(GCC编译器提供的一种属性)
表示将 drv_init 函数指针赋给该变量,告诉链接器将该变量放置在特定的内存段中,以便在加载驱动程序时能够正确地找到和执行该函数
-
LTO_REFERENCE_INITCALL(__initcall_drv_init6)
: 这是一个宏定义,用于引用初始化函数。它的作用是将__initcall_drv_init6
作为引用传递给链接器,以便在链接过程中正确处理该函数
静态加载驱动的函数调用关系
start_kernel(void)
rest_init(void)
kernel_init(void * unused)
do_basic_setup(void)
do_initcalls(void)
do_initcall_level(int level)
do_one_initcall(*fn);
-
do_initcalls(void): 用于执行在启动过程中按照 initcall_levels 的参数指定的初始化调用级别进行的初始化工作。 例如,如果initcall_levels被设置为2,则在系统启动的第三个阶段(即设备驱动初始化阶段)中,do_initcall_level函数将被调用,以执行与设备驱动程序相关的初始化工作。
-
initcall_levels: 它决定了在内核启动过程中,哪些函数会在哪个阶段被调用。initcall_levels的值可以是0到6,每个值代表一个初始化调用级别。
以下是各个调用级别的简要说明:
- 0(系统初始化):包括基本的内存管理和硬件初始化。
- 1(CPU和内存初始化):包括CPU、内存和中断控制器的初始化。
- 2(设备驱动初始化):包括设备树的解析和设备驱动的注册。
- 3(文件系统初始化):包括文件系统的挂载和初始化。
- 4(进程0初始化):包括进程0的创建和初始化。
- 5(用户空间初始化):包括用户空间的初始化,如udev、sysfs等。
- 6(虚拟化和网络初始化):包括虚拟化和网络设备的初始化。
/* 在内核目录的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, }; static void __init do_initcalls(void) { int level; for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) do_initcall_level(level); }
-
-
do_initcall_level(int level): 用于执行在启动过程中按照initcall_levels参数指定的初始化调用级别进行的初始化工作。它只执行某个指定的阶段内的初始化函数(由do_one_initcall函数完成)。
/* 在内核目录的init/main.c中 */ static void __init do_initcall_level(int level) { initcall_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, &repair_env_string); for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn); }
-
.initcall6.init 就在内核启动过程的某一个阶段(应该是第7阶段),所以处于这个阶段的函数指针都会被调用执行
动态加载
它的实现逻辑如下:
解释:
- drv.c 是我们写的驱动程序,我们都知道 module_init 将 drv_init 定义为驱动入口函数
- 驱动程序都会包含 “linux/init.h” 头文件,该头文件中定义了 “module_init” 这个宏,通过 alias 对函数指针起别名为 init_module
- 编译ko文件时,会生成 xxx_mod.c 文件,init_module 会被传入 xxx_mod.c ,被 this_module 结构体使用
- this_module结构体会被链接到ko文件中,进而内核在加载ko文件时可以解析 this_module 结构体,得到驱动入口函数等
- 当执行insmod命令时,insmod会读取ko文件的内容,解析出模块的入口函数地址,并将其添加到内核的运行队列中,会自动执行这些入口函数。
问:内核怎么知道我们用的是动态加载?
答:因为定义了 MODULE 宏
问:那 MODULE 宏是谁定义的呢?
答:GCC编译时加入的 DMODULE 参数
生成 drv.ko 文件的详细步骤
Makefile
KERN_DIR = 内核目录
PWD ?= $(shell pwd)
all:
make -C $(KERN_DIR) M=$(PWD) modules
$(CROSS_COMPILE)gcc -o sg90_test sg90_test.c
clean:
make -C $(KERN_DIR) M=$(PWD) modules clean
rm -rf modules.order
rm -f sg90_test
obj-m += sg90_drv.o
看一下make后的编译过程
make -C /home/me/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek M=/home/me/Linux_ARM/git/imx6-ull-project/01.智能家居/sg90 modules
make[1]: 进入目录“/home/me/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek”
CC [M] /home/me/Linux_ARM/git/imx6-ull-project/01.智能家居/sg90/sg90_drv.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/me/Linux_ARM/git/imx6-ull-project/01.智能家居/sg90/sg90_drv.mod.o
LD [M] /home/me/Linux_ARM/git/imx6-ull-project/01.智能家居/sg90/sg90_drv.ko
make[1]: 离开目录“/home/me/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek”
arm-linux-gnueabihf-gcc -o sg90_test sg90_test.c
不难看出,编译的过程大致为:
-
进入内核目录,找到当前目录下obj-m += sg90_drv.o对应的 sg90_drv.c 文件,并使用内核目录中的 Makefile 将其编译成 sg90_drv.o
-
MODPOST 1 modules:运行MODPOST生成临 sg90_drv.mod.c,而后编译生成 sg90_drv.mod.o
-
LD [M] :链接 sg90_drv.o 和 sg90_drv.mod.o 生成 sg90_drv.ko (init_module 被传入 __this_module结构体)
参考资料
【内核加载驱动机制详解(module_init & module_exit)】https://blog.csdn.net/weixin_42031299/article/details/124394613
【内核中__init 和module_init宏的作用】https://blog.csdn.net/gjioui123/article/details/129279220
【.mod.c是什么文件,及内核模块Makefile模板】https://blog.csdn.net/echoisland/article/details/7079586