4. 第一个内核模块
编写第一个内核模块是嵌入式Linux驱动开发的起点。一个完整的内核模块包含五个核心要素:头文件、入口/出口宏、许可证声明、模块参数以及编译体系。本节将逐一拆解这些要素,并给出可直接编译运行的示例。
4.1 模块头文件
内核模块必须包含特定的头文件,这些头文件提供了模块编程所需的宏、函数原型和数据结构。最基础的两个头文件是:
| 头文件 | 作用 | 关键符号 |
|---|---|---|
<linux/module.h> |
模块核心支持,包含模块宏、许可证声明、参数传递等 | module_init, module_exit, MODULE_LICENSE, module_param |
<linux/kernel.h> |
内核通用接口,包含打印函数、类型定义等 | printk, KERN_INFO, ARRAY_SIZE |
<linux/init.h> |
初始化与退出标记宏 | __init, __exit |
提示:在较新内核(5.x+)中,
<linux/module.h> 已间接包含 <linux/kernel.h> 和 <linux/init.h>,但显式包含这三个头文件仍是行业惯例,可提高代码可读性和跨版本兼容性。
4.2 module_init / module_exit 宏
每个内核模块必须注册一个初始化函数和一个退出函数。这两个宏将普通C函数与内核模块生命周期绑定:
module_init(func):指定模块加载时调用的函数。函数原型为int __init func(void),成功返回0,失败返回负错误码。module_exit(func):指定模块卸载时调用的函数。函数原型为void __exit func(void),无返回值。
使用 __init 和 __exit 标记可让内核在初始化完成后释放该函数占用的内存,优化运行时资源。
关键规则:
- 初始化函数必须返回
int,成功返回0,失败返回负值(如-ENOMEM、-EINVAL)。 - 退出函数必须返回
void。 - 一个模块只能有一个
module_init和一个module_exit。
4.3 LICENSE / GPL 声明
内核模块必须声明许可证,否则内核在加载时会打印“tainted”警告。最常用的声明是 MODULE_LICENSE("GPL"),表示模块遵循GNU通用公共许可证。
| 许可证字符串 | 含义 | 内核行为 |
|---|---|---|
"GPL" |
GNU GPL v2 或更高版本 | 正常加载,可使用所有GPL导出的符号 |
"GPL v2" |
仅GPL v2 | 正常加载 |
"Dual BSD/GPL" |
BSD或GPL双许可 | 正常加载 |
"Proprietary" |
专有许可证 | 加载后内核标记为“污染”(tainted),不支持调试 |
注意:如果模块使用了
EXPORT_SYMBOL_GPL 导出的内核符号,则必须声明 MODULE_LICENSE("GPL"),否则链接会失败。这是内核社区保护GPL代码不被专有模块使用的机制。
4.4 模块参数传递
内核模块支持在加载时通过 insmod 或 modprobe 传递参数,无需重新编译。使用 module_param 宏声明参数:
#include <linux/moduleparam.h>
static int count = 1;
static char *name = "default";
module_param(count, int, 0644);
module_param(name, charp, 0644);
MODULE_PARM_DESC(count, "Number of iterations (default: 1)");
MODULE_PARM_DESC(name, "Device name string");
| 宏 | 参数 | 说明 |
|---|---|---|
module_param(name, type, perm) |
变量名、类型、sysfs权限 | 声明一个模块参数 |
MODULE_PARM_DESC(name, desc) |
参数名、描述字符串 | 为参数添加帮助信息(modinfo 可查看) |
支持的参数类型包括:byte、short、ushort、int、uint、long、ulong、charp(字符串指针)、bool、invbool。
加载时传递参数示例:
sudo insmod mymodule.ko count=5 name="mydev"
4.5 编译 Makefile 编写
内核模块不能使用普通的gcc编译,必须通过内核构建系统(Kbuild)完成。标准Makefile模板如下:
# Makefile for a simple kernel module
# 模块名称(最终生成 .ko 文件)
obj-m := mymodule.o
# 如果模块由多个源文件组成,使用如下语法:
# mymodule-objs := file1.o file2.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 |
指定要编译为模块的目标文件(.o),最终生成 .ko |
KDIR |
内核源码树路径,通常指向已编译的内核构建目录 |
M=$(PWD) |
告诉Kbuild在指定目录下构建模块 |
modules |
Kbuild目标,编译所有 obj-m 指定的模块 |
clean |
清理编译产物 |
交叉编译提示:嵌入式开发中,
KDIR 应指向目标平台的内核源码树,并设置 CROSS_COMPILE 变量。例如:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
4.6 完整示例:第一个内核模块
将以上要素整合为一个可编译、可加载的完整模块:
// mymodule.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
// 模块参数
static int count = 1;
static char *name = "mymodule";
module_param(count, int, 0644);
module_param(name, charp, 0644);
MODULE_PARM_DESC(count, "Loop count (default: 1)");
MODULE_PARM_DESC(name, "Device name (default: mymodule)");
// 初始化函数
static int __init mymodule_init(void)
{
int i;
printk(KERN_INFO "mymodule: loading, name=%s, count=%d\n", name, count);
for (i = 0; i < count; i++) {
printk(KERN_INFO "mymodule: iteration %d\n", i + 1);
}
return 0;
}
// 退出函数
static void __exit mymodule_exit(void)
{
printk(KERN_INFO "mymodule: unloading\n");
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple kernel module example");
MODULE_VERSION("1.0");
编译与测试步骤:
- 将上述代码保存为
mymodule.c,Makefile 保存为Makefile。 - 执行
make,生成mymodule.ko。 - 加载模块:
sudo insmod mymodule.ko count=3 name="testdev" - 查看内核日志:
dmesg | tail -10 - 卸载模块:
sudo rmmod mymodule - 再次查看日志确认退出函数执行。
验证要点:
- 加载后
lsmod | grep mymodule应显示模块已加载。 dmesg中应看到初始化函数打印的迭代信息。- 卸载后
dmesg应看到 "unloading" 信息。 modinfo mymodule.ko可查看模块描述、参数、许可证等信息。
4.7 模块生命周期与内核交互流程
下图展示了从模块加载到卸载的完整生命周期,以及各要素在流程中的位置:
4.8 常见错误与排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
insmod: ERROR: could not insert module |
内核版本不匹配、符号未导出、许可证冲突 | 检查 KDIR 路径,确认 MODULE_LICENSE 正确 |
module: 'xxx' taints kernel |
许可证非GPL或未声明 | 添加 MODULE_LICENSE("GPL") |
unknown parameter 'xxx' |
参数未用 module_param 声明 |
检查参数宏定义,确认类型匹配 |
make: *** /lib/modules/.../build: No such file or directory |
内核头文件未安装 | 安装 linux-headers-$(uname -r) 或配置正确的 KDIR |
调试建议:初次编写模块时,建议先编译一个“最小模块”(仅包含
module_init、module_exit 和 MODULE_LICENSE),确认编译和加载环境正常后,再逐步添加参数和其他功能。使用 dmesg -w 实时监控内核日志,可快速定位问题。