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

news2024/11/24 5:28:31

目录

  • 1、module_init宏
    • 1.1 展开
    • 1.2 解释以下几个标识
      • 1.2.1 fn
      • 1.2.2 id
      • 1.2.3 类型 initcall_t :
      • 1.2.4 __used
      • 1.2.5 __init
      • 1.2.6 __attribute__
    • 1.3 实例说明
  • 2、 驱动启动机制
    • 2.1 initcall_t 类型的数组
      • 2.2.1 __initcallx_start数组
      • 2.2.2 initcall_levels[]数组
    • 2.3 编译器向数组填入初始化函数指针
  • 3、总结

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()函数会遍历链表,并按照优先级依次调用其中的初始化函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/685385.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

每日一练 | 华为认证真题练习Day64

1、如下图所示的网络&#xff0c;所有路由器运行0SPF协议&#xff0c;链路上方为Cost值的大小&#xff0c;则RA路由表中到达网络10.0.0.0/8的Cost值是多少&#xff1f; A. 70 B. 20 C. 60 D. 100 2、如下图所示的网络&#xff0c;主机A没有配置网关&#xff0c;主机B存在网关…

基于GEC6818 Qt智能病房监控系统

文章目录 一、项目设备及平台二、项目功能说明1. 整体功能2. GEC6818开发板功能介绍3. GY39模块功能介绍4. MQ-2型烟雾传感器功能介绍5. RFID模块 三、硬件系统设计实现与图表四、软件系统设计实现与流程图1. 软件系统设计总体描述2. 软件实现流程图3. 操作过程 五、调试过程中…

基于电容电流前馈与电网电压全前馈的单相LCL并网逆变器谐波抑制MATLAB仿真(电压比例反馈及一二次微分反馈)

基于电容电流前馈与电网电压全前馈的单相LCL并网逆变器谐波抑制MATLAB仿真&#xff08;电压比例反馈及一二次微分反馈&#xff09;资源-CSDN文库https://download.csdn.net/download/weixin_56691527/87941037模型简介&#xff1a; 参考阮新波教授【LCL型并网逆变器的控制技术…

scrapy的数据保存到数据库

