4、第一个内核模块:Hello World:编写最简单的内核模块代码(init/exit函数)、模块许可证声明
本章将带领你从零编写第一个内核模块——经典的“Hello World”。通过这个最小化的实例,你将掌握内核模块的骨架结构:模块加载函数(init)、模块卸载函数(exit)以及必须的许可证声明。这是所有内核模块开发的起点,理解其结构对后续编写复杂驱动至关重要。
4.1 内核模块的基本结构
一个标准的内核模块由以下几个核心部分组成,它们共同定义了模块的生命周期和行为:
| 组成部分 | 作用 | 关键点 |
|---|---|---|
| 模块加载函数(init) | 模块被 insmod 加载时自动执行 |
使用 module_init() 宏注册;负责初始化资源、注册设备等 |
| 模块卸载函数(exit) | 模块被 rmmod 卸载时自动执行 |
使用 module_exit() 宏注册;负责释放资源、注销设备等 |
| 模块许可证声明 | 声明模块的开源许可证类型 | 使用 MODULE_LICENSE() 宏;推荐使用 "GPL" |
| 模块作者/描述声明 | 提供模块的元信息(可选但推荐) | 使用 MODULE_AUTHOR()、MODULE_DESCRIPTION() 等宏 |
关键理解:内核模块不是普通的用户态程序,它没有
main() 函数。内核通过 module_init() 和 module_exit() 两个宏来定位模块的入口和出口函数。这两个宏是模块与内核之间的“契约”。
4.2 编写第一个内核模块:hello.c
下面是最简单的内核模块代码。它会在加载时打印一条内核消息,在卸载时打印另一条消息。
/*
* hello.c - 第一个内核模块:Hello World
* 演示模块的 init/exit 函数及许可证声明
*/
#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");
// 返回 0 表示初始化成功
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Hello World: 模块已卸载!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("嵌入式Linux驱动开发课程");
MODULE_DESCRIPTION("一个简单的 Hello World 内核模块");
MODULE_VERSION("1.0");
4.3 代码逐行解析
让我们拆解上述代码中的关键元素:
- 头文件包含:
<linux/module.h>:所有内核模块都必须包含的头文件,定义了模块相关的宏和函数。<linux/kernel.h>:提供内核核心功能,如printk()和日志级别宏(KERN_INFO)。<linux/init.h>:提供module_init()和module_exit()宏,以及__init、__exit标记。
- 加载函数
hello_init:- 使用
static限定符,将函数作用域限制在当前文件。 - 使用
__init标记,提示内核该函数仅在初始化阶段使用,初始化完成后可以释放其占用的内存。 - 返回类型为
int:返回0表示初始化成功;返回负数(如-ENOMEM)表示失败,模块加载将被中止。 - 使用
printk(KERN_INFO ...)打印内核日志,KERN_INFO是日志级别,表示信息性消息。
- 使用
- 卸载函数
hello_exit:- 使用
static和__exit标记。__exit提示内核该函数仅在卸载时使用,如果模块被编译进内核(而非动态加载),此函数会被忽略以节省空间。 - 返回类型为
void,卸载函数不需要返回值。
- 使用
- 宏注册:
module_init(hello_init):将hello_init注册为模块的入口点。module_exit(hello_exit):将hello_exit注册为模块的出口点。
- 模块信息声明:
MODULE_LICENSE("GPL"):声明模块采用 GPL 许可证。这是必须的,缺少此声明会导致模块加载时内核报错“module license 'unspecified' taints kernel”。MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION:提供模块的元信息,可通过modinfo命令查看,虽然不是强制要求,但属于良好的开发习惯。
常见错误:初学者容易忘记包含
<linux/init.h> 头文件,或者将 module_init() 和 module_exit() 的调用顺序写反。请确保函数定义在前,宏注册在后。
4.4 模块生命周期与内核日志流
下面的 SVG 图展示了内核模块从加载到卸载的完整生命周期,以及 printk 消息在内核日志中的流向。
4.5 模块许可证声明的意义
MODULE_LICENSE() 不仅仅是一个文本声明,它对内核的行为有实际影响:
- 内核污点标记(Kernel Tainting): 如果模块未声明许可证或声明了非 GPL 兼容的许可证(如
"Proprietary"),内核会被标记为“受污染”(Tainted)。受污染的内核在报告 Bug 时,开发者可能会降低优先级,因为无法确定问题是否由非开源模块引起。 - 符号导出限制: 内核中某些函数(使用
EXPORT_SYMBOL_GPL导出的符号)只允许 GPL 模块调用。如果你的模块声明了非 GPL 许可证,将无法使用这些受保护的函数。 - 常见许可证值:
| 许可证字符串 | 含义 | 是否污染内核 |
|---|---|---|
"GPL" |
GNU General Public License v2 或更高版本 | 否 |
"GPL v2" |
GNU General Public License v2 仅 | 否 |
"Dual BSD/GPL" |
BSD 或 GPL 双许可证 | 否 |
"Proprietary" |
专有许可证 | 是 |
| (未指定) | 默认无许可证 | 是 |
最佳实践:在嵌入式 Linux 驱动开发中,除非有明确的商业许可要求,否则始终使用
MODULE_LICENSE("GPL")。这不仅能保持内核的纯净性,还能确保你的模块可以访问所有内核导出的符号。
4.6 使用 modinfo 查看模块信息
在编译生成 hello.ko 文件后,可以使用 modinfo 命令查看模块的元信息,验证我们的声明是否正确:
$ modinfo hello.ko
filename: /home/user/hello.ko
license: GPL
author: 嵌入式Linux驱动开发课程
description: 一个简单的 Hello World 内核模块
version: 1.0
depends:
name: hello
vermagic: 5.10.0-8-amd64 SMP mod_unload modversions
输出中清晰显示了我们在代码中声明的许可证、作者、描述和版本信息。vermagic 字段由内核构建系统自动生成,用于确保模块与当前运行的内核版本兼容。
至此,你已经完成了第一个内核模块的代码编写。下一阶段,我们将学习如何为这个模块编写 Makefile 并完成编译。