4. 第一个内核模块

本章将带领你从零编写第一个内核模块——经典的“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 消息在内核日志中的流向。

内核模块生命周期与日志流 用户执行 insmod 用户执行 rmmod 内核模块加载 执行 hello_init() 模块运行中 执行 hello_exit() 内核日志缓冲区 printk 消息 "Hello World: 模块已加载!" ... "Hello World: 模块已卸载!" dmesg 查看日志 说明:用户态通过 insmod/rmmod 触发内核模块的 init/exit 函数,函数中的 printk 消息写入内核日志缓冲区,最终通过 dmesg 查看。

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 并完成编译。