linux驱动开发(1)-内核模块
内核模块
模块最大的好处是可以动态扩展应用程序的功能而无须重新编译链接生成新的应用程序镜像,在微软的Windows系统上动态链接库DLL(Dynamic Link Library),Linux系统上的共享库so(shared object)文件的形式都属于广义上的模块。内核模块可以在linux内核运行期间动态扩展内核功能而无须重新启动系统,更无须为这些新增的功能重新编译一个新的系统内核镜像。内核模块的这个特性为内核开发者提供了极大的便利,因为编译一个新内核并重新启动将花费大量的时间。这本质上是解耦和开闭原则的实践。insmod就是向系统动态加载内核模块的命令。
insmod demodev.ko
内核模块文件形式
以内核模块形式存在的驱动程序的数据组织形式是ELF (Executable and Linkable Format),内核模块是一种普通的可重定位目标文件。用file命令查看demodev.ko文件,可以得到类似如下的输出:
file demodev.ko
demodev.ko: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
ELF是Linux下非常重要的一种文件格式,常见的可执行程序都是以ELF的形式存在。在这里我们结合Linux源代码中定义的ELF相关数据结构(基于32位体系架构),给出ELF格式的一个结构图,如图所示:
静态的ELF文件视图总体上可分为三大部分:头部的ELF header,中间的Section和尾部的Section header table。在Linux环境下读取ELF文件信息的工具—readelf。
ELFheader部分大小是52字节,位于文件头部。对于驱动模块文件而言,其中一些比较重要的数据成员如下:e_type表明文件类型,对于驱动模块,这个值是1,也就是说驱动模块是一个可定位的ELF文件(relocatable file)。e_shoff表明Section header table部分在文件中的偏移量。e_shentsize表明Section header table部分中每一个entry的大小(以字节计)。e_shnum表明Section header table中有多少个entry。因此,Section header table的大小便为e_shentsize×e_shnum个字节。e_shstrndx与Section header entry中的sh_name一起用来指明对应的section的name。
Section部分ELF文件的主体,位于文件视图中间部分的一个连续区域中。但是当模块被内核加载时,会根据各自属性被重新分配到新的内存区域(有些section也可能只是起辅助作用,因而在运行时并不占用实际的内存空间)。
Section header table部分该部分位于文件视图的末尾,由若干个(具体个数由ELF header中的e_shnum变量指定) Section header entry组成,每个entry具有同样的数据结构类型。对于设备驱动模块而言,一些比较重要的数据成员如下:sh_addr这个值用来表示该entry所对应的section在内存中的实际地址。在静态的文件视图中,这个值为0,当模块被内核加载时,加载器会用该section在内存中的实际地址来改写sh_addr (如果section不占用内存空间,该值为0)。sh_offset表明对应的section在文件视图中的偏移量。sh_size表明对应的section在文件视图中的大小(以字节计)。类型为SHT_NOBITS的section例外,这种section在文件视图中不占有空间。sh_entsize主要用于由固定数量entry组成的表所构成的section,如符号表,此种情况下用来表示表中entry的大小。
EXPORT_SYMBOL的内核实现
源码中充斥着像EXPORT_SYMBOL这样的宏,它用来向外界导出一个符号。如果没有独立存在的内核模块,作为单一的Linux内核镜像,导出符号就失去了意义。对于静态编译链接而成的内核镜像而言,所有的符号引用都将在静态链接阶段完成。然而内核模块不可避免地要使用到内核提供的功能(以调用内核函数的形式发生),作为独立编译链接的内核模块,必须要解决这种静态链接无法完成的符号引用问题(在内核模块所在的ELF文件中,这种引用被称为“未解决的引用”)。处理“未解决引用”问题的本质是在模块加载期间找到当前“未解决的引用”符号在内存中的实际目标地址。内核和内核模块通过符号表的形式向外部世界导出符号的相关信息,这种导出符号的方式在代码层面以EXPORT_SYMBOL宏定义的形式存在。从全局来看,EXPORT_SYMBOL这类宏功能的完整实现需要经过三个部分来达成:EXPORT_SYMBOL宏定义部分,链接脚本链接器部分和使用导出符号部分。
<include/linux/module.h>
#define__EXPORT_SYMBOL(sym,sec) \extern typeof(sym)sym; \__CRC_SYMBOL(sym,sec) \static const char__kstrtab_##sym[] \__attribute__((section("__ksymtab_strings"),aligned(1)))\=MODULE_SYMBOL_PREFIX#sym; \static const struct kernel_symbol__ksymtab_##sym \__used \__attribute__((section("__ksymtab"sec),unused)) \={(unsigned long)&sym,__kstrtab_##sym}
#define EXPORT_SYMBOL(sym) \__EXPORT_SYMBOL(sym,"")
#define EXPORT_SYMBOL_GPL(sym) \__EXPORT_SYMBOL(sym,"_gpl")
#define EXPORT_SYMBOL_GPL_FUTURE(sym) \__EXPORT_SYMBOL(sym,"_gpl_future")
从源代码可以看出,每个EXPORT_SYMBOL宏实际上定义了两个变量:
static const char *__kstrtab_my_exp_function = "my_exp_function";//tab是table的缩写
static const struct kernel_symbol __ksymtab_my_exp_function =
{ (unsigned long)& my_exp_function, __kstrtab_my_exp_function };
第一个变量是char型指针,用来表示导出的符号名称;第二个变量类型是struct kernel_symbol数据结构,用来表示一个内核符号的实例,struct kernel_symbol的定义为:
<include/linux/module.h>
struct kernel_symbol
{unsigned long value;const char*name;
};
value是该符号在内存中的地址,name是符号名。所以,用EXPORT_SYMBOL(my_exp_function)来导出符号“my_exp_function”,实际上是要通过struct kernel_symbol的一个对象告诉外部世界关于这个符号的两点信息:符号名称和地址。由EXPORT_SYMBOL等宏导出的符号,与一般的变量定义并没有实质性的差异,唯一的不同点在于它们被放在了特定的section中。
上面的__kstrtab_my_exp_function(symbol的名称)会被放置在一个名为“__ksymtab_strings”的section中,__ksymtab_my_exp_function(symbol实例)会放置在一个名为“__ksymtab”的section中(对于EXPORT_SYMBOL_GPL和EXPORT_SYMBOL_GPL_FUTURE而言,其struct kernel_symbol实例所在的section名称则分别为“__ksymtab_gpl”和“__ksymtab_gpl_future”)。对这些section的使用需要经过一个中间环节,即链接脚本与链接器部分。链接脚本告诉链接器把所有目标文件中的名为“__ksymtab”的section放置在最终内核(或者是内核模块)镜像文件的名为“__ksymtab”的section中(对于目标文件中的名为“__ksymtab_gpl”、“__ksymtab_gpl_future”、“__kcrctab”、“__kcrctab_gpl”和“__kcrctab_gpl_future”的section都同样处理),看看下面的这个具体的链接脚本的例子就很清楚了。
<arch/x86/kernel/vmlinux.lds>
__ksymtab : AT(ADDR(__ksymtab) - 0xC0000000)
{ __start___ksymtab = .; *(__ksymtab) __stop___ksymtab = .; }
__ksymtab_gpl : AT(ADDR(__ksymtab_gpl) - 0xC0000000)
{ __start___ksymtab_gpl = .; *(__ksymtab_gpl) __stop___ksymtab_gpl = .; }
__ksymtab_gpl_future : AT(ADDR(__ksymtab_gpl_future) - 0xC0000000)
{
__start___ksymtab_gpl_future = .;
*(__ksymtab_gpl_future) __stop___ksymtab_gpl_future = .;
}
__kcrctab : AT(ADDR(__kcrctab) - 0xC0000000)
{ __start___kcrctab = .; *(__kcrctab) __stop___kcrctab = .; }
__kcrctab_gpl : AT(ADDR(__kcrctab_gpl) - 0xC0000000)
{ __start___kcrctab_gpl = .; *(__kcrctab_gpl) __stop___kcrctab_gpl = .; }
__kcrctab_gpl_future : AT(ADDR(__kcrctab_gpl_future) - 0xC0000000)
{ __start___kcrctab_gpl_future = .; *(__kcrctab_gpl_future) __stop___kcrctab_gpl_future = .; }
__ksymtab_strings : AT(ADDR(__ksymtab_strings) - 0xC0000000)
{ *(__ksymtab_strings) }
这里之所以要把所有向外界导出的符号统一放到一个特殊的section里面,是为了在加载其他模块时用来处理那些“未解决的引用”符号。注意这里由链接脚本定义的几个变量__start___ksymtab、__stop___ksymtab、__start___ksymtab_gpl、__stop___ksymtab_gpl、__start___ksymtab_gpl_future、__stop___ksymtab_gpl_future,它们会在对内核或者是某一内核模块的导出符号表进行查找时用到。内核源码中为使用这些链接器产生的变量作了如下的声明:
<kernel/module.c>
extern const struct kernel_symbol __start___ksymtab[];
extern const struct kernel_symbol __stop___ksymtab[];
extern const struct kernel_symbol __start___ksymtab_gpl[];
extern const struct kernel_symbol __stop___ksymtab_gpl[];
extern const struct kernel_symbol __start___ksymtab_gpl_future[];
extern const struct kernel_symbol __stop___ksymtab_gpl_future[];
extern const unsigned long __start___kcrctab[];
extern const unsigned long __start___kcrctab_gpl[];
extern const unsigned long __start___kcrctab_gpl_future[];
内核代码便可以直接使用这些变量而不会引起编译错误。
还是我最喜欢的老环节,总结一下:
1.内核驱动是通过模块的形式加载到内核。在软件开发中有很多相似的过程,比如linux用户态的so,windows的ddl。
2.模块的后缀是.ko,文件格式是ELF。
3.模块向其他模块或者内核导出符号是通过EXPORT_SYMBOL宏实现。
4.EXPORT_SYMBOL背后是由内核源码/链接脚本实现。
再深入思考一点,我们能得到什么,或者说更加抽象的经验是什么:
1.解耦。将驱动功能与内核的基础功能分开。内核基础功能稳定性好,而驱动则是经常变化。
2.需要通过框架实现解耦后的部件间相互依赖的问题。
忽然在脑海里想到那么几个问题:
在我们的实际开发中会遇到驱动的bug导致整个内核panic重启的情况。这种问题是否可以得到优化呢?做到类似用户态的进程隔离。驱动死了内核可以不用收到影响。
我们总会考虑用户态进程的loading。驱动的loading是否有衡量方法。但是一般驱动都是做简单业务逻辑,不会涉及复杂的算法产生巨大的CPU开销。这个问题的必要性不是那么高。
相关文章:

