第3章 Linux内核模块基础
内核模块的结构、加载与卸载、Makefile 编写、printk 调试 —— 这些是驱动开发的“基本功”。我当年刚接触 Atlas 200 DK 时,就是靠这一章的内容把第一个 GPIO 点亮的。
3.1 内核模块长什么样?
说白了,一个内核模块就是一段可以在系统运行时动态插入内核的代码。你不用重新编译整个内核,也不用重启机器。我个人习惯把模块比作“乐高积木”——想加功能就插上去,想移除就拔下来。
每个模块至少有两个关键函数:module_init() 和 module_exit()。前者是入口,后者是出口。看个最简单的例子:
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Atlas 200 DK!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Atlas!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple module for Atlas 200 DK");
注意那个 __init 和 __exit 宏。它们告诉内核:初始化函数用完后可以扔掉,节省内存。我在项目中见过有人忘了加,结果模块占着内存不放……嗯,小细节但挺重要。
3.2 加载与卸载 —— 别小看 insmod 和 rmmod
编译出 .ko 文件后,怎么塞进内核?用 insmod 和 rmmod。你可能会说:“这不就两条命令吗?” 但坑往往藏在细节里。
常用命令速查表
| 命令 | 作用 | 备注 |
|---|---|---|
insmod hello.ko | 加载模块 | 需要绝对路径或相对路径 |
rmmod hello | 卸载模块 | 不用带 .ko 后缀 |
modprobe hello | 智能加载(含依赖) | 会查找 /lib/modules/... |
lsmod | 列出已加载模块 | 看有没有你的模块 |
modinfo hello.ko | 查看模块信息 | 作者、描述、参数等 |
我曾经在 Atlas 200 DK 上调试一个 I2C 驱动,insmod 下去没报错,但 lsmod 里就是看不到。折腾了半天,发现是模块依赖的另一个驱动没加载。所以 modprobe 有时候更靠谱,它会自动处理依赖。
⚠️ 避坑指南: 卸载模块时,如果模块正被其他模块或进程使用,rmmod 会失败。我曾经在调试时忘了关掉用户空间测试程序,结果 rmmod 一直报 “Module is in use”。先 fuser -k /dev/your_device 再卸载,省心很多。
3.3 Makefile 编写 —— 其实就一个套路
很多初学者觉得 Makefile 很神秘。我告诉你,内核模块的 Makefile 基本是固定的模板。你只要改改 obj-m 后面的名字就行。
# Makefile for Atlas 200 DK kernel module
obj-m := hello.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
解释一下:obj-m := hello.o 表示要编译成模块。如果模块由多个源文件组成,比如 hello-objs := main.o utils.o。KERNELDIR 指向内核源码树,Atlas 200 DK 的交叉编译环境里,这个路径要换成你实际的内核头文件路径。
💡 我的习惯: 在 Makefile 里加一行 ARCH := arm64 和 CROSS_COMPILE := aarch64-linux-gnu-,这样在开发机上交叉编译时不会搞错架构。我第一次在 x86 机器上编了个模块,拿到 Atlas 上 insmod,直接报 “invalid module format”…… 嗯,血的教训。
3.4 printk 调试 —— 驱动开发者的“printf”
用户空间程序用 printf,内核里就用 printk。用法几乎一样,但多了个日志级别。比如 printk(KERN_INFO "value = %d\n", val);。日志级别控制消息会不会显示在控制台。
| 宏 | 数字级别 | 典型场景 |
|---|---|---|
| KERN_EMERG | 0 | 系统崩溃前 |
| KERN_ERR | 3 | 驱动出错 |
| KERN_WARNING | 4 | 警告 |
| KERN_INFO | 6 | 普通信息 |
| KERN_DEBUG | 7 | 调试信息(默认不显示) |
在 Atlas 200 DK 上,我经常用 dmesg 查看 printk 输出。如果你觉得 dmesg 刷屏太快,可以 dmesg -w 实时跟踪。另外,echo 8 > /proc/sys/kernel/printk 可以把所有调试信息都打印到控制台,调试阶段很好用。
🔧 实战小技巧: 我习惯在模块入口和出口各加一条 printk,这样通过 dmesg 能立刻确认模块是否加载成功。如果加载后 dmesg 里啥也没有,先检查 MODULE_LICENSE 是不是 "GPL" —— 有些内核会拒绝加载非 GPL 模块,而且不报错。
3.5 完整示例:带参数的内核模块
模块还可以接收参数,像这样:
#include <linux/module.h>
#include <linux/moduleparam.h>
static int count = 1;
module_param(count, int, 0644);
MODULE_PARM_DESC(count, "Number of times to print");
static int __init param_init(void)
{
int i;
for (i = 0; i < count; i++)
printk(KERN_INFO "Param module: iteration %d\n", i+1);
return 0;
}
module_init(param_init);
module_exit(...);
加载时:insmod param.ko count=5。参数会在 /sys/module/param/parameters/ 下暴露出来,运行时也能改(如果权限允许)。