4. 编写第一个内核模块:Hello World
在深入 GPIO 驱动之前,必须先掌握内核模块的基本编写与加载流程。本节通过一个最简的 Hello World 内核模块,演示从代码编写、Makefile 构建到内核日志查看的完整闭环。这是所有 Android 内核驱动开发的起点。
学习目标: 完成本节后,你应能独立编写一个可加载/卸载的内核模块,并熟练使用
dmesg 查看内核输出日志。
4.1 内核模块代码结构
一个标准的内核模块至少包含两个宏:module_init() 和 module_exit(),分别指定模块加载和卸载时的入口函数。此外还需要 MODULE_LICENSE() 声明许可证。
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World from Android Kernel Module!\n");
return 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("Your Name");
MODULE_DESCRIPTION("A simple Hello World kernel module for Android");
关键点:
__init/__exit标记:告诉内核这些函数只在初始化/退出时使用,之后可回收内存。printk():内核空间的打印函数,日志会写入内核环形缓冲区,通过dmesg查看。KERN_INFO:日志级别,表示普通信息。
4.2 Makefile 编写
内核模块的编译必须依赖内核源码树(Kernel Source Tree)。Makefile 需要指定目标模块名和内核构建系统路径。
# Makefile
obj-m := hello.o
# 指定 Android 内核源码路径(根据实际环境修改)
KDIR := /path/to/android/kernel
# 交叉编译工具链前缀(Android 通常使用 aarch64-linux-android-)
CROSS_COMPILE := aarch64-linux-android-
all:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) ARCH=arm64 CROSS_COMPILE=$(CROSS_COMPILE) clean
| 变量 | 说明 | 示例值 |
|---|---|---|
obj-m |
指定要编译成模块的目标文件(.o) | hello.o |
KDIR |
内核源码树路径 | /home/user/android_kernel |
CROSS_COMPILE |
交叉编译工具链前缀 | aarch64-linux-android- |
ARCH |
目标架构 | arm64 |
常见错误: 如果
KDIR 指向的内核源码未编译过(缺少 Module.symvers 等文件),编译会失败。请确保先执行 make defconfig && make modules_prepare。
4.3 编译与加载测试
编译过程会生成 hello.ko 文件,这就是内核模块。使用 insmod 加载,rmmod 卸载,lsmod 查看已加载模块列表。
# 编译模块
make
# 加载模块(需要 root 权限)
adb root
adb shell insmod /data/local/tmp/hello.ko
# 查看已加载模块
adb shell lsmod | grep hello
# 卸载模块
adb shell rmmod hello
提示: 在 Android 设备上,通常将
.ko 文件 push 到 /data/local/tmp/ 目录下执行,因为该目录具有可执行权限。
4.4 dmesg 日志查看
模块加载和卸载时打印的信息会出现在内核日志中。使用 dmesg 命令查看,结合 grep 过滤关键字。
# 查看所有内核日志(需要 root)
adb shell dmesg
# 只查看 Hello World 相关日志
adb shell dmesg | grep "Hello World"
# 清空日志(便于调试)
adb shell dmesg -c
典型输出示例:
[ 12.345678] Hello World from Android Kernel Module!
[ 20.123456] Goodbye, Kernel Module Unloaded.
4.5 内核模块生命周期流程图
下图展示了从编写代码到查看日志的完整流程,以及内核模块在加载/卸载时的状态变化。
4.6 常见问题与排查
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
insmod: failed to insert |
内核版本不匹配 / 缺少依赖 | 检查 uname -r 与编译内核版本一致 |
dmesg 无输出 |
日志级别被抑制 / 未正确调用 printk |
使用 dmesg -n 7 设置日志级别 |
编译报错:missing MODULE_LICENSE() |
未声明许可证 | 添加 MODULE_LICENSE("GPL") |
rmmod: module not found |
模块名拼写错误 / 未加载 | 使用 lsmod 确认模块名 |
最佳实践:
- 每次修改代码后,先
make clean再重新编译,避免缓存干扰。 - 在 Android 设备上调试时,建议先
dmesg -c清空日志,再加载模块,便于定位输出。 - 将
hello.ko和Makefile纳入版本控制,作为后续驱动开发的模板。
至此,你已经完成了第一个内核模块的编写、编译、加载和日志查看。这个基础流程将贯穿整个 GPIO 驱动开发课程,后续所有驱动模块都将沿用相同的框架。