Linux内核模块开发:从Hello World到模块间通信
内核模块是Linux驱动开发的基石。理解模块的加载卸载机制、参数传递、符号导出和依赖管理,是写好驱动的第一步。本文基于Linux 6.8内核(Ubuntu 24.04 LTS),从最简模块出发,逐步覆盖模块参数、符号导出、版本控制和模块间通信。
一、内核模块是什么?
内核模块是一段可以在内核运行时动态加载和卸载的代码,扩展内核功能不需要重新编译内核或重启系统。
| 对比 | 内核模块 | 内核内建代码 |
|:-------|:---------|:-------------|
| 加载时机 | 运行时动态加载 | 编译时链接进内核映像 |
| 内存占用 | 按需加载,不用时卸载释放 | 始终常驻内存 |
| 开发调试 | 修改后insmod即可验证 | 需重新编译内核并重启 |
| 发布形式 | .ko文件独立分发 | 绑定内核版本发布 |
| 适用场景 | 设备驱动、文件系统、网络协议 | 核心调度器、内存管理 |
典型使用场景:
- 设备驱动:USB、I2C、SPI等外设驱动,硬件不确定时用模块最灵活
- 文件系统:ext4、FUSE等,按需挂载
- 网络协议栈扩展:Netfilter模块、自定义协议
二、最简内核模块
Hello World模块
// hello.c - 最简内核模块(Linux 6.8验证:#include <linux/module.h>
#include <linux/init.h>
/* 模块元信息*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("Hello World内核模块");
MODULE_VERSION("1.0");
/* 初始化函数:insmod时执行*/
static int __init hello_init(void)
{
printk(KERN_INFO "hello: module loaded\n");
return 0;
}
/* 退出函数:rmmod时执行*/
static void __exit hello_exit(void)
{
printk(KERN_INFO "hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
关键宏说明
| 作用 | 说明 | |
|---|---|---|
MODULE_LICENSE("GPL") |
声明许可证 | 必须声明,否则内核会打印tainted警告,且无法使用GPL-only符号 |
MODULE_AUTHOR() |
声明作者 | 纯信息,不影响功能 |
MODULE_DESCRIPTION() |
模块描述 | modinfo可查看 |
MODULE_VERSION() |
版本号 | 用于版本控制,modinfo可查看 |
module_init告诉内核加载模块时调用哪个函数,module_exit告诉内核卸载时调用哪个函数。__init标记的函数在初始化完成后内存会被回收,__exit标记的函数在内建代码中会被编译器丢弃。
printk与日志级别
printk(KERN_EMERG "系统不可用\n"); // 0 - 最高优先级
printk(KERN_ALERT "必须立即处理\n"); // 1
printk(KERN_CRIT "严重条件\n"); // 2
printk(KERN_ERR "错误条件\n"); // 3
printk(KERN_WARNING "警告条件\n"); // 4
printk(KERN_NOTICE "正常但值得注意\n"); // 5
printk(KERN_INFO "信息\n"); // 6
printk(KERN_DEBUG "调试信息\n"); // 7 - 最低优先级
控制台只显示优先级低于console_loglevel的消息(数值越小优先级越高)。用dmesg可以查看所有级别的内核日志。开发驱动时常用pr_info、pr_err、pr_warn等快捷宏,等价于printk(KERN_XXX ...)。
三、Makefile与构建
外部模块Makefile
# Makefile - 外部内核模块构建
obj-m := hello.o
KDIR := /lib/modules/$(shell uname -r)/build
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
KDIR指向当前运行内核的构建目录。M=$(PWD)告诉kbuild在指定目录下查找模块源码。obj-m表示编译为模块(obj-y则是内建)。
安装内核头文件
Ubuntu 24.04 LTS默认内核。.8系列,安装头文件。
sudo apt install linux-headers-$(uname -r) build-essential
常用命令
| 命令 | 作用 | 示例 |
|---|---|---|
make |
编译模块 | 生成hello.ko |
insmod |
加载模块 | sudo insmod hello.ko |
rmmod |
卸载模块 | sudo rmmod hello |
lsmod |
查看已加载模块 | lsmod | grep hello |
modinfo |
查看模块信息 | modinfo hello.ko |
dmesg |
查看内核日志 | dmesg | tail -5 |
modprobe |
智能加载(处理依赖) | sudo modprobe hello |
insmod和modprobe的区别:insmod只加载指定模块,遇到依赖会报错;modprobe自动解析并加载依赖模块。
四、模块参数
模块参数允许在加载时传入配置值,也可以通过sysfs在运行时修改。
基本参数定义
// param_demo.c - 模块参数示例
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
/* 定义参数 */
static int count = 1;
static char *name = "default";
static bool enable = false;
static int values[4] = {1, 2, 3, 4};
static int nvalues = 4; // 数组实际元素个数
/* 声明参数 */
module_param(count, int, S_IRUGO);
module_param(name, charp, S_IRUGO);
module_param(enable, bool, S_IRUGO);
module_param_array(values, int, &nvalues, S_IRUGO);
MODULE_PARM_DESC(count, "迭代次数(默认。);
MODULE_PARM_DESC(name, "设备名称(默认default。);
MODULE_PARM_DESC(enable, "使能开关(默认false。);
MODULE_PARM_DESC(values, "数值数组);
static int __init param_init(void)
{
int i;
pr_info("param: count=%d name=%s enable=%d\n", count, name, enable);
pr_info("param: values=");
for (i = 0; i < nvalues; i++)
pr_cont("%d ", values[i]);
pr_cont("\n");
return 0;
}
static void __exit param_exit(void)
{
pr_info("param: module unloaded\n");
}
module_init(param_init);
module_exit(param_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("模块参数示例");
参数类型与权限
| 类型 | 关键字 | 说明 |
|---|---|---|
| 整数 | int |
32位有符号整数 |
| 布尔 | bool |
0。,invbool取反 |
| 字符串 | charp |
内核自动分配内存存储字符串 |
| 整数数组 | array |
module_param_array,需传入长度变量指针 |
权限标志位
| 权限 | | 含义 |
|:-----|:---|:-----|
| S_IRUGO | 0444 | 所有用户可读(sysfs中可见) |
| S_IWUSR | 0200 | 仅root可写(运行时可修改) |
| S_IRUGO | S_IWUSR | 0644 | 所有用户可读,root可写 |
S_IRUGO | S_IWUSR是最常用的组合,允许运行时通过sysfs修改参数值。
传参方式
# insmod传参
sudo insmod param_demo.ko count=5 name="mydev" enable=1 values=10,20,30
# modprobe传参(写。etc/modprobe.d/下或命令行)
sudo modprobe param_demo count=5 name="mydev"
# sysfs运行时修改(需要S_IWUSR权限。echo 10 | sudo tee /sys/module/param_demo/parameters/count
自定义参数回调
当参数被修改时需要执行额外逻辑,用module_param_cb注册回调,
// param_cb_demo.c - 参数回调示例
#include <linux/module.h>
#include <linux/init.h>
#include <linux/moduleparam.h>
static int threshold = 100;
/* 参数设置回调 */
static int threshold_set(const char *val, const struct kernel_param *kp)
{
int ret;
long new_val;
ret = kstrtol(val, 10, &new_val);
if (ret < 0)
return ret;
if (new_val < 0 || new_val > 1000) {
pr_err("threshold: value %ld out of range [0, 1000]\n", new_val);
return -EINVAL;
}
pr_info("threshold: changed from %d to %ld\n", *(int *)kp->arg, new_val);
*(int *)kp->arg = (int)new_val;
return 0;
}
/* 参数获取回调 */
static int threshold_get(char *buf, const struct kernel_param *kp)
{
return scnprintf(buf, PAGE_SIZE, "%d\n", *(int *)kp->arg);
}
/* 定义kernel_param_ops */
static const struct kernel_param_ops threshold_ops = {
.set = threshold_set,
.get = threshold_get,
};
module_param_cb(threshold, &threshold_ops, &threshold, S_IRUGO | S_IWUSR);
MODULE_PARM_DESC(threshold, "阈值(0-1000,默认500)");
static int __init cb_init(void)
{
pr_info("cb: threshold=%d\n", threshold);
return 0;
}
static void __exit cb_exit(void)
{
pr_info("cb: module unloaded, threshold=%d\n", threshold);
}
module_init(cb_init);
module_exit(cb_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("参数回调示例");
kernel_param_ops结构体在Linux 3.0引入,替代了旧的module_param_call宏。.set在写sysfs时触发,.get在读sysfs时触发。
五、符号导出
内核模块通过EXPORT_SYMBOL将函数和变量暴露给其他模块使用,这是模块间协作的基础。
EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL
| 宏 | 可见性 | 说明 |
|---|---|---|
EXPORT_SYMBOL |
所有模块可用 | 导出符号对任意许可证模块开放 |
EXPORT_SYMBOL_GPL |
仅GPL模块可用 | 非GPL模块使用会报"Unknown symbol"错误 |
内核中大量核心符号(如kmalloc、printk)都是GPL-only的,这就是为什么MODULE_LICENSE("GPL")几乎是必选项。
模块间调用示例
模块A导出函数。
// module_a.c - 导出符号
#include <linux/module.h>
#include <linux/init.h>
static int add(int a, int b)
{
return a + b;
}
/* 导出符号,其他模块可调用 */
EXPORT_SYMBOL(add);
static int __init mod_a_init(void)
{
pr_info("mod_a: loaded, add() exported\n");
return 0;
}
static void __exit mod_a_exit(void)
{
pr_info("mod_a: unloaded\n");
}
module_init(mod_a_init);
module_exit(mod_a_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("符号导出模块A");
模块B调用导出函数。
// module_b.c - 使用导出符号
#include <linux/module.h>
#include <linux/init.h>
/* 声明外部符号 */
extern int add(int a, int b);
static int __init mod_b_init(void)
{
int result = add(3, 5);
pr_info("mod_b: add(3, 5) = %d\n", result);
return 0;
}
static void __exit mod_b_exit(void)
{
pr_info("mod_b: unloaded\n");
}
module_init(mod_b_init);
module_exit(mod_b_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("符号调用模块B");
加载顺序必须是先A后B,否则B找不到add符号。
sudo insmod module_a.ko
sudo insmod module_b.ko
dmesg | tail -5
# 输出: mod_b: add(3, 5) = 8
查看内核符号
# 查看所有内核导出符号cat /proc/kallsyms | grep add
# 查看模块导出的符号grep -E "EXPORT_SYMBOL" module_a.ko # 不准确,应看Module.symvers
# 查看Module.symvers(编译后生成。cat Module.symvers
/proc/kallsyms列出内核和已加载模块的所有符号地址。未开启kptr_restrict时显示真实地址,否则显示全0。
GPL-only符号的限制
如果模块B的许可证不是GPL,
MODULE_LICENSE("Proprietary"); // 非GPL
那么调用EXPORT_SYMBOL_GPL导出的符号时,insmod会报错:
Unknown symbol in module
内核维护两个符号表:__ksymtab(普通导出)和__ksymtab_gpl(GPL-only导出),加载模块时根据许可证决定能访问哪些符号。
六、模块依赖
MODULE_SOFTDEP声明软依赖
MODULE_SOFTDEP("pre: module_a"); // 前置依赖:加载前先加载module_a
MODULE_SOFTDEP("post: module_c"); // 后置依赖:加载后再加载module_c
MODULE_SOFTDEP只是声明元信息,不会自动加载依赖模块。modprobe读取这些信息后可以按顺序加载。
request_module()动态加载
// req_mod_demo.c - 动态请求加载模块#include <linux/module.h>
#include <linux/init.h>
static int __init req_init(void)
{
int ret;
/* 请求内核加载指定模块 */
ret = request_module("module_a");
if (ret < 0) {
pr_err("req: failed to load module_a, ret=%d\n", ret);
return ret;
}
pr_info("req: module_a loaded on demand\n");
return 0;
}
static void __exit req_exit(void)
{
pr_info("req: module unloaded\n");
}
module_init(req_init);
module_exit(req_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("动态加载依赖模块示例);
request_module在内核空间发起用户空间的modprobe调用,要求modprobe配置正确且模块已安装到/lib/modules/$(uname -r)/目录下。
通知链实现模块间通信
通知链(Notifier Chain)是内核中模块间松耦合通信的标准机制,不依赖符号导出与
// notifier_demo.c - 通知链示例#include <linux/module.h>
#include <linux/init.h>
#include <linux/notifier.h>
/* 自定义通知事件。*/
#define MY_EVENT_BASE 0x8000
#define MY_EVENT_DATA_READY (MY_EVENT_BASE + 1)
#define MY_EVENT_STOP (MY_EVENT_BASE + 2)
/* 定义通知链头 */
static BLOCKING_NOTIFIER_HEAD(my_notifier_list);
/* 注册/注销通知链的导出函数 */
int my_register_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_register(&my_notifier_list, nb);
}
EXPORT_SYMBOL(my_register_notifier);
int my_unregister_notifier(struct notifier_block *nb)
{
return blocking_notifier_chain_unregister(&my_notifier_list, nb);
}
EXPORT_SYMBOL(my_unregister_notifier);
/* 发送通知的导出函数*/
void my_notify_event(unsigned long event, void *data)
{
blocking_notifier_call_chain(&my_notifier_list, event, data);
}
EXPORT_SYMBOL(my_notify_event);
static int __init notifier_init(void)
{
pr_info("notifier: chain initialized\n");
return 0;
}
static void __exit notifier_exit(void)
{
pr_info("notifier: module unloaded\n");
}
module_init(notifier_init);
module_exit(notifier_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("通知链示例);
接收方模块注册回调:
// notifier_client.c - 通知链客户端
#include <linux/module.h>
#include <linux/init.h>
#include <linux/notifier.h>
extern int my_register_notifier(struct notifier_block *nb);
extern int my_unregister_notifier(struct notifier_block *nb);
#define MY_EVENT_BASE 0x8000
#define MY_EVENT_DATA_READY (MY_EVENT_BASE + 1)
static int my_event_handler(struct notifier_block *nb,
unsigned long event, void *data)
{
switch (event) {
case MY_EVENT_DATA_READY:
pr_info("client: received DATA_READY event, data=%p\n", data);
break;
default:
break;
}
return NOTIFY_DONE;
}
static struct notifier_block my_nb = {
.notifier_call = my_event_handler,
.priority = 0,
};
static int __init client_init(void)
{
int ret = my_register_notifier(&my_nb);
if (ret)
pr_err("client: register failed, ret=%d\n", ret);
else
pr_info("client: registered\n");
return ret;
}
static void __exit client_exit(void)
{
my_unregister_notifier(&my_nb);
pr_info("client: unregistered\n");
}
module_init(client_init);
module_exit(client_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("通知链客户端示例");
通知链有三种类型。
| 类型 | 宏 | 上下文 | 特点 |
|:-----|:---|:-------|:-----|
| 阻塞通知链 | BLOCKING_NOTIFIER_HEAD | 进程上下文 | 可睡眠 |
| 原始通知链 | RAW_NOTIFIER_HEAD | 任意上下文 | 无锁保护,需调用者自行保护 |
| SRCU通知链 | SRCU_NOTIFIER_HEAD | 任意上下文 | 可睡眠,开销比阻塞链大 |
驱动开发中BLOCKING_NOTIFIER_HEAD最常用,
七、模块版本控制
MODULE_VERSION
MODULE_VERSION("1.0.0");
版本号字符串,modinfo可查看。内核不解析版本号的语义,纯信息用途。
内核版本条件编译
#include <linux/version.h>
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 8, 0)
/* 6.8及以上使用新API */
ret = new_api_call();
#else
/* 旧版使用兼容API */
ret = legacy_api_call();
#endif
LINUX_VERSION_CODE是当前内核版本的数值编码,KERNEL_VERSION(major, minor, patch)生成对应数值,两者直接比较即可。跨内核版本发布驱动时必须用这种方式处理API差异,
Module.symvers与CRC校验
内核启用CONFIG_MODVERSIONS后,每个导出符号会计算CRC校验值,记录在Module.symvers文件中:
0xabc12345 add module_a EXPORT_SYMBOL_GPL
格式为:CRC值 符号。 模块。 导出类型。
加载模块时内核会比较符号的CRC值,不匹配则拒绝加载并报"Disagrees about version of symbol"错误。这是内核ABI兼容性检查机制。
开发外部模块时,必须使用目标内核的Module.symvers文件编译,否则CRC不匹配。Ubuntu 24.04的文件位于:
/lib/modules/6.8.x-xx-generic/build/Module.symvers
内核版本变化时的API适配
Linux内核没有稳定的内部ABI,每个大版本都可能变更API。实际开发中常见的适配场景。
- 函数签名变化:参数增减或类型改变
- 函数重命名或移除:如ioctl改为unlocked_ioctl
- 结构体字段调整:如file_operations新增成员
- 头文件路径变更
应对策略:用LINUX_VERSION_CODE条件编译,或针对特定内核版本维护不同代码分支。
八、模块运行时管理
引用计数
内核通过引用计数防止模块在使用中被卸载:
// 引用计数操作
if (!try_module_get(THIS_MODULE)) {
pr_err("failed to get module reference\n");
return -ENODEV;
}
/* 使用模块功能... */
module_put(THIS_MODULE);
| 函数 | 作用 | 返回回调 |
|---|---|---|
try_module_get(mod) |
增加引用计数 | 成功返回0,模块正在卸载时返回0 |
module_put(mod) |
减少引用计数 | 无返回值 |
try_module_get在引用前调用,失败说明模块正在卸载,不能再使用。module_put在使用完毕后调用。引用计数不为零时rmmod会失败。
fops.owner自动管理
字符设备驱动中,将file_operations.owner设为THIS_MODULE,内核会在open时自动增加引用计数,release时自动减少:
static const struct file_operations my_fops = {
.owner = THIS_MODULE, // 内核自动管理引用计数
.open = my_open,
.release = my_release,
.read = my_read,
};
这是最推荐的引用计数管理方式,不需要手动调用try_module_get/module_put。
模块状态查看
#include <linux/module.h>
/* 查看模块状态*/
enum module_state {
MODULE_STATE_LIVE, // 正常运行
MODULE_STATE_COMING, // 正在加载
MODULE_STATE_GOING, // 正在卸载
MODULE_STATE_UNFORMED, // 尚未完成初始化};
通过THIS_MODULE->state可以获取当前模块状态。
强制卸载的风险
sudo rmmod -f module_name
-f标志强制卸载,不管引用计数是否为0。这会导致正在使用该模块的代码访问已释放内存,直接后果是内核崩溃(oops/panic)。只在调试阶段使用,生产环境绝对禁止。
九、初始化优先级
内核定义7级initcall,决定模块初始化的执行顺序:
// 各级initcall宏(定义在include/linux/init.h)
pure_initcall(fn) // 0 - 仅对内建代码有效
core_initcall(fn) // 1 - 核心子系统(中断控制器、时钟)
postcore_initcall(fn) // 2 - 总线注册(PCI、USB总线)
arch_initcall(fn) // 3 - 架构相关初始化
subsys_initcall(fn) // 4 - 子系统初始化(网络、块设备)
fs_initcall(fn) // 5 - 文件系统初始化
device_initcall(fn) // 6 - 设备驱动(大多数驱动的默认级别)
late_initcall(fn) // 7 - 延迟初始化(依赖前面所有子系统就绪)
| 优先级 | 宏 | 级数 | 适用场景 |
|---|---|---|---|
| 最高 | pure_initcall |
0 | 极早期,几乎不用 |
| 1 | core_initcall |
1 | 中断控制器、时钟框架、DMA引擎 |
| 2 | postcore_initcall |
2 | 总线类型注册(platform/PCI/USB) |
| 3 | arch_initcall |
3 | SoC特定初始化、引脚配置 |
| 4 | subsys_initcall |
4 | 网络子系统、输入子系统、注册表 |
| 5 | fs_initcall |
5 | 文件系统注册(ext4/tmpfs) |
| 6 | device_initcall |
6 | 普通设备驱动,module_init默认等同此级 |
| 最低 | late_initcall |
7 | 依赖前面所有子系统就绪后才能初始化的驱动 |
对于可加载模块,module_init注册的函数在insmod时执行,initcall优先级只对内建代码有效。但理解这个顺序对调试初始化依赖问题很关键——如果驱动依赖某个子系统,而子系统初始化更晚,就会出问题。
十、常见问题
*Q1:insmod。Invalid module format"
内核版本不匹配。模块编译时使用的内核头文件版本与运行内核不一致。检查方法:
modinfo hello.ko | grep vermagic
uname -r
两者的版本号必须一致。解决:安装正确版本的linux-headers,重新编译模块。
Q2:modprobe找不到模块?
modprobe在/lib/modules/$(uname -r)/目录下搜索模块。如果模块不在该目录,需要先复制过去并更新依赖:
sudo cp hello.ko /lib/modules/$(uname -r)/extra/
sudo depmod -a
sudo modprobe hello
也可以在/etc/modprobe.d/下创建配置文件指定模块搜索路径。
Q3:模块卸载失败?
rmmod。Resource temporarily unavailable"。Device or resource busy",说明引用计数不为零。
# 查看引用计数
cat /proc/modules | grep hello
# 输出: hello 16384 1 - Live 0x...
# 第三列就是引用计数lsmod | grep hello
先关闭使用该模块的设备(如关闭打开的设备文件),再卸载。不要用rmmod -f强制卸载。
十一、总结
内核模块是Linux驱动开发的基本单元。从最简的Hello World到模块间通信,核心脉络是:模块加载卸载机制 。参数配置 。符号导出与依赖。运行时管理。掌握这些基础,后续写字符设备、Platform驱动、I2C/SPI驱动才有根基。
开发场景速查表
| 场景 | API/方法 |
|:-----|:---------|
| 加载时传递 | module_param / module_param_array |
| 运行时改参数 | module_param_cb + kernel_param_ops,权限设S_IWUSR |
| 模块间共享函数 | EXPORT_SYMBOL / EXPORT_SYMBOL_GPL |
| 声明模块依赖 | MODULE_SOFTDEP |
| 动态加载依赖 | request_module() |
| 模块间松耦合通信 | blocking_notifier_chain |
| 跨内核版本兼容 | LINUX_VERSION_CODE + KERNEL_VERSION() |
| 防止使用中卸载 | try_module_get / module_put,或fops.owner = THIS_MODULE |
| 查看内核符号 | cat /proc/kallsyms |
| 查看模块信息 | modinfo / lsmod / /sys/module/ |
本文首发于linuxros.cn,转载请注明出处。