2. 第一个内核模块 Hello World:编写最简单的模块代码、Makefile 基础结构、编译与加载测试

本章是驱动开发实战的起点。我们将从零编写一个最简单的内核模块,它不操作硬件、不注册设备,只在加载和卸载时打印一条消息。通过这个最小实例,你将掌握内核模块的代码骨架、Makefile 的编写规则,以及编译、加载、卸载、查看日志的完整流程。

学习目标:完成本章后,你能够独立编写一个可编译、可加载、可卸载的内核模块,并理解模块的入口/出口函数、许可证声明、Makefile 中 obj-m 与 KERNELDIR 的作用。

2.1 内核模块的最小代码结构

一个 Linux 内核模块至少需要两个函数:模块初始化函数(加载时调用)和模块退出函数(卸载时调用)。此外,还需要使用 module_init() 和 module_exit() 宏将这两个函数注册到内核模块框架中。

下面是最简的 hello.c 代码:

#include <linux/module.h>      // 包含模块相关的宏和函数
#include <linux/kernel.h>      // 包含 printk 等内核打印函数
#include <linux/init.h>        // 包含 __init 和 __exit 标记宏

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello World from kernel module!\n");
    return 0;   // 返回 0 表示初始化成功
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");                  // 许可证声明,必须
MODULE_AUTHOR("Your Name");             // 作者信息(可选)
MODULE_DESCRIPTION("A simple Hello World module"); // 描述(可选)
MODULE_VERSION("1.0");                  // 版本号(可选)
关键点解析:
  • __init 和 __exit 是内核标记宏,告诉内核该函数在初始化/退出后可以释放内存,减少运行时开销。
  • printk 是内核空间的打印函数,KERN_INFO 指定日志级别,消息会写入内核日志缓冲区,可通过 dmesg 查看。
  • MODULE_LICENSE("GPL") 是必须的,缺少此声明会导致模块加载时内核报“tainted kernel”警告,且某些内核符号无法使用。

2.2 Makefile 基础结构

内核模块的编译不能使用普通的 gcc 命令,必须借助内核的构建系统(Kbuild)。Makefile 的核心任务是告诉 Kbuild 要编译哪个源文件,以及去哪里找内核头文件和编译规则。

下面是与 hello.c 配套的 Makefile:

# 指定要编译的目标模块名称(不带 .ko 后缀)
obj-m := hello.o

# 指定当前内核源码树的路径(根据实际环境修改)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build

# 指定当前源码目录
PWD := $(shell pwd)

# 默认目标:编译模块
all:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

# 清理目标:清除编译产物
clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
注意:Makefile 中的缩进必须使用 Tab 键,不能使用空格。这是 Make 语法的硬性要求,否则会报“missing separator”错误。

Makefile 各部分的含义如下表所示:

变量/目标 作用 说明
obj-m := hello.o 指定要编译成模块的目标文件 Kbuild 会根据此变量将 hello.c 编译为 hello.ko
KERNELDIR 内核源码树路径 /lib/modules/$(uname -r)/build 是发行版安装内核头文件后的标准路径
PWD 当前工作目录 传递给 Kbuild,告诉它源文件在哪里
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules 调用内核构建系统编译模块 -C 进入内核目录,M= 指定模块源码路径

2.3 编译与加载测试流程

将 hello.c 和 Makefile 放在同一个目录下,执行以下步骤:

  1. 编译模块:在终端执行 make,成功后会生成 hello.ko、hello.o、hello.mod.c 等文件。
  2. 加载模块:使用 sudo insmod hello.ko 将模块插入内核。
  3. 查看模块信息:lsmod | grep hello 确认模块已加载;modinfo hello.ko 查看模块的许可证、作者、描述等信息。
  4. 查看内核日志:执行 dmesg | tail -5,应看到 Hello World from kernel module! 消息。
  5. 卸载模块:sudo rmmod hello,再次查看 dmesg 应看到 Goodbye, kernel module unloaded. 消息。
  6. 清理编译产物:make clean 删除 .o、.ko、.mod.c 等临时文件。
常见问题:如果 dmesg 没有输出,可能是日志级别被过滤。尝试 dmesg -l info 或 dmesg -l 6 查看 info 级别的消息。如果模块加载失败,使用 dmesg 查看内核给出的具体错误原因。

2.4 模块编译与加载流程 SVG 图

下图展示了从源代码到内核模块加载的完整流程:

内核模块编译与加载流程 内核模块编译与加载流程 hello.c Makefile Kbuild 构建系统 hello.ko sudo insmod 内核空间(运行中) dmesg 查看日志 sudo rmmod 执行 hello_exit() 说明: 1. 编译阶段:hello.c + Makefile → Kbuild → hello.ko 2. 加载阶段:insmod 将 hello.ko 送入内核空间,执行 hello_init(),日志通过 dmesg 查看

2.5 模块信息查看与常见错误

加载模块后,可以使用以下命令获取模块的详细信息:

  • lsmod:列出当前系统中所有已加载的模块,包括模块大小、被其他模块引用的次数。
  • modinfo hello.ko:显示模块的 license、author、description、依赖关系、参数等信息。
  • cat /proc/modules | grep hello:查看模块在内核中的内存地址和状态。

初学者常遇到的错误及解决方法:

错误现象 可能原因 解决方法
make: *** /lib/modules/.../build: No such file or directory 未安装内核头文件或内核开发包 执行 sudo apt install linux-headers-$(uname -r)(Ubuntu/Debian)或 sudo yum install kernel-devel(RHEL/CentOS)
insmod: ERROR: could not insert module hello.ko: Operation not permitted 未使用 root 权限 使用 sudo insmod hello.ko
insmod: ERROR: could not insert module hello.ko: Invalid module format 模块编译时的内核版本与当前运行内核版本不一致 确保 KERNELDIR 指向当前运行内核的源码树,重新编译
dmesg 无输出 日志级别被过滤,或 printk 未正确调用 尝试 dmesg -l info 或 dmesg -T | tail -10

2.6 本章小结

通过本章的学习,你已经完成了第一个内核模块的编写、编译和加载测试。这个最小实例虽然简单,但它包含了内核模块开发的所有核心要素:

  • 代码骨架:module_init / module_exit 宏、__init / __exit 标记、许可证声明。
  • Makefile 结构:obj-m 变量、KERNELDIR 路径、make -C 调用 Kbuild。
  • 编译与测试流程:make → insmod → dmesg → rmmod → make clean。