2. 第一个内核模块:Hello World 模块的编写、Makefile 构建、insmod/rmmod 加载与卸载
内核模块是 Linux 驱动开发的最小可加载单元。本章通过一个最简的 Hello World 模块,完整演示从源码编写、Makefile 构建到动态加载/卸载的全流程。掌握这一基础链路后,后续所有驱动实验都将复用相同的框架。
2.1 模块源码结构
一个标准内核模块至少包含两个宏:模块许可证声明和入口/出口函数。以下是最简模块的完整代码:
/* 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 from kernel module!\n");
return 0; /* 0 表示加载成功 */
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, kernel module unloaded.\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("A simple Hello World module");
关键点:
__init/__exit标记将函数放入特殊段,模块加载/卸载后自动释放内存。printk是内核空间的打印函数,通过dmesg查看输出,不要使用printf。MODULE_LICENSE("GPL")是必须的,缺少会导致加载时提示tainted kernel。
2.2 Makefile 构建系统
内核模块的编译依赖内核源码树(或内核头文件包)。标准 Makefile 模板如下:
# Makefile — 构建 hello.ko
obj-m := hello.o
# KDIR 指向已编译的内核源码目录
KDIR := /lib/modules/$(shell uname -r)/build
# PWD 为当前工作目录
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
构建步骤:
- 将
hello.c和Makefile放在同一目录。 - 执行
make,生成hello.ko(内核模块文件)。 - 使用
make clean清理临时文件。
2.3 模块加载与卸载命令
| 操作 | 命令 | 说明 |
|---|---|---|
| 加载模块 | sudo insmod hello.ko |
将模块插入内核,执行 hello_init |
| 查看已加载模块 | lsmod | grep hello |
列出所有模块,过滤 hello |
| 查看内核日志 | dmesg | tail -5 |
查看 printk 输出 |
| 卸载模块 | sudo rmmod hello |
从内核移除模块,执行 hello_exit |
| 模块信息 | modinfo hello.ko |
显示作者、描述、许可证等 |
常见错误:
- “Operation not permitted” — 忘记
sudo,需要 root 权限。 - “Invalid module format” — 内核版本与编译头不匹配,检查
uname -r与 KDIR。 - “Unknown symbol” — 模块依赖其他模块未加载,或符号未导出。
2.4 模块生命周期与内核交互流程
下图展示了从 insmod 到 rmmod 的完整内核交互路径:
2.5 模块信息与依赖管理
使用 modinfo 可以查看模块的元数据,这些信息来自源码中的 MODULE_* 宏:
$ modinfo hello.ko
filename: /home/user/hello.ko
license: GPL
author: Embedded Linux Expert
description: A simple Hello World module
depends: (空)
vermagic: 5.15.0-generic SMP mod_unload
模块依赖链: 如果模块 A 使用了模块 B 导出的符号,则 A 依赖于 B。加载时必须先加载 B,卸载时先卸载 A。使用
lsmod 可以查看模块间的 Used by 关系。
2.6 本章小结
- 内核模块使用
module_init/module_exit注册入口和出口。 - Makefile 通过
obj-m和内核源码树完成交叉/本地编译。 insmod加载模块,rmmod卸载模块,dmesg查看内核日志。- 模块信息通过
modinfo查看,依赖关系由内核自动维护。