linux驱动开发(1)-内核模块
内核模块 模块最大的好处是可以动态扩展应用程序的功能而无须重新编译链接生成新的应用程序镜像,在微软的Windows系统上动态链接库DLL(Dynamic Link Library),Linux系统上的共享库so(shared object)文件的…...

AI产品风向标:从「工具属性」到「认知引擎」的架构跃迁
近年来,人工智能正在改变法律行业的游戏规则。从最初的“工具属性”——帮律师干些重复的杂活儿,到如今逐渐变身为“认知引擎”——能够理解法律逻辑、分析案例,法律AI产品正在迎来一场华丽的转身。这篇文章将带你一探究竟,看看这…...

前端八股之CSS
CSS 盒子模型深度解析与实战 一、盒子模型核心概念 Box-sizing CSS 中的 box-sizing 属性定义了引擎应该如何计算一个元素的总宽度和总高度 语法: box-sizing: content-box|border-box|inherit:content-box 默认值,元素的 width/height 不包含paddi…...
ps自然饱和度调整
在Photoshop(PS)中,自然饱和度调整是一项用于优化图像色彩的重要功能,以下是对其详细解析: 一、功能概述 自然饱和度主要针对画面中饱和度较低的像素进行着重调整,同时对高饱和度区域限制较小,…...
有公网ip但外网访问不到怎么办?内网IP端口映射公网连接常见问题和原因
有公网IP但外网访问不到的核心原因通常包括:端口未正确映射、防火墙限制、DNS解析问题、运营商端口屏蔽或路由配置错误。需依次排查这些关键环节,其中端口映射和防火墙设置是最常见的原因。 内网IP端口映射公网连接常见问题和原因及解决方案 1…...
InlineHook的原理与做法
InlineHook翻译为内联钩子 内联也就是我们的内联汇编 钩子就是修改目标的执行流程或代码 #include<iostream> using namespace std; #include<Windows.h>DWORD OldPro 0; //老的保护权限 char OldCode[9] { 0 }; //hook前的汇编代码 DWORD RetData 0; …...

