Jooooooooey

Linux | 驱动加载流程,module_init背后的故事

2024-04-11

本文谈论的内核代码,内核版本4.0以上

驱动的起点|前言

驱动开发工程师都知道module_init函数
在编写一个驱动的时候,大部分情况都是从module_init函数开始
通过在module_init调用驱动的init函数
在驱动的init函数中,进行device结构体注册、driver结构体绑定、驱动设备的硬件初始化、其他io、file...结构体绑定

module_init

使用module_init注册驱动的起点

module_init是一个宏,定义在include/linux/module.h中
#define module_init(x) __initcall(x);

__initcall(x)也是一个宏,在文件include/linux/init.h中定义:
#define __initcall(fn) device_initcall(fn)

initcall

结合上面两个宏,module_init的本质,就是将驱动中module_init(x)传入的函数x
定义成__define_initcall(x,6)

其他注册驱动的宏

有一些驱动,不使用module_init作为初始化起点
比如平台类驱动,使用module_platform_driver(x)
混杂类设备驱动,使用module_misc_device(x)
i2c总线设备驱动,使用module_i2c_driver(x)
但是这些宏,本质上最后还是通过几次宏的传递,调用了module_init

下面以平台设备驱动注册驱动时使用的module_platform_driver举例

定义在include/linux/device.h下
#define module_platform_driver(__platform_driver)
module_driver(__platform_driver, platform_driver_register,
platform_driver_unregister)

比如misc混杂设备
#define module_misc_device(__misc_device)
    module_driver(__misc_device, misc_register, misc_deregister)

module_driver的定义
initcall

所以所有的驱动,编译进内核的驱动,本质上都是通过
module_init这个函数注册进内核

__define_initcall

通过上面的分析可知,绝大部分的驱动,初始化的起点函数x
最后都演变成__define_initcall(x,6)
那么define_initcall是什么作用
initcall

综上,当使用module_init注册一个驱动函数时,比如

module_init(test_init);

通过几个宏的嵌套,最终变成
static initcall_t __initcall_test_init6  __attribute__((__section__(".initcall6.init"))) = test_init;

上面一行的代码,意思是指示编译器
把初始化函数地址保存到名为”.initcall6.init” 的数据段中。

驱动的初始化函数如何被执行

接上文,所有的驱动函数的起点函数,被编译到”.initcall6.init” 的数据段中之后
什么时候会被执行呢

答案是,在内核启动过程中,内核的起点,init/main.c文件中

内核的起点函数 start_kernel
进过一系列初始化,硬件设置,cpu设置,最后一步调用rest_init

rest_init中调用kernel_thread创建内核线程,也就是著名的1号进程kernel_init
进程号pid=1的进程

kernel_init线程
kernel_init_freeable

do_basic_setup

do_initcalls

1
2
3
4
5
6
7
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,他负责遍历initcall数据段中排列的各个初始化函数

至此,内核通过module_init注册进内核的初始化代码,将被按编译顺序依次遍历执行

总结

  1. 驱动的入口函数X,基本上最后都是使用module_init(x)
  2. 在内核编译阶段,被编译到initcall数据段
  3. 内核启动时,在1号进程中通过do_initcalls函数去遍历initcall数据段上的函数
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章