引言
Linux 内核模块是扩展内核功能的强大机制,它允许我们在不重新编译内核、不重启系统的情况下,动态添加或移除功能。无论是设备驱动、文件系统,还是安全策略,都可以通过模块的形式灵活加载。对于想深入 Linux 内核世界的开发者来说,编写一个简单的内核模块是最好的起点。
本文将从环境准备讲起,带你理解内核模块的核心概念,然后一步步编写一个带有模块参数的 Hello World 模块,并完成编译、加载、测试与卸载的全过程。即使你之前从未接触过内核代码,也能跟着完成第一个可运行的模块。
一、核心概念:什么是内核模块
内核模块是一段可以动态链接到运行中内核的二进制代码。它与普通用户态程序有本质区别:
- 运行空间:模块运行在内核空间(Ring 0),可以直接访问硬件和内核数据结构,而用户程序运行在用户空间(Ring 3)。
- 内存管理:模块没有独立的地址空间,它共享内核的地址空间。因此,内存错误可能导致整个系统崩溃。
- 函数库:模块只能调用内核导出的函数(如
printk()),不能使用标准 C 库(如printf())。 - 并发与中断:模块代码可能被多个进程并发执行,也可能在中断上下文中运行,因此编写时必须考虑同步与原子操作。
每个模块至少需要两个关键函数:
-初始化函数:在模块被加载时调用(insmod或modprobe),通常完成资源申请、注册设备等。
-清理函数:在模块被卸载时调用(rmmod),负责释放资源、注销设备。
此外,模块还需声明许可证(MODULE_LICENSE)以符合内核的 GPL 约束,否则内核会被“污染”(tainted),甚至拒绝加载部分符号。
二、实战示例:编写带参数的 Hello World 模块
下面我们创建一个完整的模块,它不仅在加载和卸载时向内核日志输出信息,还支持一个参数name,允许我们通过命令行传入自定义名称。
2.1 环境准备
首先,你需要一台 Linux 机器(物理机或虚拟机),并安装好对应内核版本的开发包。对于 Debian/Ubuntu:
sudo apt update sudo apt install build-essential linux-headers-$(uname -r)对于 CentOS/RHEL/Fedora:
sudo dnf install kernel-devel kernel-headers gcc make检查内核版本和头文件是否匹配:
uname -r ls /lib/modules/$(uname -r)/build2.2 编写模块代码
创建项目目录hello_module,在其中新建hello.c:
#include <linux/init.h> // module_init, module_exit #include <linux/module.h> // MODULE_LICENSE, MODULE_AUTHOR, module_param #include <linux/kernel.h> // printk // 声明模块参数,加载时可传入 static char *name = "World"; module_param(name, charp, S_IRUGO); MODULE_PARM_DESC(name, "The name to greet"); // 模块加载时执行 static int __init hello_init(void) { printk(KERN_INFO "Hello, %s! Module loaded.\n", name); return 0; // 返回0表示加载成功 } // 模块卸载时执行 static void __exit hello_exit(void) { printk(KERN_INFO "Goodbye, %s! Module unloaded.\n", name); } // 注册初始化与清理函数 module_init(hello_init); module_exit(hello_exit); // 模块元信息 MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name"); MODULE_DESCRIPTION("A simple hello world kernel module with parameter");代码说明:
-module_param(name, charp, S_IRUGO):定义一个字符串参数name,权限为只读(用户可查看但不能修改)。charp表示字符指针。
-printk(KERN_INFO "..."):内核日志打印,优先级KERN_INFO会被记录到环形缓冲区,可用dmesg查看。
-__init与__exit:编译提示,将函数放到特定段中,初始化完成后内存可被回收(仅对内置模块有效,但可编译时优化)。
-module_init/module_exit:宏指定初始化与退出函数入口。
2.3 编写 Makefile
在同一目录下创建Makefile:
# 模块名称(必须与源文件名对应,去除.c) obj-m += hello.o # 内核源码树路径,通常使用当前运行内核的构建目录 KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean注意:使用Tab缩进,$(MAKE)就是make,-C指定内核构建目录,M=$(PWD)让内核的 Makefile 回到当前目录编译外部模块。
2.4 编译模块
只需在项目目录下执行make:
make编译成功后,会生成一系列文件,其中hello.ko就是我们要加载的内核模块目标文件。使用modinfo查看模块信息:
modinfo hello.ko输出会显示文件名、版本、描述、作者、许可证以及我们定义的参数name及其描述。
2.5 加载与测试
加载模块需要 root 权限。可以使用默认参数加载,也通过insmod传入参数:
# 默认名称“World” sudo insmod hello.ko # 查看内核日志 dmesg | tail你会在输出中看到类似:Hello, World! Module loaded.
卸载模块:
sudo rmmod hello dmesg | tail输出会显示:Goodbye, World! Module unloaded.
现在尝试传入自定义参数:
sudo insmod hello.ko name="Linux" dmesg | tail -n 2日志中会变成Hello, Linux! Module loaded.。确认参数生效后,记得卸载模块。
三、进阶玩法:查看参数与动态修改(可选)
我们的参数权限是S_IRUGO(只读),用户可以通过 sysfs 查看该参数,路径在/sys/module/hello/parameters/name:
cat /sys/module/hello/parameters/name如果想支持模块加载后修改参数,可将权限设为S_IRUGO | S_IWUSR(即0644),并定义一个module_param_cb或使用set()回调函数,但需要注意并发问题。入门阶段暂不展开。
四、常见问题与注意事项
4.1 编译错误:缺少头文件或内核构建树
错误信息如make: *** /lib/modules/.../build: No such file or directory.
解决:安装与当前内核版本完全匹配的头文件包。内核版本可通过uname -r查看。
4.2 模块加载失败:Invalid module format或disagrees about version of symbol
通常是因为编译模块时的内核版本与当前运行内核不一致,或编译器版本差异。务必确保KDIR指向正确,且gcc版本与编译内核使用的相同。可使用modinfo hello.ko查看vermagic字段。
4.3printk信息未显示
printk的输出优先级可能低于控制台日志级别。使用dmesg查看全部日志,或调整/proc/sys/kernel/printk。- 在虚拟终端可能看不到,但
dmesg或journalctl -k一定会有。
4.4 模块卸载失败:Module is in use
模块可能正在被其他模块依赖,或打开了某个设备节点未释放。检查lsmod | grep hello的Used by列。若强行卸载可尝试rmmod -f,但可能导致不稳定。
4.5 内存泄漏与内核 oops
在内核模块中,任何内存分配(kmalloc)都必须有对应的释放(kfree),设备注册要有注销。否则卸载后再加载可能导致资源泄露或内核崩溃。养成“成对编程”的习惯:init 中每个操作都应在 exit 中有对应的清理。
4.6 避免使用浮点运算
内核空间不推荐使用浮点,因为内核通常配置为不保存/恢复浮点寄存器,这会影响性能且容易出错。模块里应完全使用整数运算。
五、总结
通过这篇教程,我们完成了一个带有参数的内核模块从编写到编译、加载、测试、卸载的全流程。你学到了:
- 内核模块的结构与核心概念;
- 使用
module_param传递参数; - Makefile 的编写规则;
- 常见问题的排查思路。
Linux 内核模块开发是一条充满挑战又极具魅力的道路。这只是万里长征的第一步,后续你可以尝试创建字符设备驱动、proc 文件接口、netfilter 钩子等。只要打好基础,循序渐进,内核的大门会为你敞开。
如果对过程中有任何疑问,欢迎在评论区留言交流。祝你编码愉快!