将数据保存到数据库 mysql数据库 下载链接数据库的依赖 Conda/pip install pymysql在piplines.py 文件中 重写open_spider方法 ​ 连接到mysql数据库 def open_spider(self, spider):self.conn pymysql.Connect(hostlocalhost,port3306,userroot,password20020115,dbscrap…

go系列-读取文件

1 概述 2 整个文件读入内存 直接将数据直接读取入内存&#xff0c;是效率最高的一种方式&#xff0c;但此种方式&#xff0c;仅适用于小文件&#xff0c;对于大文件&#xff0c;则不适合&#xff0c;因为比较浪费内存。 2.1 直接指定文化名读取 在 Go 1.16 开始&#xff0c;i…

chatgpt赋能python:Python编程语言制作的著名游戏

Python编程语言制作的著名游戏 Python是一种广泛使用的编程语言&#xff0c;其简单易读的语法让其成为许多游戏开发者的首选。本文将介绍利用Python编程语言制作的几个著名游戏&#xff0c;并将着重标记加粗它们的标题&#xff0c;以便于读者更容易了解。 1. 游戏&#xff1a…

【Python】python入门篇

概述 官网 https://www.python.org/ Python 是一种脚本语言&#xff08;scripting language&#xff09;。 与编译型语言&#xff08;如 C 和 C&#xff09;不同&#xff0c;Python 的程序代码不需要进行显式的编译&#xff0c;在执行时会动态地解释执行代码。 Python 的脚本执…

FFmpeg 解码 AAC 格式的音频

FFmpeg 默认是可以解码 AAC 格式的音频&#xff0c;但是如果需要获取 PCM16 此类数据则需要经过音频转码。首先要打开解码器&#xff0c;然后向解码器发送 AAC 音频帧&#xff08;不带 ADTS&#xff09;&#xff0c;然后从解码器获取解码后的音频帧&#xff0c;数据是 float 类…

【软考网络管理员】2023年软考网管初级常见知识考点(12)-应用层协议

涉及知识点 应用层协议详解&#xff0c;DNS的概念&#xff0c;FTP的概念&#xff0c;DHCP的概念&#xff0c;Telnet的概念&#xff0c;电子邮件协议 软考网络管理员常考知识点&#xff0c;软考网络管理员网络安全&#xff0c;网络管理员考点汇总。 原创于&#xff1a;CSDN博主…

我的内网渗透-代理转发(2)

目录 ssh telnet与SSH的区别 安装环境 常用参数 本地端口转发 远程端口转发 动态端口转发 Socks 使用方法 msf端口转发 常用参数 使用方法 创建监听 MSF读取文件命令&#xff08;开启msf的时候直接读取文件就自动设置好监听的各种配置&#xff09; 获取会话后 …

NCI Core Control Messages

NCI 版本参数应编码为 8 位字段&#xff0c;由两个 4 位无符号值组成&#xff0c;表示本规范的主要和次要版本级别。 最高有效 4 位应表示主要版本级别。 最低有效 4 位应表示本规范的次要版本级别。 如果 DH 支持 NFCC 报告的主要版本&#xff0c;则 DH 应继续通信&#xff0…

同比增长超300%,「手势识别」前装赛道借势多模态座舱交互

在座舱多模态交互系统中&#xff0c;手势识别功能正在成为主流的配置之一。高工智能汽车研究院监测数据显示&#xff0c;2022年中国市场&#xff08;不含进出口&#xff09;乘用车前装标配手势识别功能交付37.39万辆&#xff1b;今年1-4月交付23.90万辆&#xff0c;同比增长超过…

Linux下的free、uname、uptime、netstat、dmesg指令

文章目录 1 查看内存的使用情况&#xff1a;free2 查看系统与内核相关信息&#xff1a;uname3 查看系统运行时间和负载&#xff08;uptime&#xff09;4 查看端口监听&#xff1a;netstat5 分析内核产生的信息&#xff1a;dmesg 1 查看内存的使用情况&#xff1a;free free -m…

C++进阶—二叉搜索树

目录 0. 前言 1. 二叉搜索树概念 2. 二叉搜索树操作 3. 二叉搜索树的实现 3.1 非递归实现插入操作Insert 3.2 二叉搜索树中序遍历递归实现&#xff08;排序&#xff09; 3.3 非递归实现查找操作Find 3.4 非递归实现删除操作Erase 3.5 递归实现插入操作InsertR 3.5 递…

c++读取文件之---yaml-cpp使用

实际项目总会遇到有很多超参数的情况&#xff0c;用常规的结构体等无法有效的涵盖所有&#xff0c;为了方便用户进行配置使用&#xff0c;因此使用yaml的方式进行编辑配置&#xff0c;因此去调研使用了yaml-cpp的使用方法。 1、yaml-cpp下载和编译 下载方式很简单&#xff0c…

openfeign实现远程调用

一 openfeign简介 Feign 是声明性(注解)web 服务客户端它使编写 web 服务客户端更加容易请创建一个接口并对其进行注解.它具有可插入注解支持&#xff0c;包括Feign注解和JAXRS注解Feign 还支持可插拔编码器和解码器。Spring cloud 添加了对Spring MVC注解的支持&#xff0c;并…

chatgpt赋能python:Python编译成SO文件和反编译的介绍

Python编译成SO文件和反编译的介绍 什么是SO文件&#xff1f; SO文件&#xff0c;也称为共享对象文件&#xff0c;是一种二进制文件格式&#xff0c;用于在多个应用程序之间共享代码和数据。在Unix和类Unix系统中&#xff0c;它们通常是共享库文件的形式。因此&#xff0c;SO…

chatgpt赋能python:Python编译成可执行文件:让你的代码更加优雅高效

Python编译成可执行文件&#xff1a;让你的代码更加优雅高效 Python作为世界上最受欢迎的编程语言之一&#xff0c;拥有着丰富的库、面向对象的语法和简单易懂的语法结构。然而&#xff0c;在开发Python应用程序时&#xff0c;受限于Python的解释性&#xff0c;导致程序的效率…

Qt实现自定义控件能够以插件的方式加载到Qt设计师

目录 1、自定义部件/控件2、改进法3、插件法3.1、创建工程3.2、工程目录3.3、修改插件类的代码3.3.1、HexSpinBox类的头文件3.3.2、HexSpinBox类的源文件3.3.3、HexSpinBox类的UI文件3.3.4 需要的注意的事项 3.4、生成动态库 4、测试插件能否正常使用4.1、测试Qt设计师能否识别…

认识@Validated 和 @Valid

对于web应用来说&#xff0c;对方法参数的校验是十分重要的&#xff0c;参数校验的是否全面&#xff0c;直接决定整个方法的健壮性。 除了使用麻烦的if判断校验参数&#xff0c;还可以使用Validated 和 Valid注解来进行优雅地参数校验&#xff0c;让参数校验和写诗一样优雅。 …