2. 第一个内核模块:编写并加载一个简单的“Hello World”内核模块,理解 module_init 和 module_exit 机制

2.1 本章目标

  • 掌握内核模块的基本代码结构
  • 理解 module_init 与 module_exit 宏的注册与回调机制
  • 学会使用 Makefile 编译内核模块
  • 掌握 insmod、rmmod、dmesg 等基本操作

2.2 内核模块的骨架

一个最简的内核模块必须包含两个核心宏:module_init 和 module_exit。它们分别指定模块加载到内核时执行的初始化函数,以及模块卸载时执行的清理函数。

关键概念: 内核模块运行在 内核空间,不能直接调用标准 C 库函数(如 printf),必须使用内核提供的 printk 函数输出日志。

2.2.1 代码结构

#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");
    return 0;   // 返回 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("Your Name");         // 作者信息
MODULE_DESCRIPTION("A simple Hello World module");  // 模块描述

2.3 module_init 与 module_exit 机制

这两个宏本质上是编译器属性标记,它们将函数指针放入特定的 ELF 段(.init.text 和 .exit.text)。内核在加载模块时,会从模块文件中提取 module_init 注册的函数地址并执行;卸载时则调用 module_exit 注册的函数。

提示: __init 标记告诉内核:该函数仅在初始化阶段使用,初始化完成后可以释放其占用的内存。__exit 标记则用于卸载路径,如果模块被编译进内核(而非动态加载),该函数会被忽略。

2.3.1 加载与卸载流程

模块加载与卸载流程 insmod hello.ko rmmod hello 内核模块加载器 hello_init() 执行 hello_exit() 执行 printk 输出到内核日志 printk 输出到内核日志

2.4 编译内核模块:Makefile 编写

内核模块的编译必须依赖内核源码树(或内核头文件包)。标准做法是使用内核提供的 Kbuild 系统。

# Makefile
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
变量/目标 说明
obj-m := hello.o 告诉 Kbuild 将 hello.c 编译成可动态加载的内核模块 hello.ko
KDIR 指向当前运行内核的构建目录(包含 Makefile 和头文件)
-C $(KDIR) 切换到内核源码目录执行 make
M=$(PWD) 指定模块源码所在路径,make 会返回该目录编译模块
modules 内核 Makefile 的目标,用于编译外部模块
常见错误: 如果系统没有安装内核头文件(linux-headers-*),KDIR 路径不存在,编译会失败。Ubuntu/Debian 下执行 sudo apt install linux-headers-$(uname -r) 安装。

2.5 编译与测试步骤

  1. 编写代码: 将 2.2.1 节的代码保存为 hello.c,Makefile 保存为 Makefile。
  2. 编译: 在终端执行 make,生成 hello.ko。
  3. 加载模块: sudo insmod hello.ko(无输出表示成功)。
  4. 查看日志: dmesg | tail,应看到 Hello World: 模块已加载。
  5. 卸载模块: sudo rmmod hello。
  6. 再次查看日志: dmesg | tail,应看到 Hello World: 模块已卸载。
  7. 清理: make clean 删除编译产生的中间文件。

2.6 模块信息与工具

命令 功能 示例
insmod 加载模块(需指定 .ko 文件路径) sudo insmod hello.ko
rmmod 卸载模块(只需模块名,不含 .ko) sudo rmmod hello
modprobe 智能加载/卸载(自动处理依赖) sudo modprobe hello
lsmod 列出当前已加载的所有模块 lsmod | grep hello
modinfo 查看模块的详细信息(作者、描述、参数等) modinfo hello.ko
dmesg 查看内核环形缓冲区日志 dmesg -H(人类可读格式)

2.7 常见问题与避坑

  • 权限问题: 加载和卸载模块需要 root 权限,必须使用 sudo。
  • 版本不匹配: 模块必须与当前运行的内核版本完全一致,否则 insmod 会报 Invalid module format 错误。
  • printk 看不到输出: 检查日志级别,使用 dmesg -H 或 journalctl -k 查看;某些系统可能将 KERN_INFO 级别的日志过滤掉。
  • 模块名冲突: 如果内核已存在同名模块,insmod 会失败,先用 lsmod 确认。
  • 忘记 MODULE_LICENSE: 不声明许可证会导致内核报 kernel tainted 警告,且某些内核功能不可用。

2.8 本章小结

通过本章,你完成了第一个内核模块的编写、编译、加载和卸载全流程。核心要点是:

  • module_init 和 module_exit 是内核模块的入口和出口,由内核在特定时机回调。
  • 内核模块使用 printk 输出日志,通过 dmesg 查看。
  • Makefile 依赖内核构建系统 Kbuild,必须指定正确的 KDIR。
  • 模块的加载/卸载操作需要 root 权限,且版本必须与内核匹配。