2、第一个内核模块:Hello World模块编写、Makefile编写、模块编译与加载(insmod/rmmod)、查看模块信息(lsmod/modinfo)
说实话,学内核开发最怕什么?就是环境还没搭好,先被一堆概念砸晕了。
所以我个人习惯,第一件事就是先让代码跑起来。哪怕只是打印一句 "Hello World",也能让你对整个流程有个直观感受。今天我们就来干这件事。
2.1 内核模块的骨架
内核模块和用户态程序不一样。它没有 main 函数。它的入口和出口是固定的两个宏:module_init 和 module_exit。
来看一个最简的模块代码。我把它命名为 hello.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.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");
这里有几个关键点,我当年第一次写的时候踩过坑:
- printk 不是 printf。它没有格式化占位符的检查,写错了也不会报编译警告,但运行时会直接崩。嗯,这个我吃过亏。
- KERN_INFO 是日志级别。不加的话,默认级别可能不会显示在终端上。
- __init 和 __exit 是内核的优化标记。加了 __init 的函数,在模块加载完成后会被内核自动释放掉,省内存。
- MODULE_LICENSE 必须写。不写的话,内核会报 "tainted" 警告,而且某些内核功能(比如一些 GPL-only 的符号)你就用不了。
"hello: loaded"。这样 dmesg 里一搜就能定位到自己的日志,不会跟内核其他日志混在一起。
2.2 Makefile 怎么写?
内核模块的编译不靠 gcc 直接编译。你得用内核的构建系统——Kbuild。说白了,就是让内核源码树里的 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 的模块。注意这里写的是 .o,但生成的是 .ko。
- KDIR:指向当前运行内核的构建目录。这个目录包含了编译模块所需的所有头文件和 Makefile 片段。
- -C $(KDIR):切换到内核源码目录去执行 make。
- M=$(PWD):告诉内核 Makefile,我的模块源码在当前目录。
/lib/modules/$(uname -r)/build。否则加载时会报 "Invalid module format" 错误。我曾经在交叉编译环境上折腾了一下午,才发现是 KDIR 指错了。
2.3 编译与加载
在终端里执行 make,你会看到类似这样的输出:
make -C /lib/modules/5.15.0-91-generic/build M=/home/user/hello modules
CC [M] /home/user/hello/hello.o
MODPOST /home/user/hello/Module.symvers
CC [M] /home/user/hello/hello.mod.o
LD [M] /home/user/hello/hello.ko
编译成功后,目录下会多出 hello.ko 文件。这就是我们要的内核模块。
加载和卸载的命令很简单:
| 操作 | 命令 | 说明 |
|---|---|---|
| 加载模块 | sudo insmod hello.ko |
需要 root 权限。insmod 是 "insert module" 的缩写。 |
| 卸载模块 | sudo rmmod hello |
注意这里写的是模块名 hello,不是文件名 hello.ko。 |
| 查看加载日志 | dmesg | tail |
printk 的输出会写到内核日志缓冲区。 |
加载后,用 dmesg | tail 你应该能看到:
[ 1234.567890] Hello, Kernel World!
卸载后再看:
[ 1235.678901] Goodbye, Kernel World!
2.4 查看模块信息
模块加载后,怎么确认它真的在运行?两个命令就够了。
lsmod:列出当前加载的模块
lsmod 实际上就是读取 /proc/modules 文件的内容。输出格式如下:
Module Size Used by
hello 16384 0
...
- Module:模块名。
- Size:模块占用的内存大小(字节)。
- Used by:被多少个其他模块引用。0 表示当前没有被使用。
modinfo:查看模块的详细信息
这个命令读取的是模块文件里的 .modinfo 段。即使模块没加载,也能查看:
$ modinfo hello.ko
filename: /home/user/hello/hello.ko
license: GPL
author: Your Name
description: A simple Hello World kernel module
depends:
retpoline: Y
name: hello
vermagic: 5.15.0-91-generic SMP mod_unload modversions
这里有个字段值得注意:vermagic。它记录了编译时内核的版本和配置。如果加载时内核版本不匹配,insmod 会直接拒绝加载,并报 "Invalid module format"。这是内核的一种保护机制。
2.5 知识结构图
下面这张图把整个流程串起来了。从源码到加载运行,每一步都对应一个文件或命令:
2.6 避坑指南
最后,把我这些年遇到过的几个典型问题列出来,你碰到了可以直接对照:
| 错误现象 | 原因 | 解决办法 |
|---|---|---|
Invalid module format |
模块编译时的内核版本与当前运行内核不一致 | 检查 KDIR 是否指向正确的内核源码树 |
Required key not available |
内核启用了模块签名验证 | 编译内核时关闭 CONFIG_MODULE_SIG,或给模块签名 |
Unknown symbol |
模块引用了未导出的内核符号 | 检查是否缺少 EXPORT_SYMBOL,或依赖的模块未加载 |
rmmod: ERROR: Module hello is in use |
有其他模块或进程正在引用该模块 | 用 lsmod 查看 Used by 列,先卸载依赖它的模块 |
CONFIG_PREEMPT 配置不一样。vermagic 里会记录这些配置,差一点都不行。所以,模块一定要在目标机器上编译,或者用相同的内核配置交叉编译。
好了,到这里你已经亲手写了一个内核模块,并且成功加载和卸载了它。虽然只是打印了两行日志,但整个流程——编写、编译、加载、查看、卸载——你已经全部走通了。后面所有的模块开发,都是在这个基础上加功能而已。