第三章 驱动模块编译与加载:Kconfig与Makefile编写、模块参数传递、insmod/rmmod/modprobe命令详解
3.1 驱动模块的“骨架”——Kconfig与Makefile
说实话,很多刚入行的朋友觉得驱动开发就是写代码。其实不然。你代码写得再漂亮,编译不过、加载不上,一切都是白搭。我个人习惯,拿到一个新平台,第一件事就是先把Kconfig和Makefile搭好。这就像盖房子先搭脚手架,看着不起眼,但少了它真不行。
3.1.1 Kconfig——让驱动出现在内核配置菜单里
Kconfig说白了就是告诉内核配置系统:“嘿,我这有个驱动,你要不要把它编进去?” 它的语法其实不复杂,我挑最常用的说。
# drivers/char/my_drv/Kconfig
config MY_DRV
tristate "My First Character Driver"
depends on ARM
default n
help
This is a sample driver for learning purpose.
Say Y here if you want to build it into kernel.
Say M if you want to build it as a module.
这里有个关键点:tristate 表示三种状态——Y(编入内核)、M(编译成模块)、N(不编译)。如果是 bool,那就只有Y和N,不能做成模块。我在项目中遇到过有人把 tristate 写成 bool,结果模块死活编译不出来,查了半天才发现是这里的问题。
重要提醒: depends on 后面的依赖一定要写对。比如你的驱动依赖I2C子系统,那就写 depends on I2C。否则配置菜单里根本看不到你的选项。
3.1.2 Makefile——告诉编译器怎么干活
Makefile的写法更直接。你想想看,编译器又不认识你的 .c 文件,你得告诉它哪个文件要编成什么。
# drivers/char/my_drv/Makefile
obj-$(CONFIG_MY_DRV) += my_drv.o
# 如果你的驱动由多个文件组成
# my_drv-objs := main.o ioctl.o probe.o
# obj-$(CONFIG_MY_DRV) += my_drv.o
嗯,这里要注意:obj-y 表示编入内核,obj-m 表示编译成模块。但实际项目中我们很少直接写死,而是用 obj-$(CONFIG_xxx) 这种形式,让Kconfig来决定。
我曾经见过一个新手,把 obj-$(CONFIG_MY_DRV) 写成了 obj-$(CONFIG_MY_DRV) 少了个下划线,结果编译时变量没展开,驱动文件根本没被编译。这种错误排查起来特别头疼,因为编译器不会报错,它只是默默地跳过了你的文件。
3.2 模块参数传递——让驱动更灵活
驱动写死了参数值,每次改都要重新编译?太傻了。模块参数就是用来解决这个问题的。
3.2.1 基本用法
#include <linux/moduleparam.h>
static int debug_level = 0;
static char *device_name = "default_dev";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
加载模块时就可以这样传参:
insmod my_drv.ko debug_level=2 device_name="my_device"
这里 0644 是文件权限,表示这个参数在 /sys/module/my_drv/parameters/ 下可见,并且可以在运行时修改。我个人习惯把调试相关的参数权限设成0644,这样产品出了问题,不用重新编译模块,直接 echo 改参数就能打开调试信息。
小技巧: 如果你不想让参数在运行时被修改,权限设成0就行。比如密码、密钥这类敏感参数,千万别设成0644。
3.2.2 参数类型与数组
| 类型 | 宏 | 说明 |
|---|---|---|
| int | module_param(name, int, perm) | 整型参数 |
| charp | module_param(name, charp, perm) | 字符串指针 |
| bool | module_param(name, bool, perm) | 布尔值 |
| 数组 | module_param_array(name, type, num, perm) | 数组参数,num返回元素个数 |
数组参数的例子:
static int irq_array[4];
static int irq_count;
module_param_array(irq_array, int, &irq_count, 0644);
// 加载时:insmod my_drv.ko irq_array=10,20,30,40
// 此时 irq_count = 4
3.3 模块加载与卸载——insmod/rmmod/modprobe
这三个命令,你天天都会用到。但你真的了解它们的区别吗?
3.3.1 insmod——简单粗暴
insmod 就是“install module”的缩写。它直接把一个 .ko 文件塞进内核。用法最简单:
insmod my_drv.ko
insmod my_drv.ko debug_level=1 # 带参数
但它有个致命缺点:不处理依赖。如果你的驱动依赖另一个模块,你得先手动加载那个模块。我在项目中遇到过,有个同事写了个SPI设备的驱动,忘了先加载SPI核心模块,结果 insmod 时报了一堆“Unknown symbol”错误。
避坑指南: 我曾经在调试一个PMIC驱动时,反复 insmod/rmmod 同一个模块,结果第三次加载时系统直接挂了。后来发现是模块的 .exit 函数里没有正确释放资源,导致内核数据结构被破坏。记住:写 .exit 函数时,一定要把 .init 里申请的所有资源都释放干净。
3.3.2 rmmod——卸载模块
rmmod 用来卸载模块。但有个前提:模块的引用计数必须为0。你可以用 lsmod 查看当前加载的模块和引用计数。
rmmod my_drv # 卸载模块
rmmod -f my_drv # 强制卸载(不推荐,除非你知道在做什么)
为什么有时候 rmmod 会失败?说白了就是还有进程在使用这个设备。你可以用 fuser 命令查一下是哪个进程占着:
fuser -v /dev/my_device
3.3.3 modprobe——智能加载
modprobe 是我最常用的命令。它比 insmod 聪明多了,会自动处理依赖关系。但它依赖 /lib/modules/$(uname -r)/ 下的模块信息。
modprobe my_drv # 加载模块及其依赖
modprobe -r my_drv # 卸载模块及其依赖
modprobe -c # 查看当前模块配置
modprobe 读取的是 /etc/modprobe.d/ 下的配置文件。你可以在这里设置默认参数:
# /etc/modprobe.d/my_drv.conf
options my_drv debug_level=2 device_name="prod_device"
这样每次 modprobe 加载时,都会自动带上这些参数。你想想看,产品量产时,几百台设备,总不能每台都手动传参吧?用配置文件就省事多了。
3.4 实战经验总结
最后,我把自己这些年踩过的坑总结一下:
- 编译模块前先确认内核版本——不同内核版本的API可能有变化,我遇到过2.6.35和3.10的驱动接口完全不同。
- 模块参数名不要和全局变量重名——否则编译时不会报错,但运行时参数传递会出问题。
- rmmod 前先确认没有进程占用——可以用
lsof /dev/my_device查看。 - modprobe 加载失败时,先看 dmesg——大部分错误信息都在内核日志里。
- 调试阶段用 insmod,产品阶段用 modprobe——前者灵活,后者规范。
嗯,这一章的内容就到这里。驱动编译和加载是基本功,看似简单,但细节很多。下一章我们会深入讲解字符设备驱动的核心——file_operations结构体,那才是真正开始写驱动的地方。