linux驱动,之module_init,initcall 动态和静态加载驱动级别顺序,define_initcall,initcallx_start,initcall_levels的原理

本文介绍了Linux内核初始化调用宏,如pure_initcall、core_initcall等,及其对应的启动顺序。模块initcall的启动序号为6,通过do_initcalls函数执行。驱动模块在内核启动过程中的启动次序较为靠后,具体顺序可从system.map文件中查看。同一优先级的驱动启动顺序由Makefile中.o文件的链接顺序决定。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文,写一篇,驱动是如何被内核执行的,以及他们的执行顺序。

一、驱动在加载注册时,启动顺序,发生了什么

驱动注册到内核的接口就是module_init,定义在头文件include/linux/init.h。在一个驱动模块中,常会写下如下的代码:

int __init lcd_init(void)
{
	。。。。
	return 0;
}

module_init(lcd_init);

lcd_init函数是这个驱动模块的一个入口函数,module_init是驱动向内核注册的入口函数。module_init将在内核启动,do_initcalls期间调用。
这个函数会调用内部宏module_init(x) ,然后是如何在内核初始化时被调用的,或是在模块被插入时(insmod命令)被调用的,把宏一步步展开。

先看下内核对这些宏定义

#define device_initcall(fn) 	__define_initcall("6",fn,6)
#define __initcall(fn) 			device_initcall(fn)
#define module_init(x)  		__initcall(x);

最终会发现哈,module_init(lcd_init),最终会指向这个宏__define_initcall(lcd_init , 6);

来分析下__define_initcall这个宏,是怎么回事。

#define __define_initcall(fn, id) \static initcall_t __initcall_##fn##id __used \__attribute__((__section__(".initcall" #id ".init"))) = fn

看下它们传入的是什么
fn,是初始化函数的名称比如lcd_init,
id,是一个整数值,用于标识初始化函数所属的组别。
initcall_t,这是一个函数指针类型,typedef int (*initcall_t)(void),这个指针指向的函数型如: int fun(void);

