【嵌入式环境下linux内核及驱动学习笔记-(17)内核驱动模块的启动机制】

1、module_init宏

头文件在include/linux/init.h

在一个驱动模块中,常会写下如下的代码:

int __init lcd_init(void)
{
	/*做一些工作*/
	return 0;
}


module_init(lcd_init);

常说用module_init()宏来标识初始化函数lcd_init(),函数lcd_init是这个驱动模块的一个入口。这里就来解释,这个入口是如何实现的。

1.1 展开

首先,把宏module_init()一层层展开,来看这个宏到底做了什么。

#define module_init(x) __initcall(x);

参数:

x:在内核启动时或模块插入时运行的函数

*module_init()将在do_initcalls()期间(如果是内置的)或在模块插入时(如果是模块)调用。每个模块只能有一个。

这个宏是如何在内核初始化时被调用的,或是在模块被插入时(insmod命令)被调用的。带着这个疑问,把宏展开:

#define module_init(x) __initcall(x);
->
\qquad \qquad \qquad #define __initcall(fn) device_initcall(fn)
->
\qquad \qquad \qquad \qquad \qquad \qquad #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

1.2 解释以下几个标识

1.2.1 fn

\qquad fn是初始化函数的名称

1.2.2 id

\qquad id是一个整数值,用于标识初始化函数所属的组别。

1.2.3 类型 initcall_t :

typedef int (*initcall_t)(void);
这是一个函数指针类型,这个指针指向的函数型如: int fun(void);

1.2.4 __used

__used宏在Linux内核中的定义如下:


#ifdef __GNUC__
#define __used                          __attribute__((__used__))
#else
#define __used
#endif

\qquad 在这个定义中,首先通过#ifdef GNUC__判断是否使用的是GNU C编译器(gcc)。如果是,则使用__attribute((used))来标记变量或函数为“已使用”。
\qquad 这个特殊的属性告诉编译器和链接器,这些标记为未使用的代码或数据是有意的,不应该被删除或警告。
如果不是使用的GNU C编译器,则直接定义__used为空,即不做任何操作。

1、防止被一些编译器或工具误删:有时候,编译器或其他代码优化工具会删除被认为未使用的代码或数据,这可能导致一些意外的行为或错误。通过使用__used宏,可以确保这些代码或数据不会被误删。

2、用于强制链接:在某些情况下,可能需要强制链接某些未使用的模块或函数。通过使用__used宏,可以告诉链接器,这些标记为未使用的代码或数据也需要被链接进最终的可执行文件或模块中。


\qquad 需要注意的是,使用__used宏标记的代码或数据应该是确切知道未使用的,否则可能会导致不必要的内存消耗或其他潜在问题。
总而言之,__used宏用于在特定情况下标记某些代码或数据为有意的未使用,以防止编译器或链接器对其进行删除或警告。

1.2.5 __init

头文件 include / linux/init.h
宏定义
#define __init __section(.init.text) __cold notrace

\qquad 这段宏定义用于在Linux内核中标记函数为初始化函数。让我们逐个解释这个宏的各个部分:

1. 功能上__init这是一个宏,用于告诉编译器将函数放置在特定的代码段中。在内核中,.init.text是一个特殊的代码段,用于存放初始化函数。通过使用这个宏,可以将函数放置在这个代码段中。

2. __section(.init.text): 用于将函数放置在.init.text代码段中。.init.text代码段在内核启动时会被加载和执行。

3. __cold: 这是一个函数属性,用于告诉编译器这个函数很少被执行,因此可以进行一些优化。__cold属性通常用于初始化函数,因为这些函数在内核启动后只会被执行一次。

4. notrace: 这是一个函数属性,用于告诉编译器不要在这个函数中插入任何跟踪代码。跟踪代码通常用于调试和性能分析,但对于初始化函数来说,通常不需要进行跟踪。


\qquad 综上所述,这个宏定义的作用是将函数标记为初始化函数,并将其放置在特定的代码段中。同时,通过使用__cold和notrace属性,可以告诉编译器进行一些优化,并避免在这个函数中插入跟踪代码。

1.2.6 attribute

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

1.3 实例说明

如果我们用一个实例来说明,比较清楚的知道展开后是什么样的


module_init(lcd_init);


->  __define_initcall(lcd_init , 6);

