3. 第一个内核模块:Hello World 模块的编写、Makefile 构建、insmod/rmmod/lsmod 命令详解
说实话,学内核开发最让人头疼的就是环境搭建和第一个例子的跑通。我当年第一次写内核模块,光是编译就折腾了大半天——不是头文件找不到,就是符号表对不上。所以这一章,咱们就稳扎稳打,把 Hello World 模块从零写出来,再把它装进内核、卸下来,顺便把几个管理命令摸透。
3.1 内核模块的骨架:最少代码长什么样
内核模块跟用户态程序不一样。它没有 main() 函数,入口和出口是固定的两个宏。说白了,内核模块就是一段可以在运行时动态插入内核的代码片段。
来看一个最简的 Hello World 模块:
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Kernel World!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel World!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module");
这里有几个关键点,我挨个说一下:
- module_init / module_exit:这两个宏告诉内核,模块加载时执行哪个函数,卸载时执行哪个函数。名字可以随便起,但必须用这两个宏注册。
- __init / __exit:这两个标记是给内核优化用的。__init 标记的函数在模块加载完成后会被内核丢弃,释放内存。__exit 标记在模块编译进内核(非模块)时会被忽略。
- printk:内核版的 printf。KERN_INFO 是日志级别,表示信息性消息。你可以在 dmesg 里看到输出。
- MODULE_LICENSE:这个必须写。不写的话,内核会报“tainted kernel”警告。我个人习惯用 "GPL"。
3.2 Makefile 构建:别用手敲编译命令
内核模块的编译跟普通 C 程序完全不同。你不能直接用 gcc 去编译,必须借助内核的构建系统(Kbuild)。
我见过不少新手直接写 gcc -c hello.c,然后报一堆找不到头文件的错误。嗯,这里要注意:内核模块的编译需要内核源码树或者内核头文件包。
标准 Makefile 长这样:
# Makefile
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
解释一下每一行的作用:
| 行 | 含义 |
|---|---|
| obj-m := hello.o | 告诉 Kbuild 要编译一个名为 hello.ko 的模块,源文件是 hello.c |
| KDIR | 指向当前运行内核的构建目录。这个目录包含了编译模块所需的所有头文件和 Makefile 片段 |
| M=$(PWD) | 指定模块源码所在的目录。Kbuild 会进入这个目录编译 |
| modules | Kbuild 的目标,表示编译可加载模块 |
sudo apt-get install linux-headers-$(uname -r)(Ubuntu/Debian)或 sudo yum install kernel-devel(RHEL/CentOS)。
编译方法很简单:
$ make
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
make[1]: Entering directory '/usr/src/linux-headers-5.15.0-91-generic'
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
LD [M] /home/user/hello/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-5.15.0-91-generic'
编译成功后,你会看到生成了 hello.ko 文件。这就是内核模块的二进制文件。
3.3 insmod / rmmod / lsmod:三板斧
模块编译好了,怎么装进去?三个命令搞定。
3.3.1 insmod:插入模块
$ sudo insmod hello.ko
这条命令把 hello.ko 加载进内核。如果没有任何输出,说明加载成功。如果有错误,会直接打印到终端。
加载后,你可以用 dmesg 查看内核日志:
$ dmesg | tail
[ 1234.567890] Hello, Kernel World!
3.3.2 lsmod:列出模块
$ lsmod | grep hello
hello 16384 0
输出有三列:模块名、占用的内存大小、被多少个其他模块引用。第三列是 0 表示当前没有其他模块依赖它。
lsmod 其实读的是 /proc/modules 文件。你也可以直接 cat 这个文件:
$ cat /proc/modules | grep hello
hello 16384 0 - Live 0x0000000000000000
多出来的几列分别是:加载状态(Live)、内核版本标记、模块在内存中的加载地址。
3.3.3 rmmod:移除模块
$ sudo rmmod hello
移除后,dmesg 里会看到:
[ 1235.678901] Goodbye, Kernel World!
3.4 模块生命周期与内核日志
一个内核模块从加载到卸载,经历四个阶段:
- 加载:insmod 调用 module_init 注册的函数
- 运行:模块代码常驻内核,可以被其他模块或系统调用访问
- 卸载准备:rmmod 调用 module_exit 注册的函数,释放资源
- 卸载完成:模块代码从内核中移除,内存释放
我曾经在一个项目中遇到过模块卸载时崩溃的问题——原因是 exit 函数里没有释放申请的资源,导致内核 panic。从那以后,我写模块都会在 exit 函数里仔细清理所有资源,哪怕只是测试代码。
3.5 本章知识结构图
下面这张图把模块开发、编译、加载、卸载的完整流程串起来了:
3.6 常见问题与避坑指南
写第一个模块时,有几个坑我几乎每次培训都会遇到:
- 编译报错:找不到 linux/module.h——没装内核头文件。执行 apt-get install linux-headers-$(uname -r)。
- insmod 报错:Invalid module format——模块编译时的内核版本与当前运行内核版本不一致。检查 uname -r 和 KDIR 路径。
- insmod 报错:Operation not permitted——没加 sudo。加载模块需要 root 权限。
- rmmod 报错:Module is in use——有进程或其他模块在使用它。用 lsmod 查看引用计数,先卸载依赖模块。
- dmesg 看不到输出——printk 的日志级别可能被控制台过滤了。试试 dmesg | tail 或者 cat /var/log/kern.log。
核心要点回顾:
- 内核模块没有 main(),用 module_init / module_exit 注册入口和出口
- 编译必须用内核构建系统,Makefile 中的 KDIR 指向内核头文件目录
- insmod 加载模块,rmmod 卸载模块,lsmod 查看已加载的模块
- printk 输出到内核日志,用 dmesg 查看
- MODULE_LICENSE 必须声明,推荐使用 "GPL"
好了,到这里你已经亲手写完了第一个内核模块,也学会了怎么编译、加载、查看和卸载它。这些命令和流程,后面每一章都会用到。把它们练熟,后面的路就好走了。