3. 第一个字符设备驱动
本章将带领你从零编写第一个内核模块——一个简单的“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 模块生命周期与内核日志流
下图展示了从编写代码到加载/卸载模块的完整流程,以及内核日志的流向:
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表示未被使用 |
实践检查清单:
- ✅ 确认
hello.c和Makefile在同一目录。 - ✅ 执行
make无错误,生成hello.ko。 - ✅
sudo insmod hello.ko无输出(成功时无消息)。 - ✅
lsmod | grep hello显示模块已加载。 - ✅
dmesg | tail看到 "Hello, World! Module loaded."。 - ✅
sudo rmmod hello卸载成功。 - ✅
dmesg | tail看到 "Goodbye, World! Module unloaded."。
3.7 本章小结
通过本章的学习,你已经掌握了:
- 内核模块的标准框架:
module_init/module_exit配对使用。 - 使用 Makefile 调用内核构建系统编译模块。
- 使用
insmod/rmmod/lsmod/modinfo/dmesg管理模块。 - 理解内核日志的输出与查看机制。