第2章:内核模块编程入门
大家好,我是你们的Linux内核驱动讲师。今天我们来聊聊内核模块编程的入门知识。说实话,我刚开始学内核模块时,也觉得这东西挺神秘的——一个能在系统运行时动态加载的代码片段,想想就觉得很酷。
2.1 模块的加载与卸载
内核模块,说白了就是一段可以在系统运行时插入内核的代码。你不需要重新编译整个内核,也不需要重启系统。我个人觉得,这是Linux内核设计中最优雅的特性之一。
加载和卸载模块的命令很简单:
# 加载模块
insmod hello.ko
# 卸载模块
rmmod hello
# 查看已加载的模块
lsmod
嗯,这里要注意一点:insmod后面跟的是文件名(带.ko后缀),而rmmod后面跟的是模块名(不带后缀)。我在项目中遇到过好几次,同事把rmmod hello.ko写上去,结果报错说找不到模块。
2.2 module_init / module_exit 宏
每个内核模块都必须有两个基本函数:初始化和退出函数。这两个函数通过module_init和module_exit宏来注册。
#include <linux/module.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel!\n");
}
module_init(hello_init);
module_exit(hello_exit);
你想想看,为什么用__init和__exit标记?其实这两个宏告诉内核:初始化函数在运行完后可以释放掉,退出函数在模块卸载时才会用到。这是内核的一种优化手段。
2.3 LICENSE与作者信息
内核模块必须声明许可证,否则内核会认为你使用了非GPL兼容的许可证,从而限制你使用某些内核符号。
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your@email.com>");
MODULE_DESCRIPTION("A simple hello world module");
MODULE_VERSION("1.0");
为什么非要声明GPL?说白了,内核核心代码都是GPL协议的,如果你的模块不声明GPL兼容许可证,内核就不让你调用那些标记为EXPORT_SYMBOL_GPL的函数。我在项目中遇到过,有个第三方驱动没加GPL声明,结果编译通过但加载时一堆警告,功能也不正常。
| 宏 | 作用 |
|---|---|
| MODULE_LICENSE | 声明许可证类型,推荐使用"GPL" |
| MODULE_AUTHOR | 作者信息,方便维护 |
| MODULE_DESCRIPTION | 模块功能描述 |
| MODULE_VERSION | 版本号,方便追踪 |
2.4 printk调试技巧
printk是内核版的printf,但它比printf复杂得多。为什么?因为printk的输出可以有不同的日志级别。
printk(KERN_EMERG "系统崩溃级别\n");
printk(KERN_ALERT "需要立即处理\n");
printk(KERN_CRIT "严重情况\n");
printk(KERN_ERR "错误情况\n");
printk(KERN_WARNING "警告\n");
printk(KERN_NOTICE "正常但重要\n");
printk(KERN_INFO "信息性消息\n");
printk(KERN_DEBUG "调试消息\n");
我个人最常用的就是KERN_INFO和KERN_ERR。调试时用KERN_DEBUG,但要注意:默认情况下,KERN_DEBUG级别的消息不会显示在控制台上。
dmesg。我习惯在调试时开两个终端:一个运行模块,另一个用dmesg -w实时查看内核日志。
为什么会这样?因为printk的输出是写到内核环形缓冲区,而不是直接写到终端。所以你需要用dmesg来查看。嗯,这里有个坑:环形缓冲区大小有限,如果打印太多消息,旧消息会被覆盖。
2.5 模块参数传递
有时候你希望模块加载时能传递一些参数,比如设备数量、调试级别等。内核模块支持通过module_param宏来声明参数。
#include <linux/moduleparam.h>
static int count = 1;
static char *name = "default";
module_param(count, int, 0644);
module_param(name, charp, 0644);
MODULE_PARM_DESC(count, "设备数量");
MODULE_PARM_DESC(name, "设备名称");
加载时传递参数:
insmod hello.ko count=5 name="mydevice"
你想想看,为什么要有0644这个权限值?它表示这个参数在/sys/module/hello/parameters/目录下对应的文件权限。0644表示所有者可读写,其他人只读。这样你可以在运行时通过读写这些文件来修改参数值。
2.6 完整示例:一个带参数的可加载模块
好了,我们把今天学到的知识整合起来,写一个完整的模块:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/moduleparam.h>
static int count = 1;
static char *name = "hello_device";
module_param(count, int, 0644);
module_param(name, charp, 0644);
MODULE_PARM_DESC(count, "设备实例数量");
MODULE_PARM_DESC(name, "设备名称前缀");
static int __init mymodule_init(void)
{
int i;
printk(KERN_INFO "模块加载开始\n");
printk(KERN_INFO "设备名称: %s, 数量: %d\n", name, count);
for (i = 0; i < count; i++) {
printk(KERN_DEBUG "创建设备实例 %d\n", i);
}
return 0;
}
static void __exit mymodule_exit(void)
{
printk(KERN_INFO "模块卸载,再见!\n");
}
module_init(mymodule_init);
module_exit(mymodule_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("一个带参数的内核模块示例");
MODULE_VERSION("1.0");
编译这个模块,然后试试:
insmod mymodule.ko count=3 name="sensor"
dmesg | tail -10
你应该能看到模块打印的信息。然后卸载:
rmmod mymodule
dmesg | tail -5
嗯,到这里,你已经掌握了内核模块编程的基本功。说实话,这些内容虽然基础,但我在实际项目中每天都在用。下一章我们会深入探讨字符设备驱动的实现,到时候这些知识都会派上用场。
- 模块加载用insmod,卸载用rmmod,查看用lsmod
- module_init和module_exit是模块的入口和出口
- 必须声明MODULE_LICENSE,推荐使用"GPL"
- printk有8个日志级别,用dmesg查看输出
- module_param可以在加载时传递参数
记住:写内核模块不像写用户态程序,一个空指针就可能让整个系统崩溃。所以,小心谨慎,多打印调试信息,慢慢来。