2. 内核模块基础
Linux内核是一个单体内核(Monolithic Kernel),但通过内核模块(Kernel Module)机制实现了动态扩展能力。模块是可以在运行时加载到内核或从内核卸载的代码片段,无需重新编译整个内核或重启系统。本章将深入剖析模块的加载与卸载流程、module_init/module_exit宏的实现原理,以及模块许可证与作者信息的规范写法。
2.1 模块的加载与卸载流程
内核模块的生命周期由用户空间工具(insmod/rmmod)和内核模块加载器(module loader)协同管理。加载时,内核执行模块的初始化函数;卸载时,执行清理函数。
insmod mymodule.ko — 加载模块rmmod mymodule — 卸载模块modprobe mymodule — 自动加载模块及其依赖lsmod — 列出当前已加载的所有模块
2.2 module_init / module_exit 宏机制
module_init() 和 module_exit() 是内核提供的宏,用于指定模块的入口和出口函数。它们并非函数调用,而是通过 __attribute__((section(...))) 将函数指针放入特定的 ELF 段(.init.text 和 .exit.text),内核模块加载器在加载/卸载时从这些段中查找并执行对应的函数。
| 宏 | 作用 | 函数原型 | 所属段 |
|---|---|---|---|
module_init(x) |
指定模块加载时调用的初始化函数 | int __init x(void); |
.init.text |
module_exit(x) |
指定模块卸载时调用的清理函数 | void __exit x(void); |
.exit.text |
module_exit() 指定的函数不会被链接,因为 built-in 模块不允许卸载。此时 __exit 标记的函数会被编译器丢弃,以节省内存。
以下是一个完整的内核模块示例,展示了 module_init 和 module_exit 的标准用法:
#include <linux/module.h> /* 所有模块都需要 */
#include <linux/kernel.h> /* printk() */
#include <linux/init.h> /* __init / __exit 宏 */
static int __init mymodule_init(void)
{
printk(KERN_INFO "Hello, Kernel Module loaded!\n");
/* 在这里注册字符设备、platform驱动等 */
return 0; /* 0表示成功,负数表示失败 */
}
static void __exit mymodule_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module unloaded!\n");
/* 在这里注销设备、释放内存等 */
}
module_init(mymodule_init);
module_exit(mymodule_exit);
2.3 模块许可证与作者信息
Linux内核要求所有模块声明许可证,以维护GPL协议的合规性。内核通过 MODULE_LICENSE() 宏记录许可证信息,并在模块加载时检查。如果模块使用了GPL-only符号(如 EXPORT_SYMBOL_GPL 导出的符号),但未声明GPL兼容许可证,内核将拒绝加载该模块。
| 宏 | 用途 | 示例 |
|---|---|---|
MODULE_LICENSE(license) |
声明模块许可证 | MODULE_LICENSE("GPL"); |
MODULE_AUTHOR(author) |
声明模块作者 | MODULE_AUTHOR("Zhang San <zhangsan@example.com>"); |
MODULE_DESCRIPTION(desc) |
描述模块功能 | MODULE_DESCRIPTION("A simple character device driver"); |
MODULE_VERSION(ver) |
声明模块版本 | MODULE_VERSION("1.0"); |
MODULE_ALIAS(alias) |
为模块创建别名 | MODULE_ALIAS("char-major-240-0"); |
"GPL"— GNU General Public License v2 或更高版本"GPL v2"— 仅 GPL v2"GPL and additional rights"— GPL 附加额外权限"Dual BSD/GPL"— 双许可证 BSD 或 GPL"Dual MPL/GPL"— 双许可证 MPL 或 GPL"Proprietary"— 专有许可证(无法使用 GPL-only 符号)
以下是一个包含完整元信息的模块示例:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init mydriver_init(void)
{
printk(KERN_INFO "MyDriver: Initializing...\n");
return 0;
}
static void __exit mydriver_exit(void)
{
printk(KERN_INFO "MyDriver: Exiting...\n");
}
module_init(mydriver_init);
module_exit(mydriver_exit);
/* 模块元信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Li Si <lisi@example.com>");
MODULE_DESCRIPTION("Example character device driver for learning");
MODULE_VERSION("2.1");
MODULE_ALIAS("chardrv-example");
2.4 模块编译与Makefile
内核模块的编译依赖内核源码树(或内核头文件包)。标准的模块Makefile使用 obj-m 变量指定目标模块,并通过 make -C 进入内核构建系统。
# Makefile for kernel module
obj-m := mymodule.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
- 将
mymodule.c和Makefile放在同一目录 - 执行
make生成mymodule.ko - 执行
sudo insmod mymodule.ko加载模块 - 执行
dmesg | tail查看内核日志输出 - 执行
sudo rmmod mymodule卸载模块 - 执行
dmesg | tail确认清理函数执行
2.5 常见问题与调试技巧
| 问题 | 原因 | 解决方法 |
|---|---|---|
insmod: ERROR: could not insert module |
模块依赖的符号未导出,或版本不匹配 | 检查 modinfo mymodule.ko 的 vermagic;确认内核版本一致 |
module_init function returned non-zero |
初始化函数返回了负数错误码 | 检查 printk 日志定位具体失败点 |
module is already loaded |
模块已存在,重复加载 | 先 rmmod 卸载,再重新加载 |
Unknown symbol |
使用了未导出的内核符号 | 确认符号是否通过 EXPORT_SYMBOL 导出 |
disagrees about version of symbol |
内核版本与模块编译时的版本不一致 | 重新编译模块,确保使用当前内核的源码树 |
CONFIG_DEBUG_KERNEL 等调试选项。
通过本章的学习,你已经掌握了内核模块的基本骨架:加载/卸载流程、module_init/module_exit 的底层机制、模块元信息的规范写法,以及编译和调试的基本方法。这些知识将贯穿整个驱动开发课程,是后续字符设备和 platform 驱动的基础。