3. 第一个内核模块:Hello World 模块
本章是嵌入式 Linux 驱动开发的第一个实战环节。你将亲手编写一个最简单的内核模块——Hello World,并掌握模块的编译、加载、卸载以及内核日志输出的核心概念。这是后续所有字符设备驱动的基础骨架。
3.1 内核模块的基本结构
一个 Linux 内核模块本质上是一段可以在运行时动态加载到内核的代码。它不像用户态程序有 main() 函数,而是通过两个固定的入口函数与内核交互:
- 模块初始化入口:模块被加载时执行,通常用于注册设备、分配资源。
- 模块卸载出口:模块被卸载时执行,用于释放资源、注销设备。
下面是最简内核模块的代码骨架:
#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! Module loaded.\n");
return 0; // 返回 0 表示加载成功
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, World! 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"); // 模块描述
关键点:
__init 和 __exit 是内核宏,用于标记函数在初始化/退出后可以释放内存。所有内核模块必须声明 MODULE_LICENSE,否则内核会认为模块是“污染”的。
3.2 Makefile 编写:内核模块的编译系统
内核模块不能使用普通的 gcc 编译,必须借助内核的 Kbuild 构建系统。Makefile 的核心作用是告诉内核源码树如何编译外部模块。
标准的外部模块 Makefile 模板如下:
# 指定内核源码树的路径,根据实际环境修改
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# 当前模块所在目录
PWD := $(shell pwd)
# 目标模块名称(最终生成 .ko 文件)
obj-m := hello.o
all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
| Makefile 变量/目标 | 含义 |
|---|---|
KERNELDIR |
指向已编译的内核源码树或内核头文件目录,/lib/modules/$(uname -r)/build 是标准符号链接 |
obj-m |
指定要编译成模块的目标文件列表,hello.o 对应 hello.c |
-C $(KERNELDIR) |
切换到内核源码目录读取其顶层 Makefile |
M=$(PWD) |
告诉 Kbuild 返回当前目录编译外部模块 |
modules |
内核 Makefile 的目标,用于编译模块 |
常见错误:如果
KERNELDIR 指向的内核源码没有预先编译过(缺少 Module.symvers 等文件),编译会失败。在开发板上,通常需要先编译一次完整内核或至少编译内核模块依赖。
3.3 模块加载与卸载操作
编译成功后,会生成 hello.ko 文件。使用以下命令操作模块:
- 加载模块:
sudo insmod hello.ko - 查看已加载模块:
lsmod | grep hello - 卸载模块:
sudo rmmod hello - 查看模块信息:
modinfo hello.ko
加载和卸载时,内核会自动调用你在代码中注册的 hello_init 和 hello_exit 函数。
操作流程:
1. 编写 hello.c 和 Makefile
2. 执行
3. 执行
4. 执行
5. 执行
6. 再次执行
1. 编写 hello.c 和 Makefile
2. 执行
make 编译生成 hello.ko3. 执行
sudo insmod hello.ko 加载模块4. 执行
dmesg | tail 查看内核日志输出5. 执行
sudo rmmod hello 卸载模块6. 再次执行
dmesg | tail 查看退出日志
3.4 printk 日志级别详解
printk 是内核空间的“printf”,但它比用户态 printf 多了一个日志级别(loglevel)。日志级别决定了消息的严重程度以及是否会被立即输出到控制台。
常用的日志级别宏定义在 <linux/kern_levels.h> 中:
| 宏 | 数值 | 含义 | 典型场景 |
|---|---|---|---|
KERN_EMERG |
0 | 紧急事件,系统不可用 | 内核崩溃前 |
KERN_ALERT |
1 | 必须立即处理 | 严重硬件故障 |
KERN_CRIT |
2 | 临界条件 | 驱动初始化失败 |
KERN_ERR |
3 | 错误条件 | 设备操作失败 |
KERN_WARNING |
4 | 警告条件 | 不推荐的使用方式 |
KERN_NOTICE |
5 | 正常但重要的消息 | 模块加载/卸载通知 |
KERN_INFO |
6 | 信息性消息 | 驱动版本信息 |
KERN_DEBUG |
7 | 调试消息 | 开发阶段的详细日志 |
使用方式:printk(KERN_INFO "Hello, World!\n"); —— 注意日志级别宏与格式化字符串之间没有逗号,预处理器会将它们拼接在一起。
控制台输出控制:内核通过
/proc/sys/kernel/printk 文件控制哪些级别的消息会显示在控制台。该文件有 4 个数值:控制台日志级别 默认消息级别 最低控制台级别 默认控制台级别。只有日志级别数值小于控制台日志级别的消息才会被打印到控制台。
3.5 完整流程与模块生命周期 SVG 图
下图展示了从编写代码到模块卸载的完整生命周期,以及 printk 日志在其中的作用位置:
3.6 本章小结
通过本章的学习,你已经掌握了:
- 内核模块的固定代码结构:
module_init/module_exit入口。 - 使用 Kbuild 系统编写 Makefile 编译外部模块。
insmod、rmmod、lsmod、dmesg等模块管理命令。printk的 8 个日志级别及其在调试中的作用。