Android内核驱动开发基础 - 第4节

4.1 内核模块概念

Linux 内核是一个单体内核(Monolithic Kernel),但通过内核模块(Kernel Module)机制实现了动态扩展能力。内核模块是一段可以在运行时加载到内核或从内核卸载的代码,它使得内核功能可以按需添加,无需重新编译整个内核或重启系统。

核心优势:模块化设计让 Android 内核驱动开发变得灵活——驱动可以作为模块独立编译、调试和分发,大幅缩短开发周期。

4.1.1 模块与静态编译的对比

特性 内核模块(.ko) 静态编译(built-in)
加载方式 运行时动态加载/卸载 内核启动时自动包含
内核体积 按需加载,节省内存 所有功能常驻内存
开发调试 可独立编译、快速迭代 需重新编译整个内核
启动速度 不影响内核启动时间 增加内核镜像大小和加载时间
适用场景 驱动开发、文件系统、协议栈扩展 核心调度、内存管理、必须的驱动

4.1.2 模块的生命周期

一个内核模块从编写到运行经历以下阶段:

  1. 编写源码——实现模块入口和出口函数。
  2. 编译——通过 Makefile 生成 .ko 文件。
  3. 加载——使用 insmod 将模块插入内核空间。
  4. 运行——模块在内核态执行,注册设备、创建文件节点等。
  5. 卸载——使用 rmmod 移除模块,释放资源。
内核模块生命周期 编写源码 编译 .ko insmod 加载 内核运行 rmmod 卸载 模块加载后进入内核空间,拥有最高特权级;卸载时必须释放所有已申请资源

4.2 模块加载与卸载函数

每个内核模块必须实现两个核心函数:模块初始化函数和模块清理函数。它们分别对应模块加载和卸载时的回调。

4.2.1 标准函数原型

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

// 模块初始化函数(加载时调用)
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, Android Kernel Module loaded!\n");
    return 0;  // 返回0表示成功,负值表示失败
}

// 模块清理函数(卸载时调用)
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, Android Kernel Module unloaded!\n");
}

// 注册入口和出口
module_init(hello_init);
module_exit(hello_exit);

// 模块信息声明(可选但推荐)
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Android kernel module example");
关键点:
  • __init 和 __exit 宏将函数放入特殊节区,加载/卸载后释放内存。
  • module_init() 和 module_exit() 是固定宏,不可改名。
  • printk() 是内核空间的打印函数,通过 dmesg 查看输出。

4.2.2 返回值与错误处理

返回值 含义 常见场景
0 加载成功 初始化正常完成
-ENOMEM 内存不足 kmalloc 分配失败
-EINVAL 参数无效 传入非法参数
-EBUSY 资源被占用 设备或中断号已被使用
-ENODEV 设备不存在 硬件未检测到
模块加载/卸载函数调用关系 用户空间 insmod / rmmod 系统调用层 init_module / delete_module 内核模块 module_init() / module_exit() insmod 通过系统调用 init_module 进入内核,内核调用模块的 module_init 函数 rmmod 通过 delete_module 触发 module_exit,模块释放资源后从内核移除

4.3 Makefile 编写

内核模块的编译依赖内核源码树提供的 Kbuild 系统。标准的 Makefile 模板如下:

# 指定内核源码树路径(Android 交叉编译时需指向目标内核)
KERNEL_DIR ?= /path/to/android/kernel

# 当前模块名称
obj-m := hello_module.o

# 如果模块由多个源文件组成,使用如下格式:
# hello_module-objs := file1.o file2.o

all:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean

# 安装模块到临时目录(可选)
install:
    $(MAKE) -C $(KERNEL_DIR) M=$(PWD) modules_install

4.3.1 Makefile 关键变量说明

变量/语法 作用 示例
obj-m 指定要编译为模块的目标文件 obj-m := mydrv.o
obj-y 指定静态编译进内核的目标 obj-y := core.o
-C $(KERNEL_DIR) 切换到内核源码目录读取顶层 Makefile -C /kernel/msm-4.19
M=$(PWD) 告诉 Kbuild 返回当前目录编译模块 M=/home/user/module
modules 目标:编译所有 obj-m 指定的模块 make modules
modules_install 将编译好的 .ko 安装到 /lib/modules/ make modules_install
Android 交叉编译注意事项:
  • 必须设置 ARCH=arm64 和 CROSS_COMPILE=aarch64-linux-android-。
  • KERNEL_DIR 指向 Android 内核源码树,需提前执行 make defconfig 和 make modules_prepare。
  • 编译命令示例:make ARCH=arm64 CROSS_COMPILE=aarch64-linux-android- -C $(KERNEL_DIR) M=$(PWD) modules
Makefile 编译内核模块流程 编写 .c 源码 编写 Makefile make 命令编译 生成 .ko 文件 make 命令会调用内核 Kbuild 系统,依次执行:预处理 → 编译 → 链接 → 生成 .ko 最终产物为 hello_module.ko,可使用 file 命令查看 ELF 格式确认

