第2章 内核模块基础:模块概念、模块加载与卸载、模块参数传递、模块依赖关系、模块许可证

好,咱们今天聊聊内核模块。这东西说白了,就是Linux内核的「乐高积木」。你想想看,内核本身是个大块头,要是把所有功能都编译进去,那得多臃肿?而且每次加个新硬件还得重新编译内核,这谁受得了。

所以就有了模块机制。模块可以动态加载、卸载,不用重启系统。我个人觉得,这是Linux内核设计里最巧妙的地方之一。

2.1 模块到底是什么?

模块,本质上就是一段内核代码。但它不是直接编译进内核的,而是编译成一个独立的 .ko 文件(Kernel Object)。

我刚开始学的时候,总把模块和普通应用程序搞混。其实区别大了去了:

  • 模块运行在内核空间,应用程序跑在用户空间
  • 模块没有 main 函数,只有入口和出口函数
  • 模块崩溃了,整个系统可能跟着挂掉

核心要点:模块是内核的一部分,不是独立进程。它拥有最高权限,也能搞出最大的破坏。

2.2 模块的加载与卸载

写一个最简单的模块,其实就两个函数:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

static int __init my_module_init(void)
{
    printk(KERN_INFO "Hello, module loaded!\n");
    return 0;
}

static void __exit my_module_exit(void)
{
    printk(KERN_INFO "Goodbye, module unloaded!\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple example module");

嗯,这里要注意几个点:

  • __init__exit 是宏,告诉内核这些函数只在初始化/退出时用,用完后可以释放内存
  • module_initmodule_exit 是必须的,否则内核不知道从哪里开始执行
  • printk 是内核版的 printf,输出到内核日志

加载卸载的命令也很简单:

# 加载模块
insmod my_module.ko

# 查看已加载的模块
lsmod

# 卸载模块
rmmod my_module

小技巧:我习惯用 modprobe 代替 insmod,因为 modprobe 会自动处理依赖关系。比如模块 A 依赖模块 B,你用 insmod 得先手动加载 B,但 modprobe 会帮你搞定。

2.3 模块参数传递

有时候,你希望模块加载时能传一些参数进去。比如指定缓冲区大小、调试级别什么的。这就要用到 module_param 宏了。

#include <linux/moduleparam.h>

static int debug_level = 0;
static char *device_name = "default";

module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);

MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");

加载时传参:

insmod my_module.ko debug_level=2 device_name="mydev"

这里有个坑,我曾经踩过:参数类型一定要匹配。比如你声明了 int,传了个字符串进去,内核不会给你报错,但行为就不可预测了。嗯,调试起来特别痛苦。

另外,0644 是文件权限,表示这个参数在 /sys/module/ 下可见,运行时也能修改。如果你不想让别人改,设成 0 就行。

2.4 模块依赖关系

模块之间可以互相依赖。比如你写了个 USB 驱动,它肯定依赖 USB 核心模块。这种依赖关系是怎么来的?

说白了,就是符号导出。一个模块可以导出函数或变量给其他模块用:

// 在模块 A 中
EXPORT_SYMBOL(my_function);
EXPORT_SYMBOL_GPL(my_variable);  // 只允许 GPL 模块使用

然后模块 B 就可以直接调用 my_function 了。加载时,系统会自动检查依赖,如果模块 B 依赖的模块 A 没加载,就会报错。

警告:循环依赖是噩梦。模块 A 依赖 B,B 又依赖 A,加载时就会死锁。我见过有人这么干,结果系统直接 hang 住。设计模块时一定要避免循环依赖。

查看依赖关系可以用 modinfo

modinfo my_module.ko
# 输出中会显示 depends: 字段

2.5 模块许可证

最后聊聊许可证。你可能觉得这玩意儿就是个形式,其实不是。内核社区对许可证很敏感。

常用的宏有:

含义
MODULE_LICENSE("GPL") GNU General Public License
MODULE_LICENSE("GPL v2") GPL 第二版
MODULE_LICENSE("BSD") BSD 许可证
MODULE_LICENSE("Proprietary") 专有许可证

为什么重要?因为内核里有些符号是用 EXPORT_SYMBOL_GPL 导出的。如果你的模块不是 GPL 许可证,就调用不了这些符号。说白了,这是内核社区保护开源生态的一种手段。

我记得有一次,一个客户非要我们写个闭源驱动,结果发现调不了几个关键的内核 API。最后没办法,要么开源,要么重写整个逻辑。嗯,这就是现实。

我的建议:除非你有特殊原因,否则一律用 GPL。省心,也符合内核社区的规矩。

小结

这一章我们聊了模块的五个核心概念:

  • 模块是什么——内核的乐高积木
  • 怎么加载卸载——insmod/rmmod 走起
  • 怎么传参数——module_param 搞定
  • 依赖关系怎么处理——EXPORT_SYMBOL 和 modprobe
  • 许可证为什么重要——GPL 是王道

下一章,我们会深入模块的编译过程,聊聊 Makefile 怎么写,以及怎么调试模块。到时候见。