内核模块基础:模块的加载与卸载、模块参数传递、模块依赖与符号导出
说到内核模块,我刚开始接触Linux驱动时,总觉得它挺神秘的。说白了,模块就是一段可以在运行时动态加载到内核的代码。你想想看,不用重新编译整个内核,不用重启机器,就能给系统添加一个新功能——这设计确实漂亮。
模块的加载与卸载:insmod与rmmod的幕后故事
先看一个最简单的模块代码。我习惯把它叫做"Hello World"的驱动版:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, module loaded!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, module unloaded!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example module");
这里有两个关键宏:module_init 和 module_exit。它们告诉内核:加载时执行哪个函数,卸载时执行哪个函数。嗯,这里要注意——__init 和 __exit 这两个标记不是摆设。加了 __init 的函数,初始化完成后内核会把它占用的内存释放掉。我在项目中遇到过有人没加这个标记,结果模块加载后白白浪费了几KB内存,虽然不多,但积少成多啊。
加载模块用 insmod,卸载用 rmmod。但你知道吗?insmod 背后其实调用了 init_module 系统调用,而 rmmod 对应 delete_module。内核会检查模块的依赖关系、许可证、版本信息等,全部通过后才真正执行你的初始化函数。
rmmod 只负责卸载,你申请的资源必须自己在退出函数里释放。
模块参数传递:让驱动更灵活
写驱动时,你肯定不希望把所有的配置都硬编码在代码里。比如调试开关、缓冲区大小、设备地址这些,最好能在加载时动态指定。这就是模块参数的作用。
看个例子:
#include <linux/moduleparam.h>
static int debug = 0;
static char *device_name = "default_dev";
module_param(debug, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug, "Enable debug output (0=off, 1=on)");
MODULE_PARM_DESC(device_name, "Device name string");
static int __init my_init(void)
{
printk("Debug mode: %d\n", debug);
printk("Device name: %s\n", device_name);
return 0;
}
加载时这样传参:
insmod my_module.ko debug=1 device_name="my_usb_dev"
module_param 的三个参数分别是:变量名、类型、权限。权限这里我多说一句——0644 表示这个参数在 /sys/module/ 下会生成一个文件,root用户可以运行时修改。我个人习惯把调试参数设成0644,这样不用卸载模块就能动态开关调试输出,排查问题特别方便。
module_param(debug, int, 0); 这样参数只能在加载时指定,加载后就变成只读了。
支持的参数类型有:bool、int、long、charp(字符串指针)、int数组 等。数组参数用 module_param_array 宏,用法类似。
模块依赖与符号导出:模块间的"社交关系"
模块不是孤岛。一个模块可能需要调用另一个模块提供的函数。比如你写了一个I2C控制器驱动,另一个I2C设备驱动需要调用你的传输函数。这时候就需要符号导出。
导出符号用这两个宏:
EXPORT_SYMBOL(func_name);
EXPORT_SYMBOL_GPL(func_name);
区别在于:EXPORT_SYMBOL_GPL 只允许GPL许可证的模块使用。我个人建议,除非有特殊商业需求,否则一律用 EXPORT_SYMBOL_GPL。为什么?因为很多内核核心函数都要求GPL,你导出的符号如果被非GPL模块用了,可能会引发许可证合规问题。
看个实际例子:
// 模块A:导出函数
#include <linux/module.h>
void my_custom_function(void)
{
printk("This is an exported function\n");
}
EXPORT_SYMBOL_GPL(my_custom_function);
MODULE_LICENSE("GPL");
// 模块B:使用导出的函数
extern void my_custom_function(void);
static int __init b_init(void)
{
my_custom_function();
return 0;
}
module_init(b_init);
MODULE_LICENSE("GPL");
加载时,必须先加载模块A,再加载模块B。如果顺序反了,insmod 会报 "Unknown symbol" 错误。内核的模块加载器会检查所有未解析的符号,然后去已加载的模块里找。找不到就拒绝加载。
modprobe 加载模块B时,它会自动先加载模块A。但用 insmod 不会——它只做你让它做的事。所以生产环境建议用 modprobe,它会处理依赖关系。
查看模块的符号表可以用:
nm my_module.ko | grep EXPORT_SYMBOL
或者加载后用:
cat /proc/kallsyms | grep my_custom_function
我记得有一次调试一个音频驱动,发现某个符号总是解析失败。折腾了半天,最后发现是模块B里声明了 extern,但模块A导出时用了 EXPORT_SYMBOL(不带GPL),而模块B的许可证是 PROPRIETARY。内核默认不允许非GPL模块调用非GPL导出的符号?其实不是——EXPORT_SYMBOL 对所有模块开放,但 EXPORT_SYMBOL_GPL 只对GPL模块开放。那次的问题其实是模块B忘了声明 MODULE_LICENSE("GPL")。
最后,用表格总结一下关键点:
| 操作 | 命令/宏 | 说明 |
|---|---|---|
| 加载模块 | insmod / modprobe | modprobe自动处理依赖 |
| 卸载模块 | rmmod / modprobe -r | modprobe -r 递归卸载依赖 |
| 声明初始化函数 | module_init() | 加载时调用 |
| 声明退出函数 | module_exit() | 卸载时调用 |
| 模块参数 | module_param() | 支持int/charp/bool等类型 |
| 导出符号 | EXPORT_SYMBOL[_GPL]() | GPL版本限制许可证 |
| 查看已加载模块 | lsmod | 显示模块列表和依赖 |
| 查看模块信息 | modinfo | 显示作者、描述、参数等 |
嗯,模块的基础知识就这些。说白了就是三件事:怎么加载卸载、怎么传参数、怎么让模块之间互相调用。把这些搞清楚了,后面写驱动时就不会在基础设施上栽跟头。