3. Linux内核模块基础
Linux 内核模块(Loadable Kernel Module,LKM)是 Platform 驱动开发的最小可插拔单元。它允许我们在不重新编译整个内核、不重启系统的前提下,把设备驱动、文件系统、协议栈等能力动态注入内核空间。理解模块的加载与卸载机制、入口出口函数、参数传递以及符号依赖关系,是后续编写 Platform 驱动、设备树匹配和 probe 流程的必备基础。
一、核心原理:模块如何进入与离开内核
内核模块本质上是一个经过特殊编译的 .ko 目标文件,它使用内核提供的符号表,并在加载时被链接进内核地址空间。用户空间通过 insmod、modprobe 或 rmmod 触发系统调用,内核侧的模块加载器完成校验、重定位、符号解析,最后调用模块的初始化函数。
模块的生命周期可以概括为四个阶段:编译生成(生成 .ko)、加载(insmod/modprobe)、运行(内核调用入口函数并驻留)、卸载(rmmod 调用出口函数并释放)。其中加载与卸载是本节最核心的两条路径。
二、module_init 与 module_exit:模块的入口与出口
每个内核模块都必须至少定义入口函数,通常用 module_init() 宏注册;如果模块可卸载,还需要用 module_exit() 注册出口函数。这两个宏在 <linux/module.h> 中定义,它们把函数指针登记到模块的 struct module 结构中,供内核加载器在合适时机回调。
#include <linux/module.h>
#include <linux/kernel.h>
static int __init mydrv_init(void)
{
pr_info("mydrv: init\n");
return 0; /* 返回 0 表示加载成功 */
}
static void __exit mydrv_exit(void)
{
pr_info("mydrv: exit\n");
}
module_init(mydrv_init);
module_exit(mydrv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("platform-course");
MODULE_DESCRIPTION("A simple kernel module demo");
入口函数返回 0 表示加载成功,返回负值(如 -ENODEV、-EINVAL)则加载失败并自动回滚。出口函数没有返回值,必须保证释放所有在入口中申请的资源,否则会造成内存泄漏或设备状态残留。
三、模块参数:让模块在加载时可变
模块参数允许在 insmod 或 modprobe 时向模块传递配置值,常用于指定 GPIO 编号、设备索引、调试级别等。使用 module_param() 或 module_param_named() 声明,并通过 MODULE_PARM_DESC() 添加说明。
static int gpio_num = 18;
static char *dev_name = "platdev";
static bool debug_en = false;
module_param(gpio_num, int, 0644);
MODULE_PARM_DESC(gpio_num, "GPIO number used by driver");
module_param(dev_name, charp, 0644);
MODULE_PARM_DESC(dev_name, "device name string");
module_param(debug_en, bool, 0644);
MODULE_PARM_DESC(debug_en, "enable debug log");
/* 加载示例:
* insmod mydrv.ko gpio_num=23 dev_name=led0 debug_en=1
*/
参数类型支持 int、uint、long、charp、bool、数组等。权限位 0644 表示参数会在 /sys/module/模块名/parameters/ 下生成可读写节点,方便运行时查看和调整。
四、符号导出与模块依赖
模块之间可以互相调用函数,前提是被调用方通过 EXPORT_SYMBOL() 或 EXPORT_SYMBOL_GPL() 导出符号。导出后,该符号进入内核全局符号表,其他模块在加载时由内核完成符号解析和地址重定位。
| 机制 | 作用 | 典型使用场景 |
|---|---|---|
EXPORT_SYMBOL |
导出符号给所有模块使用 | 通用驱动框架、公共工具函数 |
EXPORT_SYMBOL_GPL |
仅导出给 GPL 兼容模块 | 内核内部核心接口 |
MODULE_LICENSE("GPL") |
声明许可证,影响 GPL 符号可用性 | 所有正式驱动模块 |
depmod / modprobe |
生成依赖关系并自动加载依赖模块 | 多模块协同的驱动栈 |
模块依赖关系记录在 /lib/modules/$(uname -r)/modules.dep 中,由 depmod 生成。使用 modprobe 加载模块时,内核会先加载其依赖模块;而 insmod 不会自动处理依赖,因此更适合调试单个模块。
# 查看模块信息与依赖
modinfo mydrv.ko
lsmod | grep mydrv
cat /proc/modules | grep mydrv
# 查看模块参数
ls /sys/module/mydrv/parameters/
cat /sys/module/mydrv/parameters/gpio_num
# 查看内核日志
dmesg | tail -20
五、案例场景:一个可加载的 Platform 驱动骨架
下面是一个典型的 Platform 驱动模块骨架,它把入口、出口、参数和许可证组合在一起,后续章节将在此基础上加入 platform_driver 注册与设备树匹配。
#include <linux/module.h>
#include <linux/platform_device.h>
static int irq_num = -1;
module_param(irq_num, int, 0444);
MODULE_PARM_DESC(irq_num, "IRQ number for the platform device");
static int __init platdrv_init(void)
{
pr_info("platdrv: loaded, irq=%d\n", irq_num);
return 0;
}
static void __exit platdrv_exit(void)
{
pr_info("platdrv: unloaded\n");
}
module_init(platdrv_init);
module_exit(platdrv_exit);
MODULE_LICENSE("GPL");
编译使用内核构建系统,典型 Makefile 如下:
obj-m += platdrv.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
六、常见误区与复盘
- 忘记 MODULE_LICENSE:未声明 GPL 时,使用
EXPORT_SYMBOL_GPL导出的符号会解析失败,模块加载报Unknown symbol。 - 出口函数不释放资源:入口申请的内存、中断、设备节点未释放,反复加载卸载会导致资源泄漏。
- 入口返回正值:入口函数应返回 0 或负错误码,返回正值会被内核视为异常。
- 参数权限过大:
0644允许运行时修改,若参数影响硬件行为,应使用0444只读。 - 依赖顺序错误:被依赖模块未先加载时,
insmod会报Unknown symbol,应使用modprobe或手动按序加载。
rmmod 会返回 Module is in use,强行卸载可能导致内核崩溃。