展开后就成如下的样子

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


展开后的结果是定义了一个名为__initcall_lcd_init6的静态变量,该变量的类型是initcall_t(函数指针类型),并将其初始化为lcd_init函数的地址。这个变量被放置在名为.initcall6.init的代码段中。

2、 驱动启动机制

\qquad 因此,可以看到,在Linux内核中,module_init()宏用于定义内核模块初始化时需要调用的函数。该宏的展开后,会创建一个静态的初始化调用函数指针变量,这个变量用于保存需要在内核模块加载时执行的函数。

\qquad 这个函数指针变量的目的是为了在内核启动过程中的初始化阶段,通过调用这个函数进行模块的初始化工作。这个函数指针变量会被添加到内核的初始化调用链表中,并在适当的时候被调用。
实际上,当内核加载一个模块时,会遍历这个初始化调用链表,并按照函数指针的顺序依次调用这些初始化函数。

2.1 initcall_t 类型的数组

2.2.1 __initcallx_start数组

\qquad 在Linux3.14的内核中,初始化函数是通过一系列不同组别的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[];

数组名后带有数字分组标识。这正好与前面讲过的module_init()展开后也会生成一个带组别的变量就对应起来了,(截图说明):
在这里插入图片描述

2.2.2 initcall_levels[]数组

初始化调用链表来管理的。这个链表是一个全局变量数组,称为__initcall_levles。这个变量定义在init/main.c文件中。
数组元素是上面提到的__initcallx_start数组。


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

2.3 编译器向数组填入初始化函数指针

\qquad 初始化函数被添加到初始化调用链表的过程是在内核编译阶段完成的。

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

\qquad 这些宏定义的初始化函数会在运行时被调用,完成相应的初始化工作。
在init/main.c文件中,有一个函数do_initcalls(),它会遍历初始化调用链表,并按照优先级依次调用其中的初始化函数。
\qquad 这个函数的定义如下:

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)
{
	int count = preempt_count();
	int ret;
	char msgbuf[64];

	if (initcall_debug)
		ret = do_one_initcall_debug(fn);
	else
		ret = fn();

	msgbuf[0] = 0;

	if (preempt_count() != count) {
		sprintf(msgbuf, "preemption imbalance ");
		preempt_count_set(count);
	}
	if (irqs_disabled()) {
		strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
		local_irq_enable();
	}
	WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf);

	return ret;
}

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

3、总结

\qquad 总结起来,初始化调用链表是一个全局变量,用于管理初始化函数。链表的元素是initcall_t类型的结构体,包含初始化函数的指针和优先级值。在init/main.c文件中的do_initcalls()函数会遍历链表,并按照优先级依次调用其中的初始化函数。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
很高兴回答你关于野火嵌入Linux学习的问题!以下是一些学习笔记的建议: 1. 了解嵌入系统:首先,你需要了解嵌入系统是什么以及它们与桌面操作系统的区别。嵌入系统通常用于特定的应用领域,例如智能家居、汽车电子和工业控制等。 2. 学习Linux基础知识:野火嵌入Linux是基于Linux内核的操作系统,所以你需要掌握Linux的基础知识,包括文件系统、进程管理、设备驱动程序等。 3. 硬件平台了解:野火嵌入Linux有不同的硬件平台,例如野火开发板。你需要学习如何操作和配置这些硬件平台,并了解它们的特性和限制。 4. 交叉编译环境设置:为了在PC上开发嵌入系统,你需要设置一个交叉编译环境,以便能够编译和调试嵌入应用程序。这涉及到安装和配置交叉编译工具链。 5. 内核定制和驱动程序开发:学习如何定制Linux内核以满足特定需求,并开发设备驱动程序以支持外部硬件。 6. 应用程序开发:掌握嵌入应用程序的开发技术,包括使用C/C++语言、Makefile和调试工具。 7. 调试和故障排除:学会使用调试工具和技术来定位和解决嵌入系统中的问题。 8. 实际项目经验:通过参与实际的嵌入项目或完成一些小型项目来应用你的知识和技能。 这些只是一些学习笔记的建议,野火嵌入Linux学习需要不断的实践和探索。希望这些对你有帮助!如果你有任何进一步的问题,欢迎继续提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

骑牛唱剧本

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值