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"。
小提示:printk 的输出不会出现在终端上,而是写入内核日志缓冲区。想看输出,用 dmesg 命令或者查看 /var/log/kern.log。

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 的目标,表示编译可加载模块
注意:KDIR 路径必须存在。如果你没有安装内核头文件,先执行 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!
小技巧:我习惯在终端开两个窗口,一个跑 dmesg -w 实时监控内核日志,另一个执行 insmod/rmmod。这样能立刻看到 printk 的输出。

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!
注意:如果模块被其他模块引用(lsmod 第三列不为 0),rmmod 会失败,提示 "Module is in use"。这时候需要先卸载依赖它的模块。

3.4 模块生命周期与内核日志

一个内核模块从加载到卸载,经历四个阶段:

  1. 加载:insmod 调用 module_init 注册的函数
  2. 运行:模块代码常驻内核,可以被其他模块或系统调用访问
  3. 卸载准备:rmmod 调用 module_exit 注册的函数,释放资源
  4. 卸载完成:模块代码从内核中移除,内存释放

我曾经在一个项目中遇到过模块卸载时崩溃的问题——原因是 exit 函数里没有释放申请的资源,导致内核 panic。从那以后,我写模块都会在 exit 函数里仔细清理所有资源,哪怕只是测试代码。

3.5 本章知识结构图

下面这张图把模块开发、编译、加载、卸载的完整流程串起来了:

内核模块开发与加载流程 内核模块开发与加载流程 1. 编写 hello.c 2. 编写 Makefile 3. make 编译 产物:hello.ko 4. insmod hello.ko 5. lsmod / dmesg 6. rmmod hello 关键说明: • module_init / module_exit 是模块的入口和出口,必须注册 • printk 输出到内核日志,用 dmesg 查看 • MODULE_LICENSE 必须声明,否则内核标记为 tainted • 编译必须通过内核构建系统(Kbuild),不能直接用 gcc • rmmod 前确保模块没有被其他模块引用

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"

好了,到这里你已经亲手写完了第一个内核模块,也学会了怎么编译、加载、查看和卸载它。这些命令和流程,后面每一章都会用到。把它们练熟,后面的路就好走了。