微服务-Sentinel
目录 背景 Sentinel使用 Sentinel控制台 Sentinel控制规则 Sentinel整合OpenFeign 背景 在微服务项目架构中,存在多个服务相互调用场景,在某些情况下某个微服务不可用时,上游调用者若一直等待,会产生资源的消耗,极端情…...
DNS缓存
DNS详细解释 DNS缓存(DNS Cache)是指操作系统或应用程序在本地保存的一份“域名与IP地址的对应关系”记录。 1. DNS的基本作用 当你访问一个网站(比如 www.jd.com)时,计算机需要先把这个域名转换成实际的IP地址&…...

MySQL垂直分库(基于MyCat)
参考资料: 参考视频 参考博客 Mycat基本部署 视频参考资料:链接: https://pan.baidu.com/s/1xT_WokN_xlRv0h06b6F3yg 提取码: aag3 概要: 本文的垂直分库,全部是基于前文部署的基本架构进行的 垂直分库: 垂直分库…...

Rust 变量与可变性
文章目录 变量与可变性常量遮蔽(Shadowing) 变量与可变性 Rust中变量默认是不可变的,这是 Rust 鼓励你编写更安全、易于并发代码的众多方式之一。不过,你仍然可以选择让变量可变。让我们来探讨 Rust 为什么鼓励你优先使用不可变性…...

