Linux 内核模块编程基础:模块的加载与卸载、模块参数、模块依赖、模块许可证与作者信息
好,咱们正式开始动手写驱动了。不过别急,第一个程序不是去操作硬件,而是先搞懂内核模块本身是怎么跑起来的。你想想看,Linux 内核是一个庞然大物,总不能每次加个小功能都重新编译整个内核吧?所以就有了「内核模块」——它允许我们在运行时动态地向内核添加或移除代码。
说白了,模块就是一段可以在内核空间运行的程序。它没有 main 函数,入口和出口都是固定的。我刚开始学的时候,总觉得这东西很神秘,后来发现其实就是几个回调函数的事。
模块的加载与卸载
每个内核模块都必须实现两个核心函数:module_init() 和 module_exit()。前者在模块被加载时调用,后者在卸载时调用。这是内核模块的「身份证」,缺一不可。
来看一个最简模块的代码:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_module_init(void)
{
printk(KERN_INFO "Hello, Kernel Module!\n");
return 0;
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module!\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple kernel module example");
编译之后会生成 .ko 文件。操作命令很简单:
insmod my_module.ko— 加载模块rmmod my_module— 卸载模块(注意不带 .ko)lsmod— 查看当前已加载的模块列表modinfo my_module.ko— 查看模块信息
insmod 和 rmmod,但生产环境建议用 modprobe,它能自动处理模块依赖。
这里有个细节:printk 的输出不会显示在终端,而是写入内核日志缓冲区。用 dmesg 命令查看,或者 tail -f /var/log/kern.log 实时跟踪。
init 函数返回了非零值,但没打印任何错误信息。结果模块加载失败,我查了半天才发现是某个资源申请失败了。记住,init 返回 0 表示成功,返回负数表示失败,内核会帮你回滚。
模块参数
模块写死了参数值,每次改功能都要重新编译,太麻烦了。内核模块支持参数传递,让用户可以在加载时动态指定值。
用 module_param() 宏来声明参数:
static int count = 1;
static char *name = "default";
module_param(count, int, S_IRUGO);
module_param(name, charp, S_IRUGO);
MODULE_PARM_DESC(count, "Number of times to print");
MODULE_PARM_DESC(name, "Name to print");
加载时传参:
insmod my_module.ko count=5 name="Alice"
参数类型支持 int、long、charp(字符串指针)、bool 等。第三个参数是文件权限位,S_IRUGO 表示所有人可读,S_IWUSR 表示所有者可写。如果设为 0,参数在 sysfs 中不可见。
| 参数类型 | 宏 | 说明 |
|---|---|---|
| int | module_param(name, int, perm) | 整型参数 |
| charp | module_param(name, charp, perm) | 字符串指针 |
| bool | module_param(name, bool, perm) | 布尔值(y/n) |
| 数组 | module_param_array(name, type, num, perm) | 数组参数,num 返回元素个数 |
charp 指向的是用户空间传进来的缓冲区,模块内部不要试图修改它。我曾经犯过这个错,结果内核直接 oops 了。
模块依赖
模块之间可以互相调用。比如模块 A 导出了一个函数,模块 B 想用这个函数,那么 B 就依赖于 A。
导出符号用这两个宏:
// 在模块 A 中
EXPORT_SYMBOL(my_function);
EXPORT_SYMBOL_GPL(my_function); // 只允许 GPL 模块使用
模块 B 直接声明外部函数即可:
extern void my_function(void);
加载时,必须先加载 A,再加载 B。卸载时顺序相反。用 modprobe 可以自动处理依赖关系。
查看模块的依赖关系:
lsmod
# 输出示例:
# Module Size Used by
# my_module_B 16384 0
# my_module_A 20480 1 my_module_B
第三列 Used by 表示有多少个模块正在使用它。如果引用计数不为 0,rmmod 会失败。
module_put() 释放引用。内核模块的引用计数管理一定要小心,否则模块永远卸不掉。
模块许可证与作者信息
内核模块必须声明许可证,否则内核会认为它是「污染」的。常用的宏有:
MODULE_LICENSE("GPL")— 最常用,可以使用所有内核导出的符号MODULE_LICENSE("GPL v2")— GPL 第二版MODULE_LICENSE("Dual BSD/GPL")— 双许可证MODULE_LICENSE("Proprietary")— 专有许可证,但无法使用EXPORT_SYMBOL_GPL导出的符号
其他信息宏:
MODULE_AUTHOR("Your Name <your@email.com>");
MODULE_DESCRIPTION("A brief description of the module");
MODULE_VERSION("1.0");
MODULE_ALIAS("alias-name");
这些信息可以通过 modinfo 查看,也会出现在 /sys/module/<module_name>/ 目录下。
知识结构总览
下面这张图把本章的核心知识点串起来了,你可以对照着理解模块的完整生命周期:
总结一下
这一章的内容其实就三件事:
- 怎么写模块 — init 和 exit 函数是骨架,printk 是调试利器
- 怎么让模块更灵活 — 参数传递,避免硬编码
- 怎么管理模块间的关系 — 依赖、许可证、作者信息,这些是模块的「元数据」