3、第一个内核模块:Hello World模块编写、Makefile编写、模块加载与卸载、printk调试

本章是Linux内核模块开发的起点。我们将从零编写一个最简单的“Hello World”内核模块,并配套编写标准的Kbuild Makefile,完成模块的编译、加载、查看内核日志以及卸载的全流程。通过本章,你将掌握内核模块的基本骨架和printk调试方法,为后续网卡驱动开发打下坚实基础。

3.1 内核模块的基本结构

一个标准的内核模块至少包含以下四个要素:

要素 宏/函数 说明
模块许可证声明 MODULE_LICENSE("GPL") 必须声明,否则内核会标记模块为“污染”
模块作者描述 MODULE_AUTHOR("Your Name") 可选,但推荐填写
模块初始化入口 module_init(hello_init) 模块加载时自动调用
模块卸载出口 module_exit(hello_exit) 模块卸载时自动调用

关键点: 内核模块运行在内核空间,不能直接调用C标准库函数(如printf)。内核提供的输出函数是printk,其日志通过dmesg查看。

3.2 Hello World模块代码实现

创建文件 hello.c,内容如下:

#include <linux/module.h>    // 包含 MODULE_* 宏
#include <linux/kernel.h>    // 包含 printk 和 KERN_INFO
#include <linux/init.h>      // 包含 module_init / module_exit

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello World: 模块已加载!\n");
    printk(KERN_INFO "当前内核版本: %s\n", UTS_RELEASE);
    return 0;   // 返回0表示初始化成功
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye World: 模块已卸载!\n");
}

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux网卡驱动开发65讲");
MODULE_DESCRIPTION("第一个内核模块 - Hello World");
MODULE_VERSION("1.0");

__init 和 __exit 标记: __init 告诉内核该函数仅在初始化阶段使用,初始化完成后可以释放占用的内存;__exit 标记在模块卸载时使用,如果模块被编译进内核(非动态加载),该函数会被忽略。

3.3 Makefile编写规范

内核模块的编译必须使用内核源码树提供的Kbuild系统。创建 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
Makefile变量 含义
obj-m 指定要编译成模块的目标文件(.o)
KDIR 内核源码树路径,指向当前运行内核的构建目录
M=$(PWD) 告诉Kbuild在指定目录下构建模块
modules Kbuild目标,编译所有obj-m指定的模块

常见错误: Makefile中$(MAKE)前面的缩进必须是Tab,不能是空格。否则make会报“missing separator”错误。

3.4 编译与加载卸载流程

执行以下命令完成完整流程:

# 1. 编译模块
make

# 2. 查看生成的模块文件
ls -l hello.ko

# 3. 加载模块(需要root权限)
sudo insmod hello.ko

# 4. 查看已加载模块列表
lsmod | grep hello

# 5. 查看内核日志
dmesg | tail -5

# 6. 卸载模块
sudo rmmod hello

# 7. 再次查看卸载日志
dmesg | tail -3

预期输出(dmesg日志):

[ 1234.567890] Hello World: 模块已加载!
[ 1234.567895] 当前内核版本: 5.15.0-91-generic
[ 5678.901234] Goodbye World: 模块已卸载!

3.5 printk日志级别详解

printk 是内核空间的“printf”,它通过日志级别控制消息的显示行为。常用日志级别如下:

宏 数值 含义 终端显示
KERN_EMERG 0 紧急事件,系统崩溃前 通常显示
KERN_ALERT 1 需要立即处理 通常显示
KERN_CRIT 2 严重条件 通常显示
KERN_ERR 3 错误条件 通常显示
KERN_WARNING 4 警告条件 可能显示
KERN_NOTICE 5 正常但重要 可能显示
KERN_INFO 6 信息性消息 不显示到终端
KERN_DEBUG 7 调试消息 不显示到终端

调试技巧: 在开发网卡驱动时,建议使用 KERN_INFO 或 KERN_DEBUG 级别,避免刷屏。通过 dmesg -w 可以实时跟踪内核日志输出。

3.6 内核模块加载与卸载流程图

下图展示了从编译到卸载的完整生命周期:

内核模块生命周期流程图 编写 hello.c + Makefile make 编译生成 hello.ko sudo insmod hello.ko 内核调用 hello_init() → printk 输出 sudo rmmod hello 内核调用 hello_exit() → printk 输出

3.7 常见问题与避坑指南

  • 权限问题: insmod 和 rmmod 需要 root 权限,使用 sudo 执行。
  • 内核版本不匹配: 编译模块的内核版本必须与运行内核版本一致,否则加载会报 Invalid module format 错误。
  • 符号冲突: 模块中的全局函数名不要与内核已有符号重名,建议使用 static 限制作用域。
  • printk 看不到输出: 检查是否使用了 KERN_INFO 级别,并确认通过 dmesg 查看而非终端。
  • 模块卸载失败: 如果 module_init 返回非0值,模块加载失败;如果模块被其他模块依赖,无法卸载。

生产建议: 在网卡驱动开发中,建议在 module_init 中注册网络设备,在 module_exit 中注销并释放资源。使用 printk(KERN_DEBUG ...) 输出调试信息,避免在生产环境中产生过多日志。

3.8 本章小结

通过本章,你已掌握:

  • 内核模块的标准骨架:module_init / module_exit / MODULE_LICENSE
  • Kbuild Makefile 的编写规则与编译流程
  • insmod / rmmod / lsmod / dmesg 命令的配合使用
  • printk 日志级别及其在调试中的应用