当前位置: 首页 > news >正文

Linux内核模块开发从零到一:编写你的第一个内核模块,超详细教程

引言

Linux 内核模块是扩展内核功能的强大机制,它允许我们在不重新编译内核、不重启系统的情况下,动态添加或移除功能。无论是设备驱动、文件系统,还是安全策略,都可以通过模块的形式灵活加载。对于想深入 Linux 内核世界的开发者来说,编写一个简单的内核模块是最好的起点。

本文将从环境准备讲起,带你理解内核模块的核心概念,然后一步步编写一个带有模块参数的 Hello World 模块,并完成编译、加载、测试与卸载的全过程。即使你之前从未接触过内核代码,也能跟着完成第一个可运行的模块。

一、核心概念:什么是内核模块

内核模块是一段可以动态链接到运行中内核的二进制代码。它与普通用户态程序有本质区别:

  • 运行空间:模块运行在内核空间(Ring 0),可以直接访问硬件和内核数据结构,而用户程序运行在用户空间(Ring 3)。
  • 内存管理:模块没有独立的地址空间,它共享内核的地址空间。因此,内存错误可能导致整个系统崩溃。
  • 函数库:模块只能调用内核导出的函数(如printk()),不能使用标准 C 库(如printf())。
  • 并发与中断:模块代码可能被多个进程并发执行,也可能在中断上下文中运行,因此编写时必须考虑同步与原子操作。

每个模块至少需要两个关键函数:
-初始化函数:在模块被加载时调用(insmodmodprobe),通常完成资源申请、注册设备等。
-清理函数:在模块被卸载时调用(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)/build

2.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 formatdisagrees about version of symbol

通常是因为编译模块时的内核版本与当前运行内核不一致,或编译器版本差异。务必确保KDIR指向正确,且gcc版本与编译内核使用的相同。可使用modinfo hello.ko查看vermagic字段。

4.3printk信息未显示

  • printk的输出优先级可能低于控制台日志级别。使用dmesg查看全部日志,或调整/proc/sys/kernel/printk
  • 在虚拟终端可能看不到,但dmesgjournalctl -k一定会有。

4.4 模块卸载失败:Module is in use

模块可能正在被其他模块依赖,或打开了某个设备节点未释放。检查lsmod | grep helloUsed by列。若强行卸载可尝试rmmod -f,但可能导致不稳定。

4.5 内存泄漏与内核 oops

在内核模块中,任何内存分配(kmalloc)都必须有对应的释放(kfree),设备注册要有注销。否则卸载后再加载可能导致资源泄露或内核崩溃。养成“成对编程”的习惯:init 中每个操作都应在 exit 中有对应的清理。

4.6 避免使用浮点运算

内核空间不推荐使用浮点,因为内核通常配置为不保存/恢复浮点寄存器,这会影响性能且容易出错。模块里应完全使用整数运算。

五、总结

通过这篇教程,我们完成了一个带有参数的内核模块从编写到编译、加载、测试、卸载的全流程。你学到了:

  • 内核模块的结构与核心概念;
  • 使用module_param传递参数;
  • Makefile 的编写规则;
  • 常见问题的排查思路。

Linux 内核模块开发是一条充满挑战又极具魅力的道路。这只是万里长征的第一步,后续你可以尝试创建字符设备驱动、proc 文件接口、netfilter 钩子等。只要打好基础,循序渐进,内核的大门会为你敞开。

如果对过程中有任何疑问,欢迎在评论区留言交流。祝你编码愉快!

http://www.jsqmd.com/news/1159090/

相关文章:

  • 基于MVVM框架的XUUI HelloWorld 新手教程
  • 跟大模型对话,如何让提示词控制力更强?从随缘输出到精准可控完整指南
  • 无需安装WPS 和 Office!开源 OfficeCLI,Claude/Copilot 可直接调用,自动生成编辑Word/PPT/Excel
  • 74LS151 vs 74LS138 数据手册精读:5 个关键参数对比与 3 种典型应用电路
  • 高校后勤精细化革新:智能锁赋能校园安防管控与节能运维升级
  • Calibre繁简中文转换插件:让全球中文电子书无障碍阅读
  • 九号控制器二次开发全解析:从协议逆向到功能实现
  • 信创框架 IIAF-ADM 架构开发方法:3个核心步骤与2大策略实践解析
  • OpenCode嵌入式AI开发环境:离线本地化智能编码底座
  • UC3842 反激电源设计实战:3步完成RT/CT振荡器与反馈环路参数计算
  • 海外留学安全必备:欧美/日本/东南亚留学防身装备合法携带指南
  • AI构建AI:从自动化工作流到Agent自主开发的工程实践
  • 轴向磁通电机技术解析:结构突破、功率密度优势与电动车应用前景
  • S7-200 SMART V2.6 Web服务器HTTPS配置:3步完成内部证书生成与PC端安装
  • 3100份各大庆典的司仪主持人台词大全,2026司仪主持人台词大全——司仪主持人台词大全下载
  • 工程师转型技术合规:AI赋能开发全流程的实战指南
  • Day 006:别被概念忽悠了,Agent 的核心就是个 While 循环
  • AI真正进入企业,是从开始承担责任那一天
  • OpenAI API调用实战:构建跨境支付AI智能验证的技术方案
  • Cursor 2.6.22 Agents 不是开关,而是运行时契约
  • Unity UGUI软遮罩实现原理与性能优化指南
  • 51单片机定时器T0 12MHz晶振:8音阶频率表生成与C51代码实现
  • 基于TS2007FC与STM32F745VG的高保真音频系统设计
  • 火爆了!Nginx常用命令大全
  • Fake UserAgent 2.2.0 高级用法:3种过滤策略精准生成指定浏览器UA
  • HyperFrames关键帧开源:AI视频生成从盲盒到可控编辑的突破
  • 河北xx集团考核系统操作手册
  • JavaScript ES6 模块导入 5 大常见 SyntaxError 排查:从 Unexpected identifier 到 MIME 类型
  • 计算机毕业设计选题怎么选?完整挑选指南,轻松搞定毕业设计
  • 独立开发30天,我用AI做了个发型推荐小程序(含全部踩坑记录)