3. 第一个字符设备驱动:编写最简单的Hello驱动模块

本章将带领你从零编写第一个内核模块——一个简单的“Hello World”字符设备驱动。通过这个最小化的驱动,你将掌握内核模块的基本框架、编译方法以及加载/卸载流程,为后续编写复杂驱动打下坚实基础。

3.1 内核模块的基本结构

一个完整的内核模块至少包含两个核心函数:模块初始化函数和模块退出函数。它们通过特定的宏与内核注册,分别在内核加载和卸载模块时被自动调用。

宏/函数 作用 调用时机
module_init(init_func) 指定模块初始化入口函数 执行 insmod 时
module_exit(exit_func) 指定模块退出清理函数 执行 rmmod 时
MODULE_LICENSE("GPL") 声明模块许可证(必须) 编译时嵌入
MODULE_AUTHOR("name") 声明模块作者(可选) 编译时嵌入
MODULE_DESCRIPTION("desc") 声明模块描述(可选) 编译时嵌入

3.2 编写Hello驱动模块

创建一个名为 hello.c 的文件,输入以下代码:

#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! Module loaded.\n");
    return 0;   /* 0表示成功,负数表示失败 */
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, World! Module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Driver Course");
MODULE_DESCRIPTION("A simple Hello World kernel module");
关键点:
  • __init 标记初始化函数,模块加载后该函数占用的内存会被释放。
  • __exit 标记退出函数,如果模块被编译进内核(非模块),该函数会被忽略。
  • printk 是内核空间的打印函数,KERN_INFO 指定日志级别。
  • 初始化函数返回 0 表示成功,返回负数表示失败,内核将拒绝加载。

3.3 编写Makefile编译模块

内核模块不能使用普通的gcc编译,必须借助内核的构建系统。在 hello.c 同目录下创建 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
Makefile变量 含义
obj-m 指定要编译成模块的目标文件(.o)
KDIR 内核源码树路径,通常指向已安装的内核头文件
M=$(PWD) 告诉内核构建系统在哪个目录查找模块源码

执行 make 命令,编译成功后会在当前目录生成 hello.ko(内核模块文件)。

常见编译错误:
  • “No such file or directory”:检查 KDIR 路径是否正确,确保已安装内核头文件(linux-headers-$(uname -r))。
  • “Permission denied”:确保当前用户对内核源码树有读取权限。
  • “multiple definition of `init_module'”:检查是否重复定义了 module_init 或函数名冲突。

3.4 加载与卸载模块

编译得到 hello.ko 后,使用以下命令操作模块:

# 加载模块
sudo insmod hello.ko

# 查看模块是否加载成功
lsmod | grep hello

# 查看内核日志输出
dmesg | tail -5

# 卸载模块
sudo rmmod hello

# 再次查看日志确认卸载
dmesg | tail -5
命令 功能 说明
insmod 加载模块 需要root权限;模块路径可以是绝对或相对路径
rmmod 卸载模块 只需模块名,不需要路径和.ko后缀
lsmod 列出所有已加载模块 显示模块名、大小、被谁使用
modinfo 查看模块详细信息 显示作者、描述、许可证、参数等
dmesg 查看内核环形缓冲区日志 模块的 printk 输出会出现在这里

3.5 模块生命周期与内核日志流

下图展示了从编写代码到加载/卸载模块的完整流程,以及内核日志的流向:

Hello模块生命周期与内核日志流 编写 hello.c Makefile 编译 hello.ko insmod hello.ko module_init() printk() 日志 dmesg 查看日志 rmmod hello module_exit() 说明:insmod触发module_init → printk写入内核日志缓冲区 → dmesg读取显示;rmmod触发module_exit

3.6 查看模块信息

模块加载后,可以使用 modinfo 命令查看模块的元数据信息:

modinfo hello.ko

# 输出示例:
filename:       /home/user/hello/hello.ko
license:        GPL
author:         Embedded Linux Driver Course
description:    A simple Hello World kernel module
depends:        
retpoline:      Y
name:           hello
vermagic:       5.15.0-generic SMP mod_unload modversions

使用 lsmod 查看当前已加载的模块列表:

lsmod | head -10

# 输出示例:
Module                  Size  Used by
hello                  16384  0
nfnetlink              16384  2 nfnetlink_log
...                     ...    ...
lsmod 列 含义
Module 模块名称
Size 模块占用的内存大小(字节)
Used by 被哪些模块或进程使用,0表示未被使用
实践检查清单:
  1. ✅ 确认 hello.c 和 Makefile 在同一目录。
  2. ✅ 执行 make 无错误,生成 hello.ko。
  3. ✅ sudo insmod hello.ko 无输出(成功时无消息)。
  4. ✅ lsmod | grep hello 显示模块已加载。
  5. ✅ dmesg | tail 看到 "Hello, World! Module loaded."。
  6. ✅ sudo rmmod hello 卸载成功。
  7. ✅ dmesg | tail 看到 "Goodbye, World! Module unloaded."。

3.7 本章小结

通过本章的学习,你已经掌握了:

  • 内核模块的标准框架:module_init / module_exit 配对使用。
  • 使用 Makefile 调用内核构建系统编译模块。
  • 使用 insmod / rmmod / lsmod / modinfo / dmesg 管理模块。
  • 理解内核日志的输出与查看机制。