2. Linux内核模块基础:模块的编写、编译、加载与卸载
Linux内核模块(Loadable Kernel Module, LKM)是Linux内核动态扩展能力的核心机制。它允许开发者在不重新编译整个内核、不重启系统的情况下,向内核动态添加或移除功能。对于平台设备驱动开发者而言,掌握模块的编写、编译、加载与卸载是进入内核驱动开发的第一步。
2.1 内核模块的基本结构
一个标准的内核模块至少包含两个关键入口点:模块初始化和模块退出。这两个入口点通过module_init()和module_exit()宏向内核注册。
核心概念:内核模块运行在内核空间,拥有最高权限。模块代码不能链接标准C库(如glibc),必须使用内核导出的API函数。
2.1.1 最小内核模块示例
#include <linux/module.h> // 所有模块都需要
#include <linux/kernel.h> // printk() 等内核打印函数
#include <linux/init.h> // __init, __exit 宏
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Linux Kernel Module!\n");
return 0; // 返回0表示初始化成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL"); // 许可证声明
MODULE_AUTHOR("Your Name"); // 作者信息
MODULE_DESCRIPTION("A simple Hello World module"); // 模块描述
MODULE_VERSION("1.0"); // 版本号
2.1.2 模块组成要素
| 要素 | 宏/函数 | 说明 |
|---|---|---|
| 初始化入口 | module_init(func) |
模块加载时调用,执行资源分配、设备注册等初始化操作 |
| 退出入口 | module_exit(func) |
模块卸载时调用,执行资源释放、设备注销等清理操作 |
| 许可证声明 | MODULE_LICENSE("GPL") |
必须声明,否则内核会标记为“污染”(tainted) |
| 作者信息 | MODULE_AUTHOR("text") |
可选,用于标识模块作者 |
| 模块描述 | MODULE_DESCRIPTION("text") |
可选,描述模块功能 |
| 版本号 | MODULE_VERSION("ver") |
可选,用于版本管理 |
2.2 module_init / module_exit 机制深度解析
module_init()和module_exit()是内核模块框架的核心宏。它们将开发者定义的函数指针注册到内核的模块管理链表中,使得内核在加载/卸载模块时能够正确调用对应的处理函数。
2.3 内核模块的编译
内核模块的编译不能使用普通的gcc命令,必须借助内核的Kbuild构建系统。Kbuild系统会使用内核源码树中的Makefile规则来编译模块,确保模块与当前运行内核的ABI兼容。
2.3.1 Makefile编写
# Makefile for hello kernel module
# 模块目标名称
obj-m := hello.o
# 如果模块由多个源文件组成,使用如下语法:
# hello-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
2.3.2 编译命令执行
$ make
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
LD [M] /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'
提示:编译成功后生成
.ko文件(kernel object),这就是可加载的内核模块二进制文件。Module.symvers文件记录了模块导出的符号,供其他模块依赖使用。
2.4 模块的加载与卸载
Linux提供了两组工具用于管理内核模块:insmod/rmmod(基础工具)和modprobe/modprobe -r(高级工具,支持依赖解析)。
2.4.1 加载模块
| 命令 | 语法 | 特点 |
|---|---|---|
insmod |
insmod hello.ko |
直接加载指定模块文件,不处理依赖 |
modprobe |
modprobe hello |
自动解析依赖,加载模块及其依赖模块 |
2.4.2 卸载模块
| 命令 | 语法 | 特点 |
|---|---|---|
rmmod |
rmmod hello |
直接卸载指定模块 |
modprobe -r |
modprobe -r hello |
卸载模块及其依赖模块(如无其他使用者) |
2.4.3 查看模块信息
# 查看已加载模块列表
$ lsmod
# 查看模块详细信息
$ modinfo hello.ko
filename: /home/user/hello/hello.ko
license: GPL
author: Your Name
description: A simple Hello World module
version: 1.0
depends:
name: hello
vermagic: 5.15.0-91-generic SMP mod_unload modversions
2.5 模块加载与卸载的完整流程
2.6 模块参数传递
内核模块支持在加载时通过命令行传递参数,使用module_param()宏声明。
#include <linux/moduleparam.h>
static int count = 1;
static char *name = "default";
module_param(count, int, 0644); // 参数名, 类型, 权限
module_param(name, charp, 0644); // charp = char pointer
MODULE_PARM_DESC(count, "Number of times to print");
MODULE_PARM_DESC(name, "Name to display");
static int __init param_init(void)
{
int i;
for (i = 0; i < count; i++)
printk(KERN_INFO "Hello %s!\n", name);
return 0;
}
# 加载时传递参数
$ insmod hello.ko count=3 name="DriverDev"
# 查看/sys中的参数
$ cat /sys/module/hello/parameters/count
3
$ cat /sys/module/hello/parameters/name
DriverDev
注意:模块参数权限位(如0644)控制
/sys/module/<name>/parameters/下文件的访问权限。如果设置为0,则参数在sysfs中不可见。运行时修改参数值需谨慎,某些参数不支持运行时修改。
2.7 常见问题与调试技巧
- 模块加载失败:使用
dmesg查看内核日志,定位具体错误原因(如符号未定义、版本不匹配等)。 - 模块卸载失败(EBUSY):表示模块正在被使用,使用
lsmod | grep <module>查看引用计数。 - 内核污染(Tainted):未声明GPL许可证或使用了专有符号,内核会标记为污染,影响调试支持。
- 版本魔术字不匹配:模块编译时的内核版本与运行内核版本不一致,需重新编译。
调试建议:开发阶段建议在模块初始化函数中增加
printk调试信息,使用KERN_DEBUG级别,并通过dmesg -w实时监控日志输出。生产环境中应移除或使用pr_debug()(需要DEBUG宏定义才生效)。