2、第一个内核模块:Hello World模块编写、Makefile编写、模块加载与卸载、printk日志级别
在Linux网络驱动开发的旅程中,第一个里程碑就是编写一个能够在内核空间运行的最小程序——内核模块。与用户空间的应用程序不同,内核模块运行在操作系统最核心的层级,拥有最高的权限,也承担着最大的风险。本章将引导你从零开始,亲手构建第一个内核模块,掌握其编译、加载与卸载的全流程,并深入理解内核调试信息的输出机制——printk 与日志级别。
2.1 内核模块的基本概念
内核模块(Loadable Kernel Module, LKM)是Linux内核在运行时动态加载和卸载的一段代码,用于扩展内核功能,而无需重新编译整个内核或重启系统。网络驱动程序通常以模块形式存在,便于开发和维护。
一个标准的内核模块至少包含两个关键函数:
- 模块初始化函数(
module_init):模块被加载到内核时自动执行,用于注册设备、分配资源。 - 模块清理函数(
module_exit):模块从内核卸载时自动执行,用于释放资源、注销设备。
2.2 Hello World 内核模块源码
下面是一个最简内核模块的完整代码。它不依赖任何硬件,仅在加载和卸载时打印一条消息到内核日志。
/* 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! 模块已加载。\n");
/* KERN_INFO 是日志级别,表示信息性消息 */
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("Your Name"); /* 作者信息 */
MODULE_DESCRIPTION("A simple Hello World kernel module"); /* 模块描述 */
MODULE_VERSION("1.0"); /* 版本号 */
MODULE_LICENSE,否则加载时会收到“tainted kernel”警告。
2.3 Makefile 编写:内核模块的编译系统
内核模块不能使用普通的gcc直接编译,必须借助内核的构建系统(Kbuild)。Makefile需要调用内核源码树中的Makefile来完成编译。
# Makefile — 用于编译 hello.ko 内核模块
# 指定当前内核源码树的路径(通常为 /lib/modules/$(shell uname -r)/build)
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 := hello.o |
告诉Kbuild将 hello.c 编译成名为 hello.ko 的内核模块(.o 是中间目标,最终生成 .ko) |
KDIR |
内核源码构建目录路径,指向当前运行内核的编译环境 |
$(MAKE) -C $(KDIR) M=$(PWD) modules |
进入内核源码目录,执行模块编译;M= 指定模块源码所在路径 |
modules |
Kbuild的目标,表示编译外部模块 |
*** No rule to make target 'modules'。在Ubuntu/Debian上执行 sudo apt install linux-headers-$(uname -r) 即可解决。
2.4 模块的加载与卸载操作
编译成功后,你会得到 hello.ko 文件。接下来通过以下命令操作模块:
- 加载模块:
sudo insmod hello.ko— 将模块插入内核,触发hello_init()函数。 - 查看已加载模块:
lsmod | grep hello— 确认模块是否成功加载。 - 卸载模块:
sudo rmmod hello— 从内核移除模块,触发hello_exit()函数。 - 查看内核日志:
dmesg | tail -10— 查看模块打印的Hello/Goodbye消息。
sudo)。普通用户无法执行 insmod 和 rmmod。
操作示例输出:
$ sudo insmod hello.ko
$ lsmod | grep hello
hello 16384 0
$ sudo rmmod hello
$ dmesg | tail -2
[ 1234.567890] Hello, World! 模块已加载。
[ 1235.123456] 再见,内核!模块已卸载。
2.5 printk 日志级别详解
printk 是内核空间的“printf”,用于向内核日志缓冲区(ring buffer)输出信息。与用户空间的 printf 不同,printk 必须指定日志级别(loglevel),以控制消息的重要性和显示行为。
日志级别宏(定义在 <linux/kernel.h> 中):
| 宏名称 | 数值 | 含义 | 典型使用场景 |
|---|---|---|---|
KERN_EMERG |
0 | 紧急事件,系统不可用 | 致命硬件故障、内核崩溃前 |
KERN_ALERT |
1 | 必须立即处理 | 严重错误,需要管理员介入 |
KERN_CRIT |
2 | 临界条件 | 严重的驱动程序错误 |
KERN_ERR |
3 | 错误条件 | 设备I/O错误、资源分配失败 |
KERN_WARNING |
4 | 警告条件 | 不推荐的使用方式、降级运行 |
KERN_NOTICE |
5 | 正常但重要的消息 | 模块加载/卸载通知 |
KERN_INFO |
6 | 信息性消息 | 驱动版本信息、硬件探测结果 |
KERN_DEBUG |
7 | 调试消息 | 开发阶段的详细调试输出 |
使用示例:
printk(KERN_ERR "DMA传输失败,状态码: %d\n", status);
printk(KERN_DEBUG "寄存器值: 0x%x\n", reg_val);
KERN_DEBUG 级别。可以通过 echo 8 > /proc/sys/kernel/printk 临时提高控制台日志级别,使所有调试信息显示在终端上。生产环境中应关闭调试输出,避免日志泛滥。
2.6 模块信息与许可证声明
良好的模块信息声明有助于内核管理和调试。以下宏应出现在每个模块的末尾:
| 宏 | 作用 | 是否必须 |
|---|---|---|
MODULE_LICENSE("GPL") |
声明模块许可证,影响内核符号导出 | 是(否则内核标记为“污染”) |
MODULE_AUTHOR("name") |
记录作者信息 | 推荐 |
MODULE_DESCRIPTION("text") |
简要描述模块功能 | 推荐 |
MODULE_VERSION("x.y") |
模块版本号 | 推荐 |
MODULE_LICENSE("GPL") 或声明了非GPL兼容许可证(如 "Proprietary"),加载时内核会输出 module 'hello' taints kernel 警告。这会影响内核社区对bug报告的接受度,并限制对某些GPL-only内核符号的访问。
2.7 本章小结
通过本章的学习,你已经完成了Linux内核模块开发的“Hello World”实践。你掌握了:
- 内核模块的基本结构:
module_init和module_exit的配对使用。 - 使用Kbuild系统编写Makefile,将C源码编译为
.ko模块文件。 - 通过
insmod、rmmod、lsmod和dmesg管理模块生命周期。 printk的8个日志级别及其在驱动调试中的应用策略。
这个最小模块是你构建复杂网络驱动的基石。在下一阶段,我们将在此基础上添加设备文件操作、注册网络设备,并最终实现DMA数据收发。请确保你能够独立完成本章的编译和加载实验,因为后续所有章节都将依赖这一基础技能。