Linux驱动框架
时间轴
2025-11-09
init
Linux 驱动抽象
Linux 将存储器和外设分为 3 个基础大类:字符设备驱动,块设备驱动,网络设备驱动。
Linux 源码目录结构
| 目录 | 说明 |
|---|---|
| arch | 架构相关目录,存放多种 CPU 架构(如 arm、X86、MIPS 等)的适配代码 |
| block | 块设备相关代码目录,Linux 中以块设备形式管理硬盘、SD 卡等存储设备 |
| crypto | 加密算法目录,存放各类加密相关的实现代码 |
| Documentation | 官方 Linux 内核文档目录,包含内核功能、接口等详细说明 |
| drivers | 驱动目录,存放 Linux 系统支持的各类硬件设备驱动代码 |
| firmware | 固件目录,存放硬件设备所需的固件文件 |
| fs | 文件系统目录,存放 ext2、ext3、fat 等文件系统的实现代码 |
| include | 公共头文件目录,提供内核各模块共用的头文件 |
| init | 内核启动初始化目录,存放 Linux 内核启动阶段的初始化代码 |
| ipc | 进程间通信目录,存放管道、消息队列、共享内存等 IPC 机制的实现代码 |
| kernel | 内核核心目录,存放内核本身的核心功能代码 |
| lib | 库函数目录,存放内核使用的各类库函数 |
| mm | 内存管理目录(mm 为 memory management 缩写),负责内核内存管理功能 |
| net | 网络相关目录,存放 TCP/IP 协议栈等网络功能的实现代码 |
| scripts | 脚本目录,存放内核编译、测试等流程中使用的脚本文件 |
| security | 安全相关目录,存放内核安全机制的实现代码 |
| sound | 音频相关目录,存放音频设备驱动及音频处理的代码 |
| tools | 工具目录,存放 Linux 内核开发、调试用到的工具程序 |
| usr | 与 Linux 内核启动相关的代码目录 |
| virt | 内核虚拟机相关目录,存放内核级虚拟化功能的实现代码 |
最简单的 Linux 驱动结构解析
- 组成部分
- 头文件(必须):驱动需包含内核相关头文件,其中
<linux/module.h>和<linux/init.h>是必备的。 - 驱动加载函数(必须):加载驱动时,该函数会被内核自动执行。
- 驱动卸载函数(必须):卸载驱动时,该函数会被内核自动执行。
- 许可证声明(必须):因 Linux 内核遵循 GPL 协议,驱动加载时也需遵守相关协议,常见的有 GPL v2 等多种许可证类型。
- 模块参数(可选):是模块加载时传递给内核模块的值。
- 作者和版本信息(可选):用于声明驱动的作者及代码版本信息。
- 头文件(必须):驱动需包含内核相关头文件,其中
- 例子
1 |
|
当且仅当 module_init 中的 init 函数返回大于等于 0 的值模块才会被加载成功
模块信息
1 | $ objdump -h hello_world.ko |
内核模块使用其.modinfo部分来存储关于模块的信息,所有MODULE_*宏都用参数传递的值更新这部分的内容。其中一些宏是MODULE_DESCRIPTION()、MODULE_AUTHOR()和MODULE_LICENSE()。内核提供的在模块信息部分添加条目的真正底层宏是MODULE_INFO(tag,info),它添加的一般信息形式是tag=info。这意味着驱动程序作者可以自由添加其想要的任何形式信息,例如:
1 | MODULE_INFO(my_field_name, "What eeasy value"); |
.modeinfo部分的内容:
1 | $ readelf -x .modinfo hello_world.ko |
也可以使用modinfo查看
1 | $ modinfo hello_world.ko |
编译 Linux 驱动
- 将驱动放在 Linux 内核里面,然后编译 Linux 内核。将驱动编译到 Linux 内核里面。
- 将驱动编译成内核模块,独立于 Linux 内核之外
- 内核模块是 Linux 系统中的一个特殊的机制,可以将一些使用频率很少或者暂时不用的功能编译成内核模块,在需要的时候再动态加载到内核里面。
- 使用内核模块可以减小内核体积,加快启动速度。并且可以在系统运行时插入或者卸载驱动,无需重启系统。内核模块的后缀是.ko
编译成内核模块
1 | obj-m += hello_world.o |
本地的 Linux 代码在/lib/modules/$(uname -r)/kernel 下
将驱动编译到内核
在drivers/char(以字符驱动为例)创建文件夹 helloworld,然后将驱动源代码放入,然后创建 Kconfig 文件
1 | config helloworld |
更改 drivers
1 | emacs ../Kconfig |
在驱动源码里创建 Makefile
1 | obj-$(CONFIG_helloworld) += helloworld.o |
然后在上一级的 Makefile 中添加:
1 | emacs ../Makefile |
模块相关命令
模块加载命令
- insmod
- 功能:载入 Linux 内核模块
- 语法:insmod 模块名
- 例子:insmod hello_world.ko
- modprobe
- 功能:加载内核模块,同时这个模块所依赖的模块也同时被加载
- 语法:modprobe 模块名
- 举例:modprobe hello_world.ko
系统管理员或在生产系统中则常用 modprobe。modprobe 更智能,它在加载指定的模块之前解析文件 modules.dep,以便首先加载依赖关系。它会自动处理模块依赖关系,就像包管理器所做的那样。
但是**
modprobe不能直接加载任意路径下的.ko文件** —— 它只从标准内核模块目录(如/lib/modules/$(uname -r)/)中查找模块,并依赖modules.dep索引。
模块卸载命令
rmmod
- 功能:移除已经载入 Linux 的内核模块
- 语法:rmmod 模块名
- 举例:rmmod hello_world.ko
modeprobe -r
- 它不仅会尝试卸载
mymodule,还会自动检查并卸载那些只被mymodule依赖、且当前没有其他模块/进程在使用的依赖模块。 - 语法:
modeprobe -r mymodule
- 它不仅会尝试卸载
卸载内核模块功能的启用或禁用由 CONFIG_MODULE_UNLOAD 配置选项的值决定。没有这个选项,就不能卸载任何模块。
在运行时,如果模块卸载会导致其他不良影响,则即使有人要求卸载,内核也将阻止这样做。这是因为内核通过引用计数记录模块的使用次数,这样它就知道模块是否在用。如果内核认为删除一个模块是不安全的,就不会删除它。然而,以下设置可以改变这种行为:MODULE_FORCE_UNLOAD=y
设置启动时加载模块
如果要在启动的时候加载一些模块,则只需创建文件/etc/modules-load.d/<filename>.conf,并添加应该加载的模块名称(每行一个)。人们通常使用模块:/etc/modules-load.d/modules.conf。当然也可以根据需要创建多个.conf 文件。
1 | emacs /etc/modules-load.d/mymodule.conf |
查看模块信息命令
- lsmod命令
- 功能:列出已经载入 Linux 的内核模块
- 也可以使用命令 cat /proc/modules 来查看模块是否加载成功
- modinfo命令
- 功能:查看内核模块信息
- 语法:modinfo 模块名
- 举例:modinfo hello_world.ko
配置文件
menuconfig配置驱动选项状态操作:
驱动状态:
- 把驱动编译成内核模块,用 M 来表示
- 把驱动编译到内核里面,用*来表示
- 不编译
使用空格来切换这三种不同状态。
选项的状态有:
[]: 表示有两种状态只能设置成选中或者不选中<>: 表示有三种状态,可以设置成选中,不选中,和编译成模块- () : 表示用来存放字符串或者 16 进制数
Kconfig 文件
Kconfig 文件是图形化配置界面的源文件,图形化配置界面中的选项由 Kconfig 文件决定。当我们执行命令make menuconfig命令的时候,内核的配置工具会读取内核源码目录下的arch/xxx/Kconfig。xxx 是 ARCH 的值,比如 arm64,然后生成对应的配置界面供开发者使用。
config 文件与.config 文件
config 文件和.config 文件都是 Linux 内核的配置文件。
config 文件位于 Linux 内核源码的 arch/$(ARCH)/configs 目录下,是Linux 系统默认的配置文件。.
config 文件位于 Linux 内核源码的顶层目录下,编译 linux 内核时会使用.config 文件里面的配置来编译内核镜像。
若.config 存在,make menuconfig 界面的默认配置即当前.config 文件的配置,若修改了图形化配置界面的设置并保存,则.config 文件会被更新。
若.config 文件不存在,make menuconfig 界面的默认配置则为 Kconfig 文件中的默认配置。
使用命令 make xxx_defconfig 命令会根据 arch/$(ARCH)/configs 目录下默认文件生成.config 文件。
Kconfig 语法
参考:
主菜单
mainmenu用来设置主菜单的标题
举例:mainmenu "Linux/\$(ARCH) $(KERNELVERSION) Kernel Configuration"
上述名字设置的菜单名字为Linux/\$(ARCH) $(KERNELVERSION) Kernel Configuration
菜单结构
可以用 menu/endmenu 来生成菜单,menu 是菜单开始的标志,endmenu 是菜单结束的标志。这两个是成对出现的。
如下描述的是一个名字为:”Network device support”的菜单
1 | menu "Network device support" |
配置选项
使用关键字config来定义一个新的选项。每个选项都必须指定类型,类型包括bool, tristate, string, hex, int。最常见的是 bool, tristate, string 这三个。
bool 类型有两种值:y 和 n;
trisate 有三种值:y, m 和 n;
string 为字符串类型。
help 表示帮助信息,当我们在图形化界面按下 h 按键,弹出来的就是 help 的内容。
举例:
1 | config helloworld |
依赖关系
Kconfig 中的依赖关系可以用 depends on 和 select
depends on 表示直接依赖关系:
1 | config A |
表示选项 A 依赖选项 B,只有选项 B 被选中时,A 选项才可以被选中
select 表示反向依赖关系:
1 | config A |
在 A 选项被选中的情况下,B 选项自动被选中
可选选项
使用 choice 和 endchoice 定义可选择项
1 | choice |
注释
在图形化配置界面显示一个注释
1 | config TEST_CONFIG |
souce
source 用于读取另一个 Kconfig 文件,如 source “init/Kconfig” 就是读取 init 目录下的 Kconfig 文件到当前 Kconfig 文件
驱动模块传参
驱动传参的意义:
优势:
- 通过驱动传参,可以让驱动程序更加灵活,兼容性更强。
- 可以通过驱动传参,设置安全校验,防止驱动被盗用
不足
- 使驱动代码变得复杂化
- 增加了驱动的资源占用
驱动可以传递的参数类型
C 语言常用的数据类型内核大部分都支持驱动传参。这里将内核支持的驱动传递参数类型分为三类:
- 基本类型:char, bool, int, long, short, byte, ushort, uint
- 数组:array
- 字符串:string
给 Linux 驱动传递参数的方式
参数类型与对应函数
参数类型 对应函数 功能 基本类型 module_param传递基本类型参数 数组类型 module_param_array传递数组类型参数 字符串类型 module_param_string传递字符串类型参数 函数定义位置:这三个函数在 Linux 内核源码的
include/linux/moduleparam.h中定义。
module_param
1 | /** |
module_param_array
1 |
|
module_param_string
1 | /** |
MODULE_PARM_DESC
函数功能:描述模块参数的信息。在 include/linux/moduleparam.h 定义
函数原型:MODULE_PARM_DESC(_parm, desc)
函数参数: _parm:要描述的参数的参数名称。desc:描述信息
| 参数名 | 含义 |
|---|---|
name |
参数名(可在命令行传入) |
type |
参数类型(如:int、bool、charp) |
perm |
在 /sys/module/<modname>/parameters/ 中的权限,如 0644 |
nump |
(仅数组)保存数组元素数量的变量地址 |
len |
(仅字符串)缓冲区长度 |
string |
指向字符串缓冲的指针 |
权限定义
perm 是指 sysfs 中参数文件的 文件权限位。
读写权限在include/linux/stat.h和include/uapi/linux/stat.h中定义。
- include/linux/stat.h
1 | /* SPDX-License-Identifier: GPL-2.0 */ |
- include/uapi/linux/stat.h
1 | /* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */ |
相关的权限主要是文件访问权限宏(File Permission Bits)
| 宏名 | 八进制值 | 含义 |
|---|---|---|
S_IRWXU |
00700 |
所有者(U)读、写、执行权限(RWX) |
S_IRUSR |
00400 |
所有者(USR)读权限(R) |
S_IWUSR |
00200 |
所有者(USR)写权限(W) |
S_IXUSR |
00100 |
所有者(USR)执行权限(X) |
S_IRWXG |
00070 |
所属组(G)读、写、执行权限(RWX) |
S_IRGRP |
00040 |
所属组(GRP)读权限(R) |
S_IWGRP |
00020 |
所属组(GRP)写权限(W) |
S_IXGRP |
00010 |
所属组(GRP)执行权限(X) |
S_IRWXO |
00007 |
其他用户(O)读、写、执行权限(RWX) |
S_IROTH |
00004 |
其他用户(OTH)读权限(R) |
S_IWOTH |
00002 |
其他用户(OTH)写权限(W) |
S_IXOTH |
00001 |
其他用户(OTH)执行权限(X) |
还有组合权限
S_IRWXUGO
- 定义:
(S_IRWXU | S_IRWXG | S_IRWXO) - 含义(展开后):
00700 | 00070 | 00007 = 00777 - 实际意义:允许 所有者、组、其他用户 (UGO) 拥有 读、写、执行 权限
- 举例用途:设置所有人可读写执行,如临时目录
/tmp
S_IALLUGO
- 定义:
(S_ISUID | S_ISGID | S_ISVTX | S_IRWXUGO) - 含义(展开后):
0004000 | 0002000 | 0001000 | 00777 = 01777 - 实际意义:包含特殊位(SUID、SGID、Sticky)以及所有人读写执行权限
- 举例用途:常见权限:
drwxrwxrwt(如/tmp)
S_IRUGO
- 定义:
(S_IRUSR | S_IRGRP | S_IROTH) - 含义(展开后):
00400 | 00040 | 00004 = 00444 - 实际意义:所有人 读 权限
- 举例用途:常用于只读文件
S_IWUGO
- 定义:
(S_IWUSR | S_IWGRP | S_IWOTH) - 含义(展开后):
00200 | 00020 | 00002 = 00222 - 实际意义:所有人 写 权限
- 举例用途:少用,一般仅限特定目录
S_IXUGO
- 定义:
(S_IXUSR | S_IXGRP | S_IXOTH) - 含义(展开后):
00100 | 00010 | 00001 = 00111 - 实际意义:所有人 执行 权限
- 举例用途:使所有人可执行某脚本或程序
示例
1 |
|
加载模块时可以传参:
1 | insmod parameter.ko myint=42 mycharp="hello world" myarr=9,8,7 mystr="hello" |
运行时候查看参数:
1 | cd /sys/module/parameter/parameters/ |
内核符号表导入导出
驱动程序可以编译成内核模块,也就是 KO 文件。每个 KO 文件是相互独立的,也就是说模块之间无法相互访问。但是在某些使用场景下要相互访问,如 B 模块要用 A 模块中的函数。(B 模块依赖于 A 模块)
符号表
”符号“就是内核中的函数名,全局变量名等。符号表就是用来记录这些”符号“的文件。
模块依赖关系
linux 内核中的模块可以提供函数或变量,用EXPORT_SYMBOL宏导出它们即可供其他模块使用,这些被称作符号。
模块 B 对模块 A 的依赖是指模块 B 使用从模块 A 导出的符号。
depmod 是用户空间工具(通常由 kmod 包提供),在内核安装后运行:
1 | depmod -a <kernel_version> |
- 它扫描
/lib/modules/<kernel_release>/下所有.ko模块文件:- 使用
modinfo -F depends或直接解析 ELF 符号表; - 硔定每个模块 需要哪些符号(imports) 和 提供哪些符号(exports);
- 构建模块间的依赖关系图。
- 使用
生成的依赖文件
| 文件 | 作用 |
|---|---|
modules.dep |
文本格式,每行格式: module.ko: dependent_module1.ko dependent_module2.ko ... |
modules.dep.bin |
二进制格式,供 modprobe 快速加载(避免每次解析文本) |
modules.symbols / modules.symbols.bin |
记录所有导出符号及其所属模块(用于反向查找) |
depmod 还处理模块文件以提取和收集该信息,并在/lib/modules/<kernel_release>/modules.alias中生成 modules.alias 文件,该文件将设备映射到其对应的驱动程序。
modprobe 会解析 modules.alias 文件。
内核符号表导出
导出宏
| 宏名称 | 适用场景 |
|---|---|
EXPORT_SYMBOL |
导出符号到内核符号表 |
EXPORT_SYMBOL_GPL |
仅适用于包含 GPL 许可的模块 |
导出的符号可被其他模块使用,使用前只需声明即可。
例子
1 |
|
导入
1 |
|
注意:加载时要先加载导出的那个模块,卸载时要先卸载导入的那个模块。因为此时两个模块已经有了依赖关系,modprobe 会自动处理这些依赖关系,不需要显式声明。
使用 makefile 中定义的宏
核心思路:想要让它被 C 代码可见,必须通过 编译器命令行传入宏定义:
1 | cc -D宏名=值 source.c |
例子:
1 | obj-m += mydriver.o |
其他变量名:
| 变量名 | 作用范围 | 典型用途 |
|---|---|---|
ccflags-y |
当前模块的所有 .c 文件 |
普通 C 编译选项 |
asflags-y |
汇编文件 | 汇编参数 |
subdir-ccflags-y |
当前目录及子目录 | 全局作用 |
KBUILD_CFLAGS |
全局(由内核顶层 Makefile 设置) | 平台级 CFLAGS |
KBUILD_CFLAGS_MODULE |
内核模块 | |
EXTRA_CFLAGS |
已废弃(旧版用法) | 临时附加选项 |
然后在 driver 中可以使用:
1 |
|
Linux 驱动笔记
|
| ——————— | ———————————- |
| 1. Linux 驱动框架 | https://even629.com/posts/2511093/ |
| 2. Linux 驱动加载逻辑 | https://even629.com/posts/2511100/ |
| 3. 字符设备基础 | https://even629.com/posts/2511113/ |
| 4. 并发与竞争 | https://even629.com/posts/2511123/ |
| 5. 高级字符设备进阶 | https://even629.com/posts/2511133/ |
| 6. 中断 | https://even629.com/posts/2511143/ |
| 7. 平台总线 | https://even629.com/posts/2511153/ |
| 8. 设备树 | https://even629.com/posts/2511300/ |
| 9. 设备模型 | https://even629.com/posts/2512013/ |
| 10. 热插拔 | https://even629.com/posts/2512023/ |
| 11. pinctrl 子系统 | https://even629.com/posts/2512160/ |
| 12. gpio 子系统 | https://even629.com/posts/2512173/ |
| 13. 输入子系统 | https://even629.com/posts/2512183/ |
| 14. 单总线 | https://even629.com/posts/2512233/ |
| 15. I2C | https://even629.com/posts/2512283/ |
| 16. SPI | https://even629.com/posts/2512303/ |
| 17. UART | https://even629.com/posts/2512313/ |
| 18. PWM | https://even629.com/posts/2601043/ |
| 19. RTC | https://even629.com/posts/2601053/ |
| 20. Watchdog | https://even629.com/posts/2601063/ |
| 21. CAN | https://even629.com/posts/2601093/ |
| 22. 网络设备 | https://even629.com/posts/2601133/ |
| 23. ADC | https://even629.com/posts/2601143/ |
| 24. IIO | TODO |
| 25. USB | TODO |
| 26. LCD | TODO |
| 27. PCIe | TODO |