4.4 insmod / lsmod / rmmod 命令

这三个命令是内核模块管理的基本工具,在 Android 设备上通常通过 adb shell 进入终端后执行。

4.4.1 命令详解

命令 功能 语法示例 注意事项
insmod 加载指定路径的 .ko 模块到内核 insmod /data/hello.ko 需要 root 权限;模块路径必须绝对或相对正确
lsmod 列出当前已加载的所有模块 lsmod 输出包含模块名、大小、被引用次数、依赖列表
rmmod 卸载指定名称的模块 rmmod hello_module 模块名不带 .ko 后缀;模块不能被其他模块引用

4.4.2 实际操作示例

# 1. 加载模块
console:/ $ su
console:/ # insmod /data/local/tmp/hello_module.ko

# 2. 查看模块是否加载成功
console:/ # lsmod | grep hello
hello_module 16384 0 - Live 0x0000000000000000 (O)

# 3. 查看内核日志确认初始化输出
console:/ # dmesg | tail
[ 1234.567890] Hello, Android Kernel Module loaded!

# 4. 卸载模块
console:/ # rmmod hello_module

# 5. 确认卸载
console:/ # lsmod | grep hello
(无输出,表示已卸载)

# 6. 查看卸载日志
console:/ # dmesg | tail
[ 1234.890123] Goodbye, Android Kernel Module unloaded!
lsmod 输出字段说明:
  • Module:模块名称,与 .ko 文件名对应(不含后缀)。
  • Size:模块占用的内存大小(字节)。
  • Used by:被其他模块引用的次数,若不为 0 则无法卸载。
  • Live:模块当前状态,Live 表示正常运行。
insmod / lsmod / rmmod 工作流程 用户空间 (adb shell) insmod hello.ko lsmod rmmod hello_module 内核空间 模块链表操作:插入 / 遍历 / 删除 + 调用 init/exit 函数 所有模块操作均需 root 权限;内核通过模块链表维护所有已加载模块的信息

4.5 完整示例:从零编写一个 Android 内核模块

以下是一个完整的开发流程,包含源码、Makefile 和测试步骤。

4.5.1 项目文件结构

hello_module/
├── hello_module.c      # 模块源码
├── Makefile            # 编译脚本
└── README.md           # 说明文档(可选)

4.5.2 完整源码

// hello_module.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>       // 文件操作相关
#include <linux/uaccess.h>  // copy_to/from_user

#define DEVICE_NAME "hello_device"

static int __init hello_init(void)
{
    printk(KERN_INFO "[Hello] Module loaded, device name: %s\n", DEVICE_NAME);
    // 实际驱动中会在此注册字符设备、分配设备号等
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "[Hello] Module unloaded\n");
    // 实际驱动中会在此注销设备、释放资源
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Android Kernel Developer");
MODULE_DESCRIPTION("Hello World Android Kernel Module");
MODULE_VERSION("1.0");

4.5.3 编译与测试步骤

  1. 准备内核源码树:下载 Android 内核源码,执行 make defconfig 和 make modules_prepare。
  2. 编译模块:make ARCH=arm64 CROSS_COMPILE=aarch64-linux-android- -C $(KERNEL_DIR) M=$(PWD) modules
  3. 推送模块到设备:adb push hello_module.ko /data/local/tmp/
  4. 加载测试:adb shell su -c "insmod /data/local/tmp/hello_module.ko"
  5. 验证:adb shell lsmod | grep hello 和 adb shell dmesg | tail
  6. 卸载:adb shell su -c "rmmod hello_module"
常见问题排查:
  • insmod: failed to insert —— 检查 dmesg 查看具体错误(如符号未定义、版本不匹配)。
  • rmmod: module not found —— 确认模块名正确(不带 .ko 后缀)。
  • lsmod 显示 Used by 不为 0 —— 有其他模块依赖当前模块,需先卸载依赖模块。
Android 内核模块完整开发流程 编写源码 编写 Makefile 交叉编译 adb 推送 insmod 测试 开发环境要求: 1. Android NDK 或交叉编译工具链(aarch64-linux-android-) 2. 目标设备对应的内核源码树(需执行 modules_prepare) 3. 已 root 的 Android 设备或模拟器

4.6 本章小结

  • 内核模块是 Linux 内核动态扩展的核心机制,Android 驱动开发中广泛使用。
  • module_init 和 module_exit 是模块的入口和出口,必须正确实现。
  • Makefile 通过 Kbuild 系统编译模块,Android 交叉编译需指定 ARCH 和 CROSS_COMPILE。
  • insmod / lsmod / rmmod 是模块管理三板斧,配合 dmesg 查看内核日志。
  • 掌握本章内容后,即可开始编写和测试自己的第一个 Android 内核驱动模块。