4. 第一个驱动模块

本章将带领你从零编写第一个 Linux 内核驱动模块——hello_world。通过这个最小化的模块,你将掌握驱动模块的基本骨架、内核模块的入口与出口机制,以及模块的加载与卸载流程。这是所有字符设备驱动开发的起点。

4.1 内核模块的基本结构

一个 Linux 内核模块本质上是一个可以在运行时动态加载到内核中的目标文件(.ko)。与用户态程序不同,内核模块没有 main() 函数,而是通过两个关键宏来定义模块的初始化和退出行为:

宏 / 函数 作用 调用时机
module_init() 指定模块加载时执行的初始化函数 insmod 时自动调用
module_exit() 指定模块卸载时执行的清理函数 rmmod 时自动调用
MODULE_LICENSE() 声明模块许可证(必须) 编译时嵌入
MODULE_AUTHOR() 声明模块作者(可选) 编译时嵌入
MODULE_DESCRIPTION() 声明模块描述(可选) 编译时嵌入
关键概念: module_init 和 module_exit 是内核模块的生命周期锚点。没有它们,内核无法知道模块的入口和出口在哪里,模块将无法正常加载或卸载。

4.2 Hello World 模块完整代码

下面是一个最简的 hello_world 内核模块。它会在加载时打印一条内核日志,卸载时打印另一条日志。

/* hello_world.c */
#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");
    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 内核模块");
代码要点:
  • __init 和 __exit 是内核属性标记,告诉内核这些函数在初始化/退出后可以释放内存。
  • printk 是内核空间的打印函数,KERN_INFO 指定日志级别。
  • 初始化函数必须返回 int,成功返回 0,失败返回负的错误码。
  • 退出函数返回类型为 void。

4.3 模块加载与卸载流程

下图展示了从用户态执行 insmod 和 rmmod 命令时,内核模块的生命周期流转:

内核模块加载与卸载流程 内核模块加载与卸载流程 insmod hello.ko rmmod hello 内核空间 模块生命周期管理 module_init(hello_init) 执行初始化函数 module_exit(hello_exit) 执行清理函数 printk: "Hello World! 模块已加载。" printk: "再见!模块已卸载。" 使用 dmesg 查看内核日志输出

4.4 编译与测试步骤

要编译内核模块,你需要一个 Makefile。以下是标准的模块编译方法:

# Makefile
obj-m += hello_world.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
重要提示: 编译内核模块需要安装内核头文件。在 Ubuntu/Debian 上使用 sudo apt install linux-headers-$(uname -r) 安装。如果头文件版本与当前运行内核不匹配,模块将无法加载。

编译和测试的完整步骤:

  1. 编译模块: 在源码目录执行 make,生成 hello_world.ko。
  2. 加载模块: sudo insmod hello_world.ko。无输出表示成功。
  3. 查看日志: dmesg | tail -5,应看到 "Hello World! 模块已加载。"。
  4. 查看已加载模块: lsmod | grep hello_world,确认模块在列表中。
  5. 卸载模块: sudo rmmod hello_world。
  6. 再次查看日志: dmesg | tail -5,应看到 "再见!模块已卸载。"。

4.5 常见错误与排查

错误现象 可能原因 解决方案
insmod: ERROR: could not insert module 内核版本不匹配 / 缺少许可证 检查 uname -r 与头文件版本;确保有 MODULE_LICENSE("GPL")
Unknown symbol 模块依赖的内核符号未导出 检查是否使用了未导出的函数或变量
rmmod: ERROR: Module hello_world is in use 有其他进程或模块正在使用该模块 使用 lsmod 查看引用计数,先释放依赖
加载后 dmesg 无输出 日志级别被抑制 尝试 printk(KERN_ALERT ...) 或调整 /proc/sys/kernel/printk
调试技巧: 在开发初期,建议在 printk 中使用 KERN_ALERT 或 KERN_EMERG 级别,确保日志能立即显示在控制台或 dmesg 中。生产代码中再调整为 KERN_INFO。

4.6 模块信息查看

加载模块后,你可以通过以下命令查看模块的详细信息:

# 查看模块详细信息
modinfo hello_world.ko

# 示例输出:
filename:       hello_world.ko
license:        GPL
author:         Linux驱动开发实战
description:    A simple Hello World kernel module
depends:        
retpoline:      Y
name:           hello_world
vermagic:       5.15.0-generic SMP mod_unload modversions

modinfo 会显示模块的许可证、作者、描述、依赖关系以及编译时的内核版本(vermagic)。内核在加载模块时会严格检查 vermagic 是否匹配,这是最常见的加载失败原因之一。

4.7 本章小结

通过本章的学习,你已经完成了第一个内核模块的编写、编译、加载和卸载。你掌握了以下核心知识点:

  • 内核模块必须包含 module_init 和 module_exit 宏来定义入口和出口。
  • printk 是内核空间的日志输出函数,与用户态的 printf 类似。
  • 使用 insmod 加载模块,rmmod 卸载模块,dmesg 查看内核日志。
  • 模块的许可证声明(MODULE_LICENSE)是强制要求,缺少会导致加载失败。
  • 编译内核模块需要与当前运行内核版本匹配的头文件。