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 内核模块生命周期流程图

下图展示了从编写代码到查看日志的完整流程,以及内核模块在加载/卸载时的状态变化。

内核模块生命周期与操作流程 编写 hello.c 编写 Makefile make 编译 生成 hello.ko 内核模块文件 insmod hello.ko 加载 调用 hello_init() printk 写入日志 dmesg | grep "Hello World" 查看 rmmod hello 卸载 调用 hello_exit() 卸载时同样会 调用 printk 输出

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 驱动开发课程,后续所有驱动模块都将沿用相同的框架。