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

module_init 底层实现原理_第1张图片


静态加载

在 “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阶段),所以处于这个阶段的函数指针都会被调用执行


动态加载

它的实现逻辑如下:
module_init 底层实现原理_第2张图片

解释:

  1. drv.c 是我们写的驱动程序,我们都知道 module_init 将 drv_init 定义为驱动入口函数
  2. 驱动程序都会包含 “linux/init.h” 头文件,该头文件中定义了 “module_init” 这个宏,通过 alias 对函数指针起别名init_module
  3. 编译ko文件时,会生成 xxx_mod.c 文件,init_module 会被传入 xxx_mod.c ,被 this_module 结构体使用
  4. this_module结构体会被链接到ko文件中,进而内核在加载ko文件时可以解析 this_module 结构体,得到驱动入口函数等
  5. 当执行insmod命令时,insmod会读取ko文件的内容,解析出模块的入口函数地址,并将其添加到内核的运行队列中,会自动执行这些入口函数。

问:内核怎么知道我们用的是动态加载?

​ 答:因为定义了 MODULE

问:那 MODULE 宏是谁定义的呢?

​ 答:GCC编译时加入的 DMODULE 参数
module_init 底层实现原理_第3张图片

生成 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.osg90_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

你可能感兴趣的:(驱动,linux,module_init,module)