Linux内核模块开发从零到一:编写你的第一个内核模块,超详细教程
引言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/Ubuntusudo apt update sudo apt install build-essential linux-headers-$(uname -r)对于 CentOS/RHEL/Fedorasudo 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 编译模块只需在项目目录下执行makemake编译成功后会生成一系列文件其中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 nameLinux dmesg | tail -n 2日志中会变成Hello, Linux! Module loaded.。确认参数生效后记得卸载模块。三、进阶玩法查看参数与动态修改可选我们的参数权限是S_IRUGO只读用户可以通过 sysfs 查看该参数路径在/sys/module/hello/parameters/namecat /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 钩子等。只要打好基础循序渐进内核的大门会为你敞开。如果对过程中有任何疑问欢迎在评论区留言交流。祝你编码愉快