2. 内核模块基础:module_init / module_exit、LICENSE、作者信息、编译 Makefile
在编写 proc 文件系统读写回调之前,必须掌握 Linux 内核模块的基本骨架。内核模块是动态加载到内核空间的代码段,它通过 module_init 和 module_exit 宏声明入口与出口函数,通过 MODULE_LICENSE 声明许可证,通过 MODULE_AUTHOR 和 MODULE_DESCRIPTION 提供元信息。本章将拆解这些要素,并给出标准 Makefile 模板。
2.1 模块入口与出口:module_init / module_exit
每个内核模块必须且只能有一个入口函数和一个出口函数。入口函数在模块加载时执行,完成资源申请、proc 文件创建、数据结构初始化等操作;出口函数在模块卸载时执行,负责资源释放、proc 文件移除、注销回调等清理工作。
| 宏 / 函数 | 作用 | 调用时机 | 返回值 |
|---|---|---|---|
module_init(func) |
声明模块入口函数 | insmod 加载时 |
0 成功,负值失败 |
module_exit(func) |
声明模块出口函数 | rmmod 卸载时 |
void |
| 入口函数名 | 通常为 xxx_init |
— | int |
| 出口函数名 | 通常为 xxx_exit |
— | void |
关键点:入口函数返回非 0 值时,内核认为模块加载失败,不会保留模块在内存中。出口函数不应返回错误,所有清理逻辑必须在出口函数内完成。
2.1.1 标准入口/出口函数模板
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_proc_init(void)
{
pr_info("my_proc: module loaded\n");
/* 在这里创建 proc 文件、分配内存、注册回调 */
return 0;
}
static void __exit my_proc_exit(void)
{
pr_info("my_proc: module unloaded\n");
/* 在这里移除 proc 文件、释放内存、注销回调 */
}
module_init(my_proc_init);
module_exit(my_proc_exit);
提示:
__init 和 __exit 是内核属性标记,告诉编译器将函数放入特定内存段。加载完成后 __init 函数占用的内存可以被回收。
2.2 模块元信息:LICENSE、AUTHOR、DESCRIPTION
Linux 内核要求每个模块明确声明许可证,否则内核会认为模块是“污染”的(tainted)。MODULE_LICENSE 必须使用内核认可的字符串,常见值如下:
| 许可证字符串 | 含义 | 是否污染内核 |
|---|---|---|
"GPL" |
GNU General Public License v2 | 否 |
"GPL v2" |
GPL v2 专用 | 否 |
"Dual BSD/GPL" |
BSD 或 GPL 双许可 | 否 |
"Proprietary" |
专有许可证 | 是 |
除许可证外,建议提供作者和描述信息,便于 modinfo 命令查看:
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("Proc file system read/write example module");
MODULE_VERSION("1.0");
警告:如果模块使用了 GPL-only 符号(如
proc_create 系列函数),但声明了非 GPL 许可证,内核会拒绝加载该模块,并打印 Unknown symbol 错误。
2.3 模块编译:标准 Makefile 模板
内核模块不能使用用户空间的 gcc 直接编译,必须借助内核构建系统(Kbuild)。以下 Makefile 适用于单文件模块:
obj-m := my_proc.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
如果模块包含多个源文件,使用 模块名-objs 语法:
obj-m := my_proc.o
my_proc-objs := main.o proc_ops.o utils.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
| 变量 | 含义 | 示例 |
|---|---|---|
obj-m |
需要编译成模块的目标文件 | obj-m := my_proc.o |
KDIR |
内核源码树路径 | /lib/modules/$(uname -r)/build |
M=$(PWD) |
告诉 Kbuild 在模块源码目录构建 | M=$(shell pwd) |
modules |
Kbuild 目标,生成 .ko 文件 | $(MAKE) -C $(KDIR) M=$(PWD) modules |
2.4 模块加载与卸载流程 SVG
下图展示了从 insmod 到 rmmod 的完整生命周期,以及 module_init / module_exit 在其中的位置:
2.5 编译与加载验证步骤
- 准备源码:将上述模块代码保存为
my_proc.c,Makefile 保存为Makefile。 - 执行编译:在源码目录运行
make,生成my_proc.ko。 - 加载模块:
sudo insmod my_proc.ko,使用dmesg | tail查看入口打印。 - 查看模块信息:
modinfo my_proc.ko显示许可证、作者、描述。 - 卸载模块:
sudo rmmod my_proc,再次查看dmesg确认出口打印。
常见错误排查:
- “Unknown symbol”:检查
MODULE_LICENSE是否与使用的内核符号兼容。 - “Invalid module format”:内核版本与编译头不匹配,确认
KDIR指向当前运行内核。 - “Permission denied”:加载/卸载需要 root 权限,使用
sudo。