深入理解 C++ 中的 list 容器:从基础使用到模拟实现
一、list 的底层数据结构与核心特性 1.1 双向循环链表的物理结构 节点定义:每个节点包含三个部分 template <typename T> struct ListNode {T data; // 存储的数据ListNode* prev; // 指向前驱节点的指针ListNode* next; // 指向后继节点的指针L…...

状态机实现文件单词统计
系统如何查找可执行文件 默认:在PATH路径下寻找文件文件下 执行当前目录下文件: ./:指定文件目录是当前目录 ./count:执行当前目录文件 编译.c文件为运行文件 gcc -o count 0voice.c #将0voice.c编译为名字count 为什么主函数要那么写&a…...

从0开始学习R语言--Day13--混合效应与生存分析
混合效应模型(Mixed Effects Model) 对于数据来说,我们通常把所有样本共有的影响因素(性别,实验处理,实验方法),这种可以推广到总体的叫做固有效应,而仅适用于特定分组的…...

基于mediapipe深度学习的虚拟画板系统python源码
目录 1.前言 2.算法运行效果图预览 3.算法运行软件版本 4.部分核心程序 5.算法仿真参数 6.算法理论概述 7.参考文献 8.算法完整程序工程 1.前言 虚拟画板系统基于计算机视觉与深度学习技术,通过摄像头捕获用户手部动作,利用 MediaPipe框架实现手…...

复变函数 $w = z^2$ 的映射图像演示
复变函数 w z 2 w z^2 wz2 的映射图像演示 复变函数 w z 2 w z^2 wz2 是一个基本的二次函数,在复平面上具有有趣的映射性质。下面我将介绍这个函数的映射特性,并使用MATLAB进行可视化演示。 映射特性 极坐标表示:若 z r e i θ z …...

Python实现P-PSO优化算法优化循环神经网络LSTM回归模型项目实战
说明:这是一个机器学习实战项目(附带数据代码文档),如需数据代码文档可以直接到文章最后关注获取。 1.项目背景 在当今数据驱动的时代,时间序列预测和回归分析是许多领域中不可或缺的技术手段。循环神经网络ÿ…...

复合机器人:纠偏算法如何重塑工业精度与效率?
在智能制造领域,复合机器人正成为柔性生产与高精度作业的核心载体。面对“复合机器人有纠偏算法吗?”这一行业高频问题,富唯智能以多模态纠偏技术体系给出了答案——通过自研的AI驱动纠偏算法与多传感器融合方案,复合机器人不仅能…...

审计- 1- 审计概述
1.财务报表审计的概念 财务报表审计是指注册会计师对财务报表是否不存在重大错报提供合理保证,以积极方式提出意见,增强除管理层之外的预期使用者对财务报表信赖的程度。 1.1 审计业务三方关系人 注册会计师对财务报表发表审计意见是注册会计师的责任管…...

在MDK中自动部署LVGL,在stm32f407ZGT6移植LVGL-8.4,运行demo,显示label
在MDK中自动部署LVGL,在stm32f407ZGT6移植LVGL-8.4 一、硬件平台二、实现功能三、移植步骤1、下载LVGL-8.42、MDK中安装LVGL-8.43、配置RTE4、配置头文件 lv_conf_cmsis.h5、配置lv_port_disp_template 四、添加心跳相关文件1、在STM32CubeMX中配置TIM7的参数2、使能…...

模块二:C++核心能力进阶(5篇) 篇一:《STL源码剖析:vector扩容策略与迭代器失效》
一、前言:重新认识vector的复杂性 在C开发者中,std::vector常被视为"动态数组"的简单实现,但其底层机制实则蕴含着深刻的工程智慧。本篇将通过: 多维度源码剖析(GCC/Clang/MSVC三平台实现对比)…...
计算机组成原理核心剖析:CPU、存储、I/O 与总线系统全解
引言 在当今数字化时代,计算机已经渗透到我们生活的方方面面,从智能手机到超级计算机,从智能家居到自动驾驶汽车。然而,你是否曾好奇过,这些功能强大的设备内部究竟是如何工作的?是什么让计算机能够执行各种…...
数据分类分级的实践与反思:源自数据分析、治理与安全交叉视角的洞察
在数据安全体系的构建中,数据分类分级无疑扮演着基石性的角色,其系统性与复杂性对从业者提出了极高要求。尽管个人并非专注于该领域的研究专家,但凭借在数据分析、数据治理及数据安全策略应用交叉地带的长期实践与系统性认知,得以…...

自动化立体仓库WCS的设计与实现
导语 大家好,我是社长,老K。专注分享智能制造和智能仓储物流等内容。欢迎大家使用我们的仓储物流技术AI智能体。 新书《智能物流系统构成与技术实践》 新书《智能仓储项目出海-英语手册,必备!》 完整版文件和更多学习资料…...
百度蜘蛛池的作用是什么?技术@baidutopseo
百度蜘蛛池的作用 百度蜘蛛池是一种用于集中管理和调度百度搜索引擎蜘蛛(爬虫)的技术工具。其主要目的是优化网站内容的收录效率,帮助搜索引擎更高效地发现和抓取网页内容。技术baidutopseo 提升收录效率 通过集中大量百度蜘蛛的访问请求&…...
8.linux文件与文件夹内处理命令cp,mv,rm
cp 可以用于复制文件/文件夹 cp 【-r】 参数1 参数2 -r,用于复制文件夹使用,表示递归 mv 用于移动文件/文件夹,没有选项 参数2:表示移动去的地方,如果参数2不存在,则进行改名,保证目标存在…...
JavaScript性能优化:实战技巧提升10倍速度
JavaScript 性能优化实战技术文章大纲 基础优化策略 减少 DOM 操作:频繁的 DOM 操作会导致重绘和回流,影响性能。使用文档片段(DocumentFragment)或虚拟 DOM 技术优化批量操作。 避免全局变量污染:全局变量会增加内…...
核函数:解锁支持向量机的强大能力
在机器学习的世界中,支持向量机(SVM)是一种强大的分类算法,而核函数则是其背后的“魔法”,让 SVM 能够处理复杂的非线性问题。今天,我们就来深入探讨核函数的奥秘,看看它们是如何帮助 SVM 在高维…...

UE5 2D地图曝光太亮怎么修改
UE5 2D地图曝光怎么修改 在场景添加后期处理体积 修改后期处理体积Exposure曝光参数最大值最小值都改为0 勾选Infinite Extend 全地图范围应用此后期处理体积...

C# 类和继承(基类访问)
基类访问 如果派生类必须访问被隐藏的继承成员,可以使用基类访问(base access)表达式。基类 访问表达式由关键字base后面跟着一个点和成员的名称组成,如下所示: 例如,在下面的代码中,派生类Oth…...

帕金森带来的生活困境
当这种健康状况出现,行动不再自如成为最明显的改变。日常行走时,步伐会逐渐变小、变慢,甚至会出现 “小碎步” 往前冲,难以停下,简单的起身、转身都可能变得艰难。手部也会不受控制地颤抖,拿水杯、系纽扣这…...