前言:为什么是内核模块?
对于从事高性能网络开发(如 3A 系统、网关)的工程师来说,用户态(User Space)的优化往往有极限。当我们需要极致的性能时,我们就必须踏入**内核态(Kernel Space)**的领地。
Linux 内核模块(Loadable Kernel Module, LKM)是进入这一领域的敲门砖。它允许我们在不重新编译整个操作系统内核的情况下,动态地向内核添加功能。
本文将带你从零开始编写一个"Hello World"内核模块,并着重解决老旧内核版本无法下载开发依赖这一常见痛点。
第一部分:环境准备(最关键的一步)
编写内核模块不同于写普通的 C 语言程序。普通的 C 程序依赖 stdio.h 等标准库,而内核模块依赖当前运行内核的头文件(Kernel Headers)。
1. 检查环境
编译内核模块需要内核构建目录,通常位于 /lib/modules/$(uname -r)/build。
你可以通过以下命令检查:
bash
ls -l /lib/modules/$(uname -r)/build
如果显示 No such file or directory,说明你缺少内核开发包,必须安装。
2. 常规安装方法
如果是较新的系统,直接使用包管理器即可:
bash
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install linux-headers-$(uname -r)
# CentOS/RedHat
sudo yum install kernel-devel-$(uname -r)
3. 进阶故障排除:当 apt-get 找不到包时
很多时候(特别是云服务器或长期未重启的机器),我们会遇到以下报错:
E: Unable to locate package linux-headers-6.1.0-31-amd64
原因 :你运行的内核版本(如 6.1.0-31)较旧,而官方源(Repo)已经更新到了新版本(如 6.1.0-32),旧版本的头文件包被下架了。
解决方案:去 Debian Snapshot 仓库"捡漏"
我们需要手动下载对应的 .deb 包并安装。
步骤 A:下载依赖包
我们需要两个包:
- Common 包:包含通用的头文件。
- Arch 包:包含特定架构(如 amd64)的头文件。
前往 Debian Snapshot 或使用 wget 直接下载(注意:版本号需与 uname -r 严格匹配,以下以 6.1.0-31 为例):
bash
# 创建临时目录
mkdir -p ~/kheaders && cd ~/kheaders
# 1. 下载 Common 包 (请根据实际情况替换链接)
wget https://snapshot.debian.org/archive/debian/20250207T205318Z/pool/main/l/linux/linux-headers-6.1.0-31-common_6.1.128-1_all.deb
# 2. 下载架构专用包 (amd64)
wget https://snapshot.debian.org/archive/debian/20250207T205318Z/pool/main/l/linux/linux-headers-6.1.0-31-amd64_6.1.128-1_amd64.deb
由于我的系统使用wget直接下载不到对应的deb包,所有手动进入对应的发行版本链接进行下载的。



