第三章 驱动模块编译与加载: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 实战经验总结

最后,我把自己这些年踩过的坑总结一下:

  1. 编译模块前先确认内核版本——不同内核版本的API可能有变化,我遇到过2.6.35和3.10的驱动接口完全不同。
  2. 模块参数名不要和全局变量重名——否则编译时不会报错,但运行时参数传递会出问题。
  3. rmmod 前先确认没有进程占用——可以用 lsof /dev/my_device 查看。
  4. modprobe 加载失败时,先看 dmesg——大部分错误信息都在内核日志里。
  5. 调试阶段用 insmod,产品阶段用 modprobe——前者灵活,后者规范。

嗯,这一章的内容就到这里。驱动编译和加载是基本功,看似简单,但细节很多。下一章我们会深入讲解字符设备驱动的核心——file_operations结构体,那才是真正开始写驱动的地方。