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) 告诉内核编译系统当前模块的路径。
注意: Makefile 中的缩进必须用 tab,不能用空格。这是 make 的硬性要求,没得商量。

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 声明。
核心要点: insmod 加载模块时,内核会调用 module_init 指定的函数。如果该函数返回非零值,加载失败。rmmod 则调用 module_exit。这两个函数必须成对出现,否则模块卸载时会出问题。

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
避坑指南: 我曾经在嵌入式板子上调试,发现 modprobe 总是报 Module not found。查了半天,原来是 depmod 没跑,依赖文件没更新。记住:每次添加或修改模块后,都要重新跑一次 depmod。

4.5 知识体系总览

下面这张图,帮你把模块编译与加载的核心逻辑串起来:

驱动模块编译与加载流程 编写模块代码 module_init / module_exit 编写 Makefile obj-m / KDIR / modules 编译生成 .ko make 命令 加载模块 insmod / rmmod 手动加载/卸载 modprobe 自动处理依赖 模块加载成功 → 设备注册 → 驱动工作

从图上你能看到,整个流程从编写代码开始,经过 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 用熟,后面写复杂驱动时就不会被这些基础问题绊住脚了。

一句话总结: 写好模块代码,配好 Makefile,用 insmod 加载,用 rmmod 卸载,依赖交给 modprobe。这套流程走顺了,驱动开发就成功了一半。

可可精品资料库