Linux 内核模块(Loadable Kernel Module,LKM)是 Linux 系统最具灵活性的设计之一。它允许我们在不重新编译内核、不重启系统的前提下,动态地向内核添加功能。无论是设备驱动、文件系统还是网络协议,都可以通过模块的形式实现。本文将从零开始,带你编写、编译并加载一个最简单的内核模块,理解其背后的机制。
一、内核模块是什么
内核模块是一段可以在运行时被动态加载到内核空间的目标代码。加载后,它便成为内核的一部分,拥有与内核代码同等的权限——可以直接访问硬件、操作内核数据结构、注册回调函数。
与用户态程序不同,内核模块有几个显著特点:
- 运行在内核空间:没有内存保护,一个空指针解引用就可能导致整个系统崩溃(俗称 kernel panic)。
- 没有标准库可用:无法使用 printf、malloc 等用户态函数,只能使用内核提供的 API,如 printk、kmalloc。
- 有明确的入口和出口:每个模块都必须定义初始化和清理函数。
正是这些限制,使得编写内核模块需要格外谨慎,但也正是它赋予了开发者直接操控系统的能力。
二、准备工作
在开始之前,你需要确保系统具备编译内核模块的条件。以 Ubuntu/Debian 为例:
sudo apt update
sudo apt install build-essential linux-headers-$(uname -r)
其中 linux-headers-$(uname -r) 会安装与当前运行内核版本匹配的头文件,这是编译模块的必要依赖。你可以通过以下命令验证:
ls /lib/modules/$(uname -r)/build
如果该目录存在且包含 Makefile,说明环境就绪。
三、编写第一个模块
创建一个工作目录,并新建文件 hello_module.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple hello world kernel module");
MODULE_VERSION("1.0");
static int __init hello_init(void)
{
printk(KERN_INFO "hello_module: loaded successfully\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "hello_module: unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
逐段解析这段代码:
- 头文件:
linux/init.h提供module_init和module_exit宏;linux/module.h包含模块相关的宏定义;linux/kernel.h提供printk等内核函数。 - MODULE_LICENSE:声明许可证。使用 GPL 可以让模块访问内核中仅对 GPL 代码开放的内核符号,缺少这一行会在加载时收到内核污染警告。
- hello_init:模块加载时调用的函数,返回 0 表示成功,返回负值表示失败并阻止加载。
__init标记告诉内核该函数仅在初始化阶段使用,之后可以释放其占用的内存。 - hello_exit:模块卸载时调用,负责清理资源。
- module_init / module_exit:将上述两个函数注册为模块的入口和出口。
四、编写 Makefile
内核模块的编译方式与普通程序不同,需要借助内核的构建系统。在同一目录下创建 Makefile:
obj-m += hello_module.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
关键点说明:
obj-m += hello_module.o告诉内核构建系统将这个目标编译为可加载模块(.ko 文件)。-C $(KDIR)切换到内核源码目录,使用内核自身的 Makefile。M=$(PWD)指明模块源码所在的外部目录。- 注意命令前必须是 Tab 缩进,而非空格,这是 Makefile 的硬性要求。
五、编译与加载
执行编译:
make
如果一切顺利,你会看到 hello_module.ko 文件生成。接下来加载模块:
sudo insmod hello_module.ko
查看内核日志,确认模块已加载:
dmesg | tail -n 5
你应该能看到 hello_module: loaded successfully 的输出。用 lsmod 也可以看到模块已被列入内核模块列表:
lsmod | grep hello
卸载模块:
sudo rmmod hello_module
再次查看 dmesg,会看到 hello_module: unloaded。
六、深入理解加载过程
当你执行 insmod 时,内核实际完成了以下工作:
- 读取 .ko 文件:这是一个 ELF 格式的目标文件,包含代码段、数据段以及模块元信息。
- 分配内核内存:将模块代码和数据复制到内核空间。
- 符号解析:解析模块引用的外部符号(如 printk),将其绑定到内核中的实际地址。
- 调用初始化函数:执行
module_init注册的函数。 - 注册到模块链表:模块成为内核的一部分,出现在
/proc/modules和lsmod输出中。
rmmod 则执行相反的过程,调用清理函数并释放内存。
七、常见问题与注意事项
- printk 日志级别:
KERN_INFO只是日志级别之一。如果dmesg看不到输出,可能是控制台日志级别过滤所致,可通过dmesg -n 8调整。 - 模块版本不匹配:如果头文件版本与运行内核不一致,加载时会报
invalid module format错误。 - 内核污染:未声明 GPL 许可证的模块会标记内核为“污染”状态,在生产环境中应避免。
- 安全性:内核模块拥有最高权限,错误代码可能导致系统崩溃或安全漏洞,务必在虚拟机或测试环境中先行验证。
八、小结
本文从零完成了一个 Linux 内核模块的编写、编译、加载和卸载全过程。虽然这个模块只做了一件事——打印一行日志,但它涵盖了内核模块开发的所有基本要素:入口/出口函数、模块元信息、内核构建系统以及动态加载机制。
掌握这些基础后,你可以进一步探索更复杂的主题,例如字符设备驱动、procfs/sysfs 接口、内核定时器、中断处理等。内核模块是理解 Linux 内核运作方式的一把钥匙,而今天你已經拿到了它。
未经允许不得转载:任鹏个人博客 » 从零构建一个 Linux 内核模块并动态加载


朋友圈点赞图在线生成源码