Linux内核模块开发:Android驱动编译与加载流程 - 第4节
4.1 模块代码结构总览
一个最简的 Linux 内核模块由两部分核心函数构成:模块入口函数(初始化)和模块出口函数(清理)。在 Android 内核驱动开发中,这两个函数是内核与模块交互的唯一接口。下面通过经典的 “Hello World” 模块来拆解其代码骨架。
关键概念:模块入口函数使用
module_init() 宏注册,在模块被加载时自动调用;模块出口函数使用 module_exit() 宏注册,在模块被卸载时自动调用。这两个宏必须成对出现,否则内核无法正确管理模块生命周期。
4.2 Hello World 模块完整代码
以下代码展示了一个最简内核模块的完整实现。该模块在加载时打印 “Hello, Android Kernel!”,卸载时打印 “Goodbye, Kernel!”。
#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, Android Kernel!\n");
/* 返回 0 表示初始化成功 */
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module for Android kernel");
MODULE_VERSION("1.0");
Android 驱动提示:在 Android 内核中,
printk() 的输出会进入 dmesg 日志缓冲区。调试驱动时,使用 adb shell dmesg | grep "Hello" 即可查看模块加载日志,无需连接串口。
4.3 代码逐段解析
4.3.1 头文件包含
| 头文件 | 作用 |
|---|---|
<linux/module.h> |
提供 module_init、module_exit、MODULE_LICENSE 等核心宏 |
<linux/kernel.h> |
提供 printk 内核打印函数及日志级别宏(如 KERN_INFO) |
<linux/init.h> |
提供 __init 和 __exit 属性宏,优化内存布局 |
4.3.2 入口函数:hello_init
- 函数签名:
static int __init hello_init(void) __init修饰符:告诉内核该函数仅在初始化阶段使用,执行完毕后所占内存可被回收。- 返回值:成功返回 0;失败返回负的错误码(如
-ENOMEM)。 - 打印:使用
printk(KERN_INFO ...)输出信息,KERN_INFO是日志级别,表示普通信息。
4.3.3 出口函数:hello_exit
- 函数签名:
static void __exit hello_exit(void) __exit修饰符:如果模块被编译进内核(非模块方式),该函数会被忽略以节省空间。- 无返回值:出口函数不需要返回状态。
4.3.4 注册宏与模块信息
| 宏 / 声明 | 说明 |
|---|---|
module_init(hello_init) |
将 hello_init 注册为模块入口 |
module_exit(hello_exit) |
将 hello_exit 注册为模块出口 |
MODULE_LICENSE("GPL") |
声明许可证,Android 内核强制要求 GPL 兼容许可证 |
MODULE_AUTHOR(...) |
作者信息(可选) |
MODULE_DESCRIPTION(...) |
模块功能描述(可选) |
MODULE_VERSION("1.0") |
版本号(可选) |
4.4 编译与加载验证
4.4.1 Makefile 示例
在 Android 内核源码树外编译模块,需要编写如下 Makefile:
obj-m := hello.o
KDIR := /path/to/android/kernel/source
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
Android 编译注意:
KDIR 必须指向已配置好的 Android 内核源码目录(包含 vmlinux 和 Module.symvers)。如果使用 Android 通用内核(GKI),需要先执行 make defconfig 和 make modules_prepare。
4.4.2 加载与卸载步骤
- 编译模块:在模块源码目录执行
make,生成hello.ko。 - 推送到设备:
adb push hello.ko /data/local/tmp/ - 加载模块:
adb shell insmod /data/local/tmp/hello.ko - 查看日志:
adb shell dmesg | tail -5,应看到 “Hello, Android Kernel!” - 卸载模块:
adb shell rmmod hello - 再次查看日志:应看到 “Goodbye, Kernel!”
4.5 常见错误与排查
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
insmod: ERROR: could not insert module |
内核版本不匹配或缺少依赖符号 | 确认 KDIR 与设备内核版本一致;检查 Module.symvers |
module: 'hello' not found |
模块名拼写错误或未正确卸载 | 使用 lsmod 查看已加载模块列表 |
printk 无输出 |
日志级别被抑制或 dmesg 缓冲区满 |
尝试 echo 8 > /proc/sys/kernel/printk 提高日志级别 |
编译报错:implicit declaration |
缺少头文件或内核 API 变更 | 检查 Android 内核版本对应的 API 差异 |
最佳实践:在 Android 驱动开发中,建议始终在模块入口函数中检查返回值,并在出口函数中确保所有资源(内存、GPIO、中断、设备节点)都被正确释放。一个健壮的出口函数是防止内核崩溃的第一道防线。