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 |
|
编译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
模块卸载命令
- rmmod
- 功能:移除已经载入Linux的内核模块
- 语法:rmmod模块名
- 举例:rmmod hello_world.ko
查看模块信息命令
- 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文件
驱动模块传参
驱动传参的意义:
优势:
- 通过驱动传参,可以让驱动程序更加灵活,兼容性更强。
- 可以通过驱动传参,设置安全校验,防止驱动被盗用
不足
- 使驱动代码变得复杂化
- 增加了驱动的资源占用
驱动可以传递的参数类型
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中定义。
1 | module_param(name, type, perm); |
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 |
指向字符串缓冲的指针 |
示例:
1 |
|
加载模块时可以传参:
1 | insmod hello_world.ko myint=42 mystring="hi" myarray=9,8,7 |
运行时候查看参数:
1 | cd /sys/module/hello_world/parameters/ |
权限定义位置:读写权限在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 - 实际意义:所有人 执行 权限
- 举例用途:使所有人可执行某脚本或程序
内核符号表导入导出
驱动程序可以编译成内核模块,也就是KO文件。每个KO文件是相互独立的,也就是说模块之间无法相互访问。但是在某些使用场景下要相互访问,如B模块要用A模块中的函数。(B模块依赖于A模块)
符号表
”符号“就是内核中的函数名,全局变量名等。符号表就是用来记录这些”符号“的文件。
内核符号表导出
导出宏
| 宏名称 | 适用场景 |
|---|---|
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 | TODO |
| 16. SPI | TODO |
| 17. 串口 | TODO |
| 18. PWM | TODO |
| 19. RTC | TODO |
| 20. 看门狗 | TODO |
| 21. CAN | TODO |
| 22. 网络设备 | TODO |
| 23. ADC | TODO |
| 24. IIO | TODO |
| 25. USB | TODO |
| 26. LCD | TODO |
| 27. PCIe | TODO |





