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");

编译与测试步骤:

  1. 将上述代码保存为 mymodule.c,Makefile 保存为 Makefile。
  2. 执行 make,生成 mymodule.ko。
  3. 加载模块:sudo insmod mymodule.ko count=3 name="testdev"
  4. 查看内核日志:dmesg | tail -10
  5. 卸载模块:sudo rmmod mymodule
  6. 再次查看日志确认退出函数执行。
验证要点:
  • 加载后 lsmod | grep mymodule 应显示模块已加载。
  • dmesg 中应看到初始化函数打印的迭代信息。
  • 卸载后 dmesg 应看到 "unloading" 信息。
  • modinfo mymodule.ko 可查看模块描述、参数、许可证等信息。

4.7 模块生命周期与内核交互流程

下图展示了从模块加载到卸载的完整生命周期,以及各要素在流程中的位置:

内核模块生命周期与核心要素 1. 编译阶段 Makefile + Kbuild 2. 模块加载 insmod / modprobe 3. 初始化 module_init() 模块核心要素 头文件 <linux/module.h> 入口/出口宏 module_init/exit 许可证声明 MODULE_LICENSE 模块参数 module_param Makefile 编译体系 4. 运行阶段 内核空间执行 5. 模块卸载 rmmod 6. 退出清理 module_exit() 参数传递流程 用户空间 insmod count=5 内核参数解析 module_param 宏 模块全局变量 static int count

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 实时监控内核日志,可快速定位问题。