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 is in use"。我查了半天,发现是另一个模块忘记调用 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>/ 目录下。

我的建议:即使你写的是内部使用的模块,也建议声明 GPL 许可证。否则某些内核核心功能(比如 tracepoint、一些安全函数)你无法调用。这不是法律问题,是技术限制。

知识结构总览

下面这张图把本章的核心知识点串起来了,你可以对照着理解模块的完整生命周期:

Linux 内核模块编程基础 — 知识体系 内核模块 加载与卸载 module_init / module_exit insmod / rmmod / lsmod printk + dmesg 调试 模块参数 module_param 宏 int / charp / bool / 数组 sysfs 权限控制 依赖与许可证 EXPORT_SYMBOL MODULE_LICENSE MODULE_AUTHOR / DESCRIPTION 模块生命周期:编写 → 编译 → 加载 → 使用 → 卸载 💡 核心要点:模块是内核的「插件」,加载时注册功能,卸载时清理资源。 参数让模块更灵活,依赖管理让模块协作更安全,许可证是合规基础。

总结一下

这一章的内容其实就三件事:

  • 怎么写模块 — init 和 exit 函数是骨架,printk 是调试利器
  • 怎么让模块更灵活 — 参数传递,避免硬编码
  • 怎么管理模块间的关系 — 依赖、许可证、作者信息,这些是模块的「元数据」