2. 内核模块基础
Linux 内核模块是设备驱动的主要载体。与直接编译进内核的代码不同,模块允许在运行时动态加载和卸载,从而减小内核体积并提高开发效率。本章将系统讲解模块的编写规范、Makefile 构建、加载/卸载流程、参数传递与模块依赖管理。
2.1 模块的编写:最小框架
一个完整的内核模块至少包含两个核心函数:模块初始化入口(module_init)和模块卸载出口(module_exit)。此外,还需要通过 MODULE_LICENSE 声明许可证,否则内核会标记为“污染”。
关键点: 模块运行在内核空间,不能链接标准 C 库(如 glibc),只能使用内核导出的 API(如
printk、kmalloc)。
/* hello.c — 最小内核模块示例 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, 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");
| 元素 | 作用 | 注意事项 |
|---|---|---|
__init / __exit | 标记函数在初始化/卸载后释放内存 | 仅用于模块,非模块驱动不要使用 |
module_init() | 注册模块加载入口 | 必须且只能有一个 |
module_exit() | 注册模块卸载出口 | 必须且只能有一个 |
MODULE_LICENSE | 声明许可证,影响内核符号导出 | 推荐使用 "GPL" |
printk() | 内核日志输出,类似用户态 printf | 日志级别用 KERN_INFO 等宏 |
2.2 模块的编译:Makefile 编写
内核模块的编译依赖内核源码树(或内核头文件包),不能使用普通的 gcc 命令。标准做法是使用 kbuild 系统,通过 make -C /lib/modules/$(uname -r)/build M=$(PWD) modules 进行编译。
# Makefile — 编译 hello.ko
obj-m := hello.o
# 如果模块由多个源文件组成,使用如下格式:
# hello-objs := file1.o file2.o
# obj-m := hello.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
提示: 如果交叉编译,需要指定
ARCH 和 CROSS_COMPILE 变量,例如:make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-。
编译完成后,会生成 hello.ko(内核模块文件)、hello.mod.c(模块信息)、Module.symvers(符号版本表)等文件。
2.3 模块的加载与卸载:insmod / rmmod
加载模块使用 insmod 或 modprobe,卸载使用 rmmod。查看已加载模块使用 lsmod,查看模块信息使用 modinfo。
| 命令 | 功能 | 示例 |
|---|---|---|
insmod | 加载指定 .ko 文件,不自动处理依赖 | insmod hello.ko |
rmmod | 卸载指定模块 | rmmod hello |
modprobe | 智能加载/卸载,自动处理依赖 | modprobe hello |
lsmod | 列出所有已加载模块 | lsmod | grep hello |
modinfo | 查看模块详细信息 | modinfo hello.ko |
注意: 加载模块需要 root 权限。如果
insmod 失败,使用 dmesg | tail 查看内核日志定位错误。
# 加载模块
sudo insmod hello.ko
# 查看内核日志
dmesg | tail -5
# 预期输出: [ 1234.5678] Hello, Kernel Module!
# 卸载模块
sudo rmmod hello
dmesg | tail -5
# 预期输出: [ 1235.6789] Goodbye, Kernel Module!
2.4 模块参数:向模块传递配置
内核模块支持通过 module_param() 宏定义参数,在加载时通过 insmod 或 modprobe 传递。参数类型支持 int、charp(字符串指针)、bool、int array 等。
/* param_demo.c — 模块参数示例 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.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 times to print");
MODULE_PARM_DESC(name, "Name to greet");
static int __init param_init(void)
{
int i;
for (i = 0; i < count; i++)
printk(KERN_INFO "Hello %s! (count=%d)\n", name, count);
return 0;
}
static void __exit param_exit(void)
{
printk(KERN_INFO "Param module unloaded.\n");
}
module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL");
# 带参数加载
sudo insmod param_demo.ko count=3 name="DriverDev"
dmesg | tail -3
# 输出:
# [ ...] Hello DriverDev! (count=3)
# [ ...] Hello DriverDev! (count=3)
# [ ...] Hello DriverDev! (count=3)
| 参数 | 含义 | 示例 |
|---|---|---|
name | 变量名,与 C 变量一致 | count |
type | 参数类型:int, charp, bool, long 等 | int |
perm | sysfs 权限位,0 表示不暴露 | 0644(所有用户可读) |
2.5 模块依赖:符号导出与依赖管理
当一个模块使用另一个模块导出的符号(函数或变量)时,就形成了模块依赖。内核通过 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 宏导出符号供其他模块使用。
/* export_demo.c — 导出符号供其他模块使用 */
#include <linux/module.h>
#include <linux/kernel.h>
void my_print(const char *msg)
{
printk(KERN_INFO "my_print: %s\n", msg);
}
EXPORT_SYMBOL(my_print);
static int __init export_init(void)
{
printk(KERN_INFO "Export module loaded.\n");
return 0;
}
static void __exit export_exit(void)
{
printk(KERN_INFO "Export module unloaded.\n");
}
module_init(export_init);
module_exit(export_exit);
MODULE_LICENSE("GPL");
/* import_demo.c — 使用其他模块导出的符号 */
#include <linux/module.h>
#include <linux/kernel.h>
extern void my_print(const char *msg);
static int __init import_init(void)
{
my_print("Hello from import module!");
return 0;
}
static void __exit import_exit(void)
{
printk(KERN_INFO "Import module unloaded.\n");
}
module_init(import_init);
module_exit(import_exit);
MODULE_LICENSE("GPL");
依赖加载顺序: 必须先加载
export_demo.ko,再加载 import_demo.ko。使用 modprobe 可以自动处理依赖,但需要将模块安装到标准路径(/lib/modules/$(uname -r)/)并运行 depmod 生成依赖文件。
# 手动加载依赖模块
sudo insmod export_demo.ko
sudo insmod import_demo.ko
dmesg | tail -2
# 输出:
# [ ...] Export module loaded.
# [ ...] my_print: Hello from import module!
# 查看模块依赖
lsmod | grep import_demo
# 输出: import_demo 16384 0
# 注意:依赖关系不会在 lsmod 中直接显示,需查看 /proc/modules 或使用 modinfo
2.6 模块生命周期与内核日志
下图展示了内核模块从加载到卸载的完整生命周期,以及各阶段对应的内核日志输出。
2.7 常见问题与避坑指南
- 模块加载失败: 使用
dmesg | tail -20查看具体错误,常见原因包括符号未定义、版本魔术不匹配、内存不足。 - 模块卸载失败: 模块被其他模块依赖、有打开的设备文件、或模块代码中有资源未释放(如定时器、工作队列)。使用
lsmod查看引用计数,若不为 0 则无法卸载。 - printk 不输出: 检查日志级别,使用
KERN_ERR或KERN_ALERT确保输出到控制台;或通过dmesg查看。 - Makefile 编译错误: 确认内核头文件已安装(
apt install linux-headers-$(uname -r)),且KDIR路径正确。 - 模块参数不生效: 检查
module_param的perm权限位,若为 0 则无法通过 sysfs 查看,但加载时仍可传递。
开发效率建议: 在模块开发阶段,建议在
~/.bashrc 中添加别名:alias mkmod='make -C /lib/modules/$(uname -r)/build M=$(pwd) modules',简化编译命令。