attribute((section(.initcall” #id “.init”))): 这是一个属性声明,用于将初始化函数放置在特定的代码段中。#id是一个预处理器的字符串化操作,将id参数转换为字符串。这样,初始化函数就会被放置在名为.initcallX.init的代码段中,其中X是id的值。

所以最终就是绑定一个函数指针,展开后就成如下的样子

static   initcall_t    __initcall_lcd_init6    __used  __attribute__((__section__(".initcall6.init"))) = lcd_init;

在这里插入图片描述

深入研究内核,发现,module_init,的启动序号为6,它的展开后就是__define_initcall(“6”,fn,6),__define_initcall向内核注册了级别顺序。

可以看下内核对各个模块,赋予的启动级别,顺序,最先启动的是arm,arch芯片架构相关的东西,容纳后是fs文件系统,最后比较后面是驱动,device_initcall就是驱动的定义的顺序。

#define pure_initcall(fn) 			__define_initcall("0",fn,0)
#define core_initcall(fn) 			__define_initcall("1",fn,1)
#define core_initcall_sync(fn)		__define_initcall("1s",fn,1s)
#define postcore_initcall(fn) 		__define_initcall("2",fn,2)
#define postcore_initcall_sync(fn) 	__define_initcall("2s",fn,2s)
#define arch_initcall(fn) 			__define_initcall("3",fn,3)
#define arch_initcall_sync(fn) 		__define_initcall("3s",fn,3s)
#define subsys_initcall(fn) 		__define_initcall("4",fn,4)
#define subsys_initcall_sync(fn) 	__define_initcall("4s",fn,4s)
#define fs_initcall(fn) 			__define_initcall("5",fn,5)
#define fs_initcall_sync(fn) 		__define_initcall("5s",fn,5s)
#define rootfs_initcall(fn)	 		__define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn) 		__define_initcall("6",fn,6)
#define device_initcall_sync(fn)	__define_initcall("6s",fn,6s)
#define late_initcall(fn) 			__define_initcall("7",fn,7)
#define late_initcall_sync(fn)		 __define_initcall("7s",fn,7s)

看下内核定义的,具体的每个驱动的启动次序可以从system.map看出,特别对于同一个优先级的各类驱动:

c003288ct 	__initcall_i2c_init2
c00328b0t	__initcall_video_early_init3
c00328b4t	__initcall_video2_early_init3
c00328b8t 	__initcall_aml_i2c_init3
c0032c18t 	__initcall_i2c_dev_init6
c0032c28t	__initcall_videodev_init6
c0032c30t	 __initcall_v4l2_i2c_drv_init6
c0032c34t 	__initcall_v4l2_i2c_drv_init6
c0032d24t	__initcall_video_init6
c0032d28t	__initcall_video2_init6
所以,static   initcall_t     __initcall_lcd_init6    __used  __attribute__((__section__(".initcall6.init"))) = lcd_init;
最终会变出来一个,__initcall_lcd_init6函数指针变量,这个指针指向lcd_init函数,现在是不是特别清楚了,module_init到底是干嘛的,就是向内核注册提交,
保存一个驱动的,指针变量嘛,当我们open的时候,就可以访问到这个驱动了。

前面我们说了Kernel通过调用do_initcalls(void)加载各个模块,具体流程如下图:根据上面设置,发现驱动是6,因此驱动模块在Kernel启动过程中的启动次序是非常靠后的。

static void__init do_initcalls(void)
{

	initcall_t*fn;
	for (fn =__early_initcall_end; fn < __initcall_end; fn++)
	do_one_initcall(*fn);
	/* Makesure there is no pending stuff from the initcall sequence */
	flush_scheduled_work();
}

对于同一级别的 __initcall的次序 ,主要由Makefile中,.o文件的链接次序决定,具体看Kernel下的主Makefile ---- Build vmlinux,以及kernel/driver 下的obj-y的顺序。

看下上面的这个for循环怎么回事,for (fn =__early_initcall_end; fn < __initcall_end; fn++)
就是轮询指针嘛,这个指针就是一个地址,比如,0x0001,0x0002,他们分别绑定了不同的驱动函数。这样就可以把所有驱动加载到内核里。do_one_initcall(*fn),这个函数就是在执行驱动init函数,比如lcd_init函数。

我们继续分析下,他们是怎么回事,for循环,是怎么回事。

initcall_t 类型的数组
__initcallx_start数组,初始化函数是通过一系列不同组别的initcall_t类型的数组组成的。如下:

extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];

这些个数组,不就是前面我们讲到的,轮询指针,各个数组存放了各个函数指针,数组是设置好了级别的,所一,内核启动时,按级别,一级一级的去启动。如下

c003288ct 	__initcall_i2c_init2
c0032d24t	__initcall_video_init6

不就是前面我们讲的这些,函数指针变量嘛,类型就是initcall_t ,这样就可以放入到__initcall_end数组里,前面for循环,就可以一个个函数执行了。

接续分析,initcall_levels,哎,是不是很熟悉啊,初始化调用链表来管理的,这个链表是一个全局变量数组,称为__initcall_levles。这个变量定义在init/main.c文件中。
数组元素是上面提到的__initcallx_start数组。上面各个级别的数组的地址,又放入到了这个数组指针,initcall_levels

static initcall_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

编译器向数组填入初始化函数指针,初始化函数被添加到初始化调用链表的过程是在内核编译阶段完成的。
这些初始化函数的地址是在编译期间通过特定的代码生成工具(如scripts/link-vmlinux.sh)生成的。这些工具会扫描内核源代码中对__define_initcall宏的调用,并将生成的初始化函数地址放入对应的数组中。
因此,__initcall_start[]等数组的值是由编译过程中的代码生成工具填入的。在Linux中,这些数组存储了所有需要在内核初始化过程中调用的初始化函数的地址。

最开始,我们就讲了调用过程,这些宏定义的初始化函数会在运行时被调用,完成相应的初始化工作。
在init/main.c文件中,有一个函数do_initcalls(),它会遍历初始化调用链表,并按照优先级依次调用其中的初始化函数。

这个函数的定义如下:

static void __init do_initcalls(void)
{
	int level;
	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
		do_initcall_level(level);
}
static void __init do_initcall_level(int level)
{
	extern const struct kernel_param __start___param[], __stop___param[];
	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);
}
int __init_or_module do_one_initcall(initcall_t fn)
{
	ret = fn();	
}

do_initcalls()函数会遍历链表中的每个元素,检查初始化函数是否存在并且没有被列入黑名单。然后,它会调用do_one_initcall()函数来执行初始化函数。这个函数会将初始化函数作为参数传递,并执行它。

总结:
发现没有,驱动是按级别顺序执行的,所以某些驱动,需要依赖其他的驱动,一定要先启动别的驱动,放在Makefile的后面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值