3. 第一个内核模块

本章是驱动开发的“零号里程碑”。你将亲手编写一个最简单的内核模块,并掌握从源码到 .ko 文件的完整编译流程。这个模块不操作硬件、不注册设备,只在内核空间打印一条消息——但它验证了你的开发环境、工具链和内核构建系统是否就绪。

3.1 内核模块的基本结构

一个标准的内核模块至少包含两个宏:模块许可证声明和入口/出口函数。与用户空间程序不同,内核模块没有 main(),而是通过 module_init() 和 module_exit() 指定加载和卸载时的回调。

关键概念:内核模块运行在内核空间,拥有最高特权级。任何崩溃都会导致整个系统挂掉——所以第一个模块务必简单、正确。

3.1.1 模块组成要素

要素 宏/函数 说明
入口函数 module_init(hello_init) insmod 加载时调用,返回 0 表示成功
出口函数 module_exit(hello_exit) rmmod 卸载时调用,清理资源
许可证 MODULE_LICENSE("GPL") 必须声明,否则内核会标记为“污染”
作者/描述 MODULE_AUTHOR / MODULE_DESCRIPTION 可选,但推荐填写以方便维护

3.2 编写 Hello World 模块源码

创建一个文件 hello.c,内容如下。注意头文件 <linux/module.h> 和 <linux/init.h> 是所有模块都必须包含的基础头文件。

// hello.c — 第一个内核模块
#include <linux/module.h>   // 模块核心宏
#include <linux/init.h>     // __init / __exit 宏
#include <linux/kernel.h>   // printk 内核打印函数

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, World! 模块已加载。\n");
    printk(KERN_INFO "当前内核版本: %s\n", UTS_RELEASE);
    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内核模块示例");
MODULE_VERSION("1.0");
💡 提示:printk 是内核版的 printf,KERN_INFO 是日志级别。消息会写入内核环形缓冲区,可通过 dmesg 查看。

3.3 模块生命周期 SVG 流程

下图展示了从加载到卸载的完整生命周期,以及内核空间与用户空间的边界。

内核模块生命周期流程图 内核模块生命周期:从加载到卸载 用户空间 (User Space) 内核空间 (Kernel Space) insmod module_init() 模块运行中 rmmod module_exit() 卸载完成 printk 输出 通过 dmesg 查看

3.4 Makefile 编写:内核构建系统

内核模块不能直接用 gcc 编译,必须使用内核提供的 Kbuild 构建系统。Makefile 的核心是调用内核源码树的 make modules 目标。

3.4.1 标准 Makefile 模板

在 hello.c 同目录下创建 Makefile,内容如下:

# Makefile — 内核模块编译
# 目标模块名(不带 .ko 后缀)
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
⚠️ 常见错误:KDIR 必须指向已编译过的内核源码树或内核头文件包。如果系统未安装内核头文件,先执行 sudo apt install linux-headers-$(uname -r)(Ubuntu/Debian)或 sudo yum install kernel-devel(RHEL/CentOS)。

3.4.2 Makefile 关键变量解析

变量 含义 示例值
obj-m 指定要编译成模块的目标文件(不含路径) hello.o
KDIR 内核构建目录路径 /lib/modules/6.8.0-45-generic/build
M=$(PWD) 告诉 Kbuild 返回当前目录编译模块 M=/home/user/driver/hello
-C 切换到内核目录执行 Makefile —

3.5 编译与测试步骤

按照以下顺序执行,验证你的第一个内核模块:

  1. 准备源码:确保 hello.c 和 Makefile 在同一目录。
  2. 执行编译:运行 make,输出应包含 Building modules, stage 2. 和 MODPOST 等字样。
  3. 检查产物:确认生成了 hello.ko 文件。
  4. 加载模块:sudo insmod hello.ko(无输出表示成功)。
  5. 查看日志:dmesg | tail -5,应看到 Hello, World! 模块已加载。
  6. 卸载模块:sudo rmmod hello。
  7. 再次查看日志:dmesg | tail -3,应看到 再见,内核空间!模块已卸载。
✅ 验证清单:
  • lsmod | grep hello 在加载后应列出模块。
  • modinfo hello.ko 可查看作者、描述、许可证等元信息。
  • 如果 insmod 返回 Operation not permitted,检查是否使用 sudo。

3.6 编译产物与目录结构

编译成功后,目录下会生成多个中间文件。下表说明各文件的用途:

文件 类型 说明
hello.ko 最终模块 可动态加载的内核对象文件
hello.o 目标文件 编译后的二进制,未链接
hello.mod.c 自动生成 模块元信息(许可证、依赖等)的 C 源码
hello.mod.o 目标文件 hello.mod.c 编译后的对象
Module.symvers 符号表 导出的内核符号版本信息(多模块依赖时重要)
.hello.ko.cmd 命令记录 Kbuild 用于增量编译的依赖追踪文件

3.7 常见编译错误与解决

  • “No rule to make target ‘modules’”:KDIR 路径错误或内核头文件未安装。
  • “implicit declaration of function ‘printk’”:缺少 #include <linux/kernel.h>。
  • “Module license ‘unspecified’ taints kernel”:未添加 MODULE_LICENSE("GPL")。
  • “Invalid module format”:模块编译时的内核版本与运行内核版本不一致。用 uname -r 确认版本。
🔧 调试技巧:在 Makefile 中添加 CFLAGS_hello.o := -DDEBUG 可以启用模块内的 pr_debug() 输出,配合 dmesg -n 7 查看所有调试信息。

3.8 本章小结

通过本章,你完成了驱动开发的“Hello World”里程碑:

  • 理解了内核模块的 init/exit 生命周期。
  • 掌握了 Kbuild 构建系统的 Makefile 编写方法。
  • 成功编译并动态加载/卸载了第一个 .ko 模块。
  • 学会了使用 dmesg 和 modinfo 验证模块行为。

这个基础流程将贯穿整个课程——后续所有驱动项目都遵循同样的“编写源码 → 编写 Makefile → make → insmod → 测试 → rmmod”循环。