4. 第一个驱动模块:Hello World 模块的编写、module_init 与 module_exit、模块加载与卸载
本章将带领你从零编写第一个 Linux 内核驱动模块——hello_world。通过这个最小化的模块,你将掌握驱动模块的基本骨架、内核模块的入口与出口机制,以及模块的加载与卸载流程。这是所有字符设备驱动开发的起点。
4.1 内核模块的基本结构
一个 Linux 内核模块本质上是一个可以在运行时动态加载到内核中的目标文件(.ko)。与用户态程序不同,内核模块没有 main() 函数,而是通过两个关键宏来定义模块的初始化和退出行为:
| 宏 / 函数 | 作用 | 调用时机 |
|---|---|---|
module_init() |
指定模块加载时执行的初始化函数 | insmod 时自动调用 |
module_exit() |
指定模块卸载时执行的清理函数 | rmmod 时自动调用 |
MODULE_LICENSE() |
声明模块许可证(必须) | 编译时嵌入 |
MODULE_AUTHOR() |
声明模块作者(可选) | 编译时嵌入 |
MODULE_DESCRIPTION() |
声明模块描述(可选) | 编译时嵌入 |
关键概念:
module_init 和 module_exit 是内核模块的生命周期锚点。没有它们,内核无法知道模块的入口和出口在哪里,模块将无法正常加载或卸载。
4.2 Hello World 模块完整代码
下面是一个最简的 hello_world 内核模块。它会在加载时打印一条内核日志,卸载时打印另一条日志。
/* hello_world.c */
#include <linux/module.h> /* 所有模块都需要 */
#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");
return 0; /* 返回 0 表示初始化成功 */
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "再见!模块已卸载。\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Linux驱动开发实战");
MODULE_DESCRIPTION("一个简单的 Hello World 内核模块");
代码要点:
__init和__exit是内核属性标记,告诉内核这些函数在初始化/退出后可以释放内存。printk是内核空间的打印函数,KERN_INFO指定日志级别。- 初始化函数必须返回
int,成功返回 0,失败返回负的错误码。 - 退出函数返回类型为
void。
4.3 模块加载与卸载流程
下图展示了从用户态执行 insmod 和 rmmod 命令时,内核模块的生命周期流转:
4.4 编译与测试步骤
要编译内核模块,你需要一个 Makefile。以下是标准的模块编译方法:
# Makefile
obj-m += hello_world.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
重要提示: 编译内核模块需要安装内核头文件。在 Ubuntu/Debian 上使用
sudo apt install linux-headers-$(uname -r) 安装。如果头文件版本与当前运行内核不匹配,模块将无法加载。
编译和测试的完整步骤:
- 编译模块: 在源码目录执行
make,生成hello_world.ko。 - 加载模块:
sudo insmod hello_world.ko。无输出表示成功。 - 查看日志:
dmesg | tail -5,应看到 "Hello World! 模块已加载。"。 - 查看已加载模块:
lsmod | grep hello_world,确认模块在列表中。 - 卸载模块:
sudo rmmod hello_world。 - 再次查看日志:
dmesg | tail -5,应看到 "再见!模块已卸载。"。
4.5 常见错误与排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
insmod: ERROR: could not insert module |
内核版本不匹配 / 缺少许可证 | 检查 uname -r 与头文件版本;确保有 MODULE_LICENSE("GPL") |
Unknown symbol |
模块依赖的内核符号未导出 | 检查是否使用了未导出的函数或变量 |
rmmod: ERROR: Module hello_world is in use |
有其他进程或模块正在使用该模块 | 使用 lsmod 查看引用计数,先释放依赖 |
加载后 dmesg 无输出 |
日志级别被抑制 | 尝试 printk(KERN_ALERT ...) 或调整 /proc/sys/kernel/printk |
调试技巧: 在开发初期,建议在
printk 中使用 KERN_ALERT 或 KERN_EMERG 级别,确保日志能立即显示在控制台或 dmesg 中。生产代码中再调整为 KERN_INFO。
4.6 模块信息查看
加载模块后,你可以通过以下命令查看模块的详细信息:
# 查看模块详细信息
modinfo hello_world.ko
# 示例输出:
filename: hello_world.ko
license: GPL
author: Linux驱动开发实战
description: A simple Hello World kernel module
depends:
retpoline: Y
name: hello_world
vermagic: 5.15.0-generic SMP mod_unload modversions
modinfo 会显示模块的许可证、作者、描述、依赖关系以及编译时的内核版本(vermagic)。内核在加载模块时会严格检查 vermagic 是否匹配,这是最常见的加载失败原因之一。
4.7 本章小结
通过本章的学习,你已经完成了第一个内核模块的编写、编译、加载和卸载。你掌握了以下核心知识点:
- 内核模块必须包含
module_init和module_exit宏来定义入口和出口。 printk是内核空间的日志输出函数,与用户态的printf类似。- 使用
insmod加载模块,rmmod卸载模块,dmesg查看内核日志。 - 模块的许可证声明(
MODULE_LICENSE)是强制要求,缺少会导致加载失败。 - 编译内核模块需要与当前运行内核版本匹配的头文件。