4、驱动模块编译与加载:内核模块编写基础、Makefile编写、模块编译与insmod/rmmod、模块依赖处理
驱动开发的第一步,就是搞定模块的编译和加载。说实话,很多新手在这块栽跟头,不是因为代码逻辑多复杂,而是编译环境和加载流程没摸透。我刚开始做驱动时,也曾在 insmod 之后系统直接 panic,那种感觉……嗯,记忆犹新。
这一节,咱们就把模块编译与加载的整套流程捋清楚。从最简单的 Hello World 模块开始,到 Makefile 的写法,再到 insmod/rmmod 的使用,最后聊聊模块依赖怎么处理。你跟着走一遍,基本就能上手了。
4.1 内核模块的基本结构
一个标准的内核模块,说白了就两个核心函数:module_init 和 module_exit。前者在模块加载时调用,后者在卸载时调用。我习惯把模块比作一个「可插拔」的功能单元——插上去干活,拔下来收工。
来看一个最简模块的代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
printk(KERN_INFO "Hello, kernel! My driver loaded.\n");
return 0;
}
static void __exit my_driver_exit(void)
{
printk(KERN_INFO "Goodbye, kernel! My driver unloaded.\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example driver module");
这里有几个关键点:
__init和__exit是宏,告诉内核这些函数只在初始化或卸载时使用,之后可以释放内存。我在项目中见过有人忘了加,结果模块占着内存不放,虽然不影响功能,但不够优雅。printk是内核的打印函数,相当于用户态的 printf。日志级别用KERN_INFO等宏控制。MODULE_LICENSE必须声明,否则内核会报「tainted kernel」警告。我个人习惯统一用 GPL。
dmesg 查看 printk 的输出。我经常开两个终端,一个敲 insmod,一个跑 dmesg -w 实时看日志。
4.2 Makefile 编写——别小看它
Makefile 是模块编译的「指挥中心」。很多初学者觉得 Makefile 随便写写就行,其实不然。我见过最离谱的错误,就是 Makefile 里少了个 tab,结果编译报错半天找不到原因。
一个典型的内核模块 Makefile 长这样:
obj-m := my_driver.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
解释一下:
obj-m指定要编译成模块的目标文件。如果有多个源文件,写成obj-m := my_driver.o,然后加一行my_driver-objs := file1.o file2.o。KDIR指向当前内核的构建目录。注意,这里用的是uname -r动态获取版本号,确保编译环境与运行环境一致。我曾经因为内核版本不匹配,编译出的模块 insmod 直接报Invalid module format,折腾了一下午。-C选项让 make 进入内核源码目录,M=$(PWD)告诉内核编译系统当前模块的路径。
4.3 编译与加载:insmod 和 rmmod
编译完成后,你会得到一个 .ko 文件。这就是内核模块的「可执行文件」。加载和卸载分别用 insmod 和 rmmod 命令。
基本操作:
# 加载模块
sudo insmod my_driver.ko
# 查看已加载的模块
lsmod | grep my_driver
# 卸载模块
sudo rmmod my_driver
这里有几个坑,我帮你提前踩一踩:
- 权限问题: insmod 和 rmmod 都需要 root 权限。忘了加 sudo?嗯,你会看到 Permission denied。
- 模块卸载失败: 如果模块正在被使用(比如有设备文件打开着),rmmod 会报
Module is in use。可以用lsmod查看模块的引用计数。 - 模块参数: 你可以在 insmod 时传递参数,比如
insmod my_driver.ko debug=1。这需要在模块代码中用module_param声明。
4.4 模块依赖处理
实际项目中,模块之间经常有依赖关系。比如你的驱动用到了另一个模块导出的符号(函数或变量),那就必须先加载那个模块。
处理依赖的常用方法:
- 使用 modprobe:
modprobe会自动解析模块依赖,并加载所有需要的模块。它依赖/lib/modules/$(uname -r)/modules.dep文件,这个文件由depmod命令生成。 - 手动管理: 先用
lsmod查看当前加载的模块,再用modinfo查看模块的依赖信息。我早期做项目时,团队里有人把所有模块一股脑全加载,结果系统资源被占满……后来我们规范了依赖管理,用 modprobe 统一处理。
举个例子,假设你的模块 my_driver.ko 依赖 core_lib.ko:
# 先生成依赖关系
sudo depmod -a
# 用 modprobe 加载,它会自动加载 core_lib
sudo modprobe my_driver
# 卸载时也自动处理依赖
sudo modprobe -r my_driver
Module not found。查了半天,原来是 depmod 没跑,依赖文件没更新。记住:每次添加或修改模块后,都要重新跑一次 depmod。
4.5 知识体系总览
下面这张图,帮你把模块编译与加载的核心逻辑串起来:
从图上你能看到,整个流程从编写代码开始,经过 Makefile 编译,最终通过 insmod 或 modprobe 加载到内核。我个人更推荐用 modprobe,尤其是模块之间有依赖时,它能省去很多手动操作的麻烦。
4.6 常见问题与排查
最后,我把实际开发中遇到的高频问题列出来,你遇到时可以对照排查:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| insmod 报 Invalid module format | 内核版本不匹配,或编译配置不一致 | 检查 uname -r 与 KDIR 是否一致,重新编译 |
| insmod 报 Unknown symbol | 依赖的模块未加载,或符号未导出 | 用 modprobe 加载,或检查 EXPORT_SYMBOL |
| rmmod 报 Module is in use | 有进程或设备正在使用该模块 | 用 lsmod 查看引用计数,关闭相关进程 |
| dmesg 看不到 printk 输出 | 日志级别设置过高,或控制台未显示 | 用 dmesg 查看,或调整 printk 级别 |
嗯,这一节的内容就到这儿。模块编译与加载是驱动开发的基本功,你只要多练几次,把 Makefile 和 insmod/rmmod 用熟,后面写复杂驱动时就不会被这些基础问题绊住脚了。
可可精品资料库