步骤 B:智能安装(使用 apt install ./xxx.deb)
技巧 :不要直接用 dpkg -i 安装,因为它处理不了依赖问题(比如可能缺 gcc 或 kbuild)。我们要用 apt 来安装本地文件,它会自动补齐缺少的依赖。
bash
# ./ 表示安装当前目录下的文件
sudo apt install ./linux-headers-*.deb
安装完成后,再次检查 /lib/modules/$(uname -r)/build,应该能看到蓝色的软链接,环境准备完毕!
第二部分:编写代码
创建一个工作目录 my_hello,我们需要两个文件。
1. 源代码:hello.c
内核编程与用户态编程有几个显著区别:
- 没有
main():使用初始化函数module_init。 - 没有
printf():使用内核日志函数printk()。 - 必须清理 :必须提供
module_exit函数,否则模块卸载后内存泄漏。
c
#include <linux/module.h> // 包含内核模块机制的定义
#include <linux/kernel.h> // 包含 printk 定义
#include <linux/init.h> // 包含 __init, __exit 宏
// 必须声明开源协议,否则加载时内核会报 "tainted" (污染) 警告,甚至拒绝加载
MODULE_LICENSE("GPL");
MODULE_AUTHOR("DevTeam");
MODULE_DESCRIPTION("A simple Hello World Kernel Module");
// 1. 模块加载函数 (入口)
// __init 宏告诉内核:这个函数只在初始化时用一次,用完可以回收内存
static int __init hello_init(void)
{
// KERN_INFO 是日志级别
printk(KERN_INFO "Hello, Kernel! 模块加载成功,开始工作。\n");
return 0; // 返回 0 表示成功,非 0 表示失败
}
// 2. 模块卸载函数 (出口)
// __exit 宏表示该代码仅用于卸载阶段
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel! 模块卸载完毕,下班回家。\n");
}
// 3. 注册宏
module_init(hello_init);
module_exit(hello_exit);
2. 编译脚本:Makefile
Linux 内核使用 Kbuild 构建系统。我们需要编写一个特殊的 Makefile。
⚠️ 注意:Makefile 中的缩进必须使用 Tab 键,不能用空格!
makefile
# obj-m 表示我们要把 hello.o 编译成模块 (module)
obj-m += hello.o
# 指向我们刚才辛苦配置好的内核构建目录
KDIR := /lib/modules/$(shell uname -r)/build
# 获取当前目录
PWD := $(shell pwd)
# 默认编译目标
all:
# -C 切换到内核目录,借用其环境
# M= 回到当前目录编译模块
make -C $(KDIR) M=$(PWD) modules
# 清理目标
clean:
make -C $(KDIR) M=$(PWD) clean
第三部分:编译与验证
1. 编译
在目录下直接运行 make:
bash
root@server:~/my_hello# make
make -C /lib/modules/6.1.0-31-amd64/build M=/root/my_hello modules
make[1]: Entering directory '/usr/src/linux-headers-6.1.0-31-amd64'
CC [M] /root/my_hello/hello.o
MODPOST /root/my_hello/Module.symvers
CC [M] /root/my_hello/hello.mod.o
LD [M] /root/my_hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-6.1.0-31-amd64'
如果没有报错,目录下会生成一个 hello.ko 文件。这就是编译好的内核模块。
第四部分:加载模块的两种姿势
姿势一:insmod (适合开发测试)
insmod (Install Module) 是最原始的加载方式,它需要指定 .ko 文件的具体路径。它不解决依赖,指哪打哪。
操作步骤:
-
加载模块 :
bashsudo insmod hello.ko -
查看日志 :
内核日志不会直接打印在终端,需要查看 Ring Buffer。bashdmesg | tail输出:
[ ... ] Hello, Kernel! 模块加载成功,开始工作。 -
卸载模块 :
bashsudo rmmod hello(再次查看 dmesg 会看到 "Goodbye..." 的日志)
关于 "Tainted Kernel" 警告 :
如果
dmesg出现loading out-of-tree module taints kernel,请不用惊慌。这只是内核在发牢骚:"你加载了一个非官方发布的模块,出了问题我不负责。" 只要没报 Error,模块就是正常运行的。
姿势二:modprobe (适合生产部署)
如果你尝试运行 modprobe hello,你会发现报错:
modprobe: FATAL: Module hello not found in directory ...
这是因为 modprobe 是通过读取系统的模块索引数据库 来工作的,它不知道你的临时目录里有个 hello.ko。
如何让 modprobe 生效?
-
归档:将模块复制到系统的标准模块库中。
bashsudo mkdir -p /lib/modules/$(uname -r)/kernel/drivers/misc/ sudo cp hello.ko /lib/modules/$(uname -r)/kernel/drivers/misc/ -
更新索引:告诉系统重新扫描一遍所有模块。
bashsudo depmod -a -
加载 :
现在,你可以在任何目录下直接加载它了,且不需要加
.ko后缀。bashsudo modprobe hello
modprobe 的优势:
- 自动解决依赖 :如果模块 A 依赖模块 B,
modprobe A会自动先把 B 加载进来。 - 无需路径:不需要知道文件具体在哪。
总结
通过本文,我们完成了一次完整的内核模块开发流程:
- 环境 :通过下载 Snapshot 的 deb 包并用
apt安装,解决了老旧内核缺失头文件的问题。 - 代码 :编写了标准的
module_init/module_exit结构。 - 编译 :利用 Kbuild 系统和 Makefile 生成了
.ko文件。 - 运行 :区分了
insmod(开发用)和modprobe(生产用)的区别。