Linux中驱动模块加载方法分析
如何管理驱动模块
由于Linux驱动模块众多,系统对模块加载顺序有要求,一些基础模块在系统启动时需要很早就被加载;开发者加入自己的模块时,需要维护一个模块初始化列表,上面两方面的做起来很困难,为了科学地管理这些模块,首先要解决两个问题:
- 如何方便开发者快捷加入自己的模块
- 如何管理模块的加载顺序
Linux 内核开发者是怎么实现的呢?在内核镜像文件中,自定义了一个段,这个段里面专门用来存放这些初始化函数的地址,内核启动时,只需要在这个段地址处取出函数指针,依次执行即可。
对模块的开发者,Linux内核提供了统一的宏定义接口,驱动开发者只需要将驱动程序用这些宏定义来修饰,这个模块的初始化函数接口的指针就被自动添加到了上述的段中,开发者完全不需要关心这个实现的细节。
对于各种各样的驱动而言,会存在一定的依赖关系,需要遵循先后顺序来进行初始化,考虑到这个问题,Linux内核开发者也对这一部分的初始化顺序做了分级处理。
Linux驱动模块的加载方式
Linux 驱动模块有两种加载方式,一是静态编译链接进内核,在系统启动过程中进行初始化;另外一是编译成可动态加载的module,通过insmod动态加载重定位到内核。
Linux 使用宏定义
Linux提供了一组宏定义对模块进行静态和动态加载,同时对不同的模块加载顺序做了处理,提供了不同的宏定义方法。这组宏定义在路径/kernel/include/linux/init.h中。
#ifndef MODULE#define __define_initcall(level,fn,id) \static initcall_t __initcall_##fn##id __used \__attribute__((__section__(".initcall" level ".init"))) = fn#define early_initcall(fn) __define_initcall("early",fn,early)
#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)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x);#else /* MODULE *//* Don't use these in modules, but some people do... */
#define early_initcall(fn) module_init(fn)
#define core_initcall(fn) module_init(fn)
#define postcore_initcall(fn) module_init(fn)
#define arch_initcall(fn) module_init(fn)
#define subsys_initcall(fn) module_init(fn)
#define fs_initcall(fn) module_init(fn)
#define device_initcall(fn) module_init(fn)
#define late_initcall(fn) module_init(fn)
#define security_initcall(fn) module_init(fn)/* Each module must use one module_init(). */
#define module_init(initfn) \static inline initcall_t __inittest(void) \{ return initfn; } \int init_module(void) __attribute__((alias(#initfn)));/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \static inline exitcall_t __exittest(void) \{ return exitfn; } \void cleanup_module(void) __attribute__((alias(#exitfn)));
#endif /*end ifndef MODULE*/
Makefile是如何控制模块的加载模式的?在init.h中可以看到下面的宏定义:
#ifndef MODULE
// 宏定义--->(静态加载方式)
#else
// 宏定义--->(动态加载方式)
#endif
当配置Makefiel时,将某个module配置为obj-m时,MODULE 这个宏就被定义,此时当前模块就被编译到内核代码中,内核启动时这个模块就被静态加载,反之,模块配置为obj-y,当前模块被配置为动态加载方式。
宏定义 __define_initcall 分析
我们看看如何解析这个宏__define_initcall:
#define __define_initcall(level,fn,id) \static initcall_t __initcall_##fn##id __used \__attribute__((__section__(".initcall" level ".init"))) = fn
_ * attribute * _ () 是gnu C中的扩展语法,它可以用来实现很多灵活的定义行为。
_ * attribute * _ ((_ * section * _ (“.initcall” #id “.init”)))表示编译时将目标符号放置在括号指定的段中。
在宏定义中,# 的作用是将目标字符串化,## 在宏定义中的作用是符号连接,将多个符号连接成一个符号,并不字符串化。
level是一个数字或者是数字+s,这个数字代表这个fn执行的优先级,数字越小,优先级越高,带s的fn优先级低于不带s的fn优先级
__used是一个宏定义
#define __used __attribute__((__used__))
使用前提是在编译器编译过程中,如果定义的符号没有被引用,编译器就会对其进行优化,不保留这个符号,而__attribute__((used))的作用是告诉编译器这个静态符号在编译的时候即使没有使用到也要保留这个符号。
这里的 initcall_t 是函数指针类型,对应的段:.initcall,如下:
typedef int (*initcall_t)(void);
上面所述,这个宏将我们的初始化函数放在".initcall" level ".init"中。这个段可以在Vmlinux.lds.h里面找到,如下:
#define INITCALLS \*(.initcall0.init) \*(.initcall0s.init) \*(.initcall1.init) \*(.initcall1s.init) \*(.initcall2.init) \*(.initcall2s.init) \*(.initcall3.init) \*(.initcall3s.init) \*(.initcall4.init) \*(.initcall4s.init) \*(.initcall5.init) \*(.initcall5s.init) \*(.initcallrootfs.init) \*(.initcall6.init) \*(.initcall6s.init) \*(.initcall7.init) \*(.initcall7s.init)
INITCALL 可以在vmlinux.lds.S里面找到:
.init.text : AT(ADDR(.init.text) - LOAD_OFFSET) {__init_begin = .;_sinittext = .;*(.init.text)_einittext = .;}.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { *(.init.data) }. = ALIGN(16);.init.setup : AT(ADDR(.init.setup) - LOAD_OFFSET) {__setup_start = .;*(.init.setup)__setup_end = .;}.initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {__initcall_start = .;INITCALLS //这里__initcall_end = .;}.con_initcall.init : AT(ADDR(.con_initcall.init) - LOAD_OFFSET) {__con_initcall_start = .;*(.con_initcall.init)__con_initcall_end = .;}
Vmlinux.lds.h 中是系统启动时存放初始化数据的指针,执行完成后会被释放掉内存。根据上面的内存布局,可以列出初始化宏和 内存的对应关系:
_init_begin |-------------------|| .init.text | ---- __init|-------------------|| .init.data | ---- __initdata
_setup_start |-------------------|| .init.setup | ---- __setup_param
__initcall_start |-------------------|| .initcall1.init | ---- core_initcall|-------------------|| .initcall2.init | ---- postcore_initcall|-------------------|| .initcall3.init | ---- arch_initcall|-------------------|| .initcall4.init | ---- subsys_initcall|-------------------|| .initcall5.init | ---- fs_initcall|-------------------|| .initcall6.init | ---- device_initcall|-------------------|| .initcall7.init | ---- late_initcall
__initcall_end |-------------------|| || ... ... ... || |
__init_end |-------------------|
宏定义 #define module_init(initfn)
#define module_init(initfn) \static inline initcall_t __inittest(void) \{ return initfn; } \int init_module(void) __attribute__((alias(#initfn)));
前两句话只是做了一个检测,当传进来的函数指针的参数和返回值与initcall_t不一致时,就会有告警。第三句,是使用alias将initfn变名为init_module,当调用insmod将module加载进内核时,就会去找init_module作为入口地址,即传进来的initfn, 这样module就被加载了。
举例:
为了更方便地理解,我们举个例子来说明,开发者声明了这样一个函数:module_init(hello_init);
首先宏展开成:__define_initcall(“6”,hello_init, 6)
然后接着展开:static initcall_t __initcall_hello_init6 = hello_init; 定义了函数指针变量。
同时声明 __initcall_hello_init6 这个变量即使没被引用也保留符号,且将其放置在内核镜像的.initcall6.init段处。
xxx_initcall()宏定义调用追踪
从上面的分析,我们知道xxx_initcall是如何被定义,知道目标函数的放置位置,那么使用xxx_initcall()修饰的函数是怎么被调用的呢?下面就从内核 init/main.c函数起始部分start_kernel开始往下追踪,它的调用顺序为:
start_kernel -> rest_init();-> kernel_thread(kernel_init, NULL, CLONE_FS);-> kernel_init()-> do_basic_setup();-> do_initcalls();
rest_init();启动 RCU 锁调度器 ,调用函数 kernel_thread 创建 kernel_init 进程,也就是 init 内核进程, init 进程的 PID 为 1。调用函数 kernel_thread 创建 kthreadd 内核进程,此内核进程的 PID 为 2。kthreadd 进程负责所有内核进程的调度和管理。
do_initcalls() 在这个函数中执行所有使用xxx_initcall()声明的函数,完成 Linux 下驱动模型子系统的初始化。
static void __init do_initcalls(void)
{initcall_t *fn;for (fn = __early_initcall_end; fn < __initcall_end; fn++)do_one_initcall(*fn);
}
函数中的 fn 为函数指针,fn++ 相当于函数指针+1,相当于内存地址+sizeof(fn)。
int do_one_initcall(initcall_t fn)
{ret.result = fn();//执行功能函数
}
在do_one_initcall函数里执行被初始化的模块。
相关文章:

Linux中驱动模块加载方法分析
如何管理驱动模块 由于Linux驱动模块众多,系统对模块加载顺序有要求,一些基础模块在系统启动时需要很早就被加载;开发者加入自己的模块时,需要维护一个模块初始化列表,上面两方面的做起来很困难,为了科学地…...

yarn 通过 resolutions,指定子孙依赖包版本号,解决froala-editor 版本问题
前端开发项目过程中会使用到各种依赖包。但是这些依赖包虽然好用,但是一味使用最新版本可能会出现各种奇葩问题,因此我们经常会针对一些依赖包指定一个稳定版本。 常用版本 版本号注释“1.0.2”必须切到1.0.2版“>1.0.2”必须大于1.0.2版“>1.0.…...

Elasticsearch7.8.0版本进阶——多文档操作流程
目录一、多文档操作1.1、多文档操作的概述1.2、多文档操作与单文档模式区别二、用单个 mget 请求取回多个文档2.1、用单个 mget 请求取回多个文档的图解2.2、用单个 mget 请求取回多个文档的步骤三、bulk API 的模式请求取回多个文档3.1、bulk API 的模式请求取回多个文档的图解…...

Scala函数式编程(第五章:函数基础、函数高级详解)
文章目录第 5 章 函数式编程5.1 函数基础5.1.1 函数基本语法5.1.2 函数和方法的区别5.1.3 函数定义5.1.4 函数参数5.1.5 函数至简原则(重点)5.2 函数高级5.2.1 高阶函数5.2.2 匿名函数5.2.3 高阶函数案例5.2.4 函数柯里化&闭包5.2.5 递归5.2.6 控制抽…...

ZED相机快速使用指南
1、安装SDK ZED SDK 3.8 - Download | Stereolabs 2、安装ros GitHub - stereolabs/zed-ros-wrapper: ROS wrapper for the ZED SDK 其他教程:ZED2相机SDK安装使用及ROS下使用_可即的博客-CSDN博客 3、官方文档 Get Started with ZED | Stereolabs 4、标定参…...

树莓派4b配置OpenWrt联网
文章目录前言一、下载固件二、配置wan口三、简单介绍1、修改无线名称、设置密码2、下载软件包总结前言 树莓派4b内置wifi模块,加一个千兆网口 好像有一种办法,通过无线wifi链接其他wifi通网,然后把这个网口作为lan口,连接电脑使…...

不同语言下的定时器,你都掌握了吗?
我们大家都对定时器不陌生,无论是现实中还是项目中,都离不开定时。在现实中,它叫闹钟,在项目上,它叫定时器,即定时触发某件事情。它能帮助我们在某一个既定的时间节点上,来提醒我们做一些事情&a…...

华为OD机试 - 水仙花数(Python) | 机试题+算法思路+考点+代码解析 【2023】
水仙花数 题目 所谓的水仙花数是指一个n位的正整数其各位数字的n次方的和等于该数本身, 例如153 = 1^3 + 5^3 + 3^3,153是一个三位数 输入 第一行输入一个整数N, 表示 N 位的正整数 N 在3-7之间包含3,7 第二行输入一个正整数M, 表示需要返回第M个水仙花数 输出描述 返…...

在onBindViewHolder设置View的translation失败或错乱的问题
这个问题,可以换成“为什么在onCreate里面修改一些子View不生效,错位,乱”等问题。 本质原因肯定是在没有把整个ViewGroup渲染完成之前,操作了部分子View,导致了位置偏移等。 解决办法也很简单,通过调用Vi…...

【2.21】MySQL索引、动态规划、学习方法
索引常见面试题 什么是索引 索引的定义就是帮助存储引擎快速获取数据的一种数据结构,形象的说就是索引是数据的目录。存储引擎,说白了就是如何存储数据、如何为存储的数据建立索引和如何更新、查询数据等技术的实现方法。索引和数据就是位于存储引擎。…...

华为OD机试题 - 二叉树层次遍历(JavaScript)| 包含代码编写思路
最近更新的博客 华为OD机试题 - 字符串加密(JavaScript) 华为OD机试题 - 字母消消乐(JavaScript) 华为OD机试题 - 字母计数(JavaScript) 华为OD机试题 - 整数分解(JavaScript) 华为OD机试题 - 单词反转(JavaScript) 华为OD机试题 最近更新的博客使用说明二叉树层次遍…...

力扣解法汇总1140. 石子游戏 II
目录链接: 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目: https://github.com/September26/java-algorithms 原题链接:力扣 描述: 爱丽丝和鲍勃继续他们的石子游戏。许多堆石子 排成一行,每堆都有正整…...

Kerberos认证原理与使用教程
Kerberos认证原理与使用教程 一、Kerberos 概述 二、什么是 Kerberos Kerberos 是一种计算机网络认证协议,用来在非安全网络中,对个人通信以安全的手段进行身份认证。这个词又指麻省理工学院为这个协议开发的一套计算机软件。软件设计上采用客户端…...
内存取证常见例题思路方法-volatility (没有最全 只有更全)
目录 1.从内存文件中获取到用户hacker 的密码并且破解密码,将破解后的密码作为 Flag值提交; 2.获取当前系统的主机名,将主机名作为Flag值提交; 3.获取当前系统浏览器搜索过的关键词,作为Flag提交; 4.获取当前内存文件的 ip地址 5.当前系…...

10 种主数据模型设计示例分享,推荐收藏
主数据模型是主数据管理的基础,一个完整的、可扩展的、相对稳定的主数据模型对于主数据管理的成功起着重要的作用。规划、创建主数据模型的过程,是梳理主数据管理体系的过程,目的是建立一个良好的资源目录结构,划分合理的资源粒度…...

React学习笔记
React学习笔记 概述 React是用于构建用户界面的JavaScript库。 现在前端领域最为流行的三大框架: VueReactAngular 其中,Vue和React是国内最为流行的两个框架。 React的特点: 1、声明式编程:它允许我们只需要维护自己的状态…...

【Vue源码解析】Vue虚拟dom和diff算法
Vue虚拟dom和diff算法1. 简介2. 搭建环境1. 安装snabbdom2. 安装webpack5并配置3、函数3.1 虚拟节点vnode的属性3.2 使用h函数 创建虚拟节点3.3 使用patch函数 将虚拟节点上DOM树3.4 h函数嵌套使用,得到虚拟DOM树(重要)3.5 patchVnode函数3.6…...

算法学习与填充计划---2023.2.21---夏目
🚀write in front🚀 📝个人主页:认真写博客的夏目浅石.CSDN 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝 📣系列专栏:ACM周训练题目合集.CSDN 💬总结:…...

JavaScript中怎么实现链表?
JavaScript中怎么实现链表? 学习数据结构的的链表和树时,会遇到节点(node)这个词,节点是处理数据结构的链表和树的基础。节点是一种数据元素,包括两个部分:一个是实际需要用到的数据;…...

多孔弹性材料中传播的膨胀波方法(Matlab代码实现)
👨🎓个人主页:研学社的博客💥💥💞💞欢迎来到本博客❤️❤️💥💥🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密…...

时间复杂度与空间复杂度
目录一、算法的复杂度二、时间复杂度2.1 什么叫时间复杂度2.2 大O的渐进表示法2.3 计算时间复杂度的练习三、空间复杂度四、常见复杂度的对比一、算法的复杂度 算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏…...

UDP报文详解
目录 🐳今日良言:走好选择的路,别选择好走的路,你才能拥有真正的自己。 🐼一、UDP协议特点 🐼二、UDP协议段格式详解 🐳今日良言:走好选择的路,别选择好走的路,你才能拥有真正的自…...

C#开发的OpenRA的NextPowerOf2
C#开发的OpenRA的NextPowerOf2 在游戏里,经常需要对计算资源进行优化。 比如屏幕的大小,以及缓冲区的大小,还有纹理的大小。 由于计算机都是基于二进制的原理,那么它的最快计算速度,就是让计算的数字都是2的n次方。 基于此策略,在程序里就需要计算出来最接近2的n次方的数…...

CDH 6.3.2启用HDFS高可用
启用原因 CDH 6.3.2平台即将用于生产,生产平台几乎需要高可用平台,故需要升级CDH中的HDFS为HA。 启用准备 CDH已经成功安装并正常使用CMS的管理员账号正常登陆 HDFS启用HA 登陆CMS系统->选择HDFS服务->点击进入到HDFS服务详情页面,…...

多服务器节点访问解决一人一单问题+redis设置锁方案
项目地址及项目具体介绍-码云仓库:https://gitee.com/flowers-bloom-is-the-sea/distributeNodeSolvePessimisticLockByRedis 测试1: 这里使用jmeter同时启动2各线程: 原来的数据库表的数据: goods的数据是: id …...

tensorflow 学习笔记(三):神经网络八股
本节内容: 前两节使用 Tensorflow2 的原生代码大叫神经网络。本节使用 keras 搭建神经网络(八股:六步法,有 Sequential 和 class 两种)。 文章目录一、搭建网络八股 sequential1.1、keras 介绍1.2、六步法搭建 keras …...

华为OD机试真题Python实现【射击比赛】真题+解题思路+代码(20222023)
射击比赛 题目 给定一个射击比赛成绩单 包含多个选手若干次射击的成绩分数 请对每个选手按其最高三个分数之和进行降序排名 输出降序排名后的选手 ID 序列 条件如下: 一个选手可以有多个射击成绩的分数 且次序不固定如果一个选手成绩小于三个 则认为选手的所有成绩无效 排名忽…...

【YBT2023寒假Day12 C】树的计数 II(prufer)(结论)(数学)
树的计数 II 题目链接:YBT2023寒假Day12 C 题目大意 给你一个长度为 n 的排列 p,问你有多少个不同的有标号无根树,满足如果 i,j 有边那 pi,pj 也有边。 思路 首先可以把排列变成置换环。 注意到是树,发现一个置换中似乎不太可…...

深入浅出C++ ——多态
文章目录一、多态的概念二、多态的定义及实现1. 多态的构成条件2. 虚函数3. 虚函数的重写4. virtual的使用:5. 虚函数重写的两个例外:6. C11 override 和 final7. 重载、重写、重定义的对比三、抽象类四、多态的原理1. 虚函数表2. 多态的原理3. 静态绑定…...

华为OD机试真题Python实现【整数编码】真题+解题思路+代码(20222023)
整数编码 题目 实现一个整数编码方法 使得待编码的数字越小 编码后所占用的字节数越小 编码规则如下 编码时7位一组,每个字节的低 7 位用于存储待编码数字的补码字节的最高位表示后续是否还有字节,置1表示后面还有更多的字节,置0表示当前字节为最后一个字节采用小端序编码…...