Linux内核模块开发:Android驱动编译与加载流程 - 第4节

4.1 模块代码结构总览

一个最简的 Linux 内核模块由两部分核心函数构成:模块入口函数(初始化)和模块出口函数(清理)。在 Android 内核驱动开发中,这两个函数是内核与模块交互的唯一接口。下面通过经典的 “Hello World” 模块来拆解其代码骨架。

关键概念:模块入口函数使用 module_init() 宏注册,在模块被加载时自动调用;模块出口函数使用 module_exit() 宏注册,在模块被卸载时自动调用。这两个宏必须成对出现,否则内核无法正确管理模块生命周期。
内核模块生命周期状态机 未加载状态 运行状态 已卸载状态 insmod module_init() 执行 rmmod 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") 版本号(可选)
Hello World 模块代码结构 头文件包含:module.h / kernel.h / init.h 入口函数 hello_init() __init 修饰,返回 0 表示成功 出口函数 hello_exit() __exit 修饰,无返回值 注册宏:module_init(hello_init) + module_exit(hello_exit) 模块信息声明 MODULE_LICENSE / MODULE_AUTHOR / MODULE_DESCRIPTION / MODULE_VERSION

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 加载与卸载步骤

  1. 编译模块:在模块源码目录执行 make,生成 hello.ko。
  2. 推送到设备:adb push hello.ko /data/local/tmp/
  3. 加载模块:adb shell insmod /data/local/tmp/hello.ko
  4. 查看日志:adb shell dmesg | tail -5,应看到 “Hello, Android Kernel!”
  5. 卸载模块:adb shell rmmod hello
  6. 再次查看日志:应看到 “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、中断、设备节点)都被正确释放。一个健壮的出口函数是防止内核崩溃的第一道防线。