Android内核驱动开发基础 - 第4节
4.1 内核模块概念
Linux 内核是一个单体内核(Monolithic Kernel),但通过内核模块(Kernel Module)机制实现了动态扩展能力。内核模块是一段可以在运行时加载到内核或从内核卸载的代码,它使得内核功能可以按需添加,无需重新编译整个内核或重启系统。
核心优势:模块化设计让 Android 内核驱动开发变得灵活——驱动可以作为模块独立编译、调试和分发,大幅缩短开发周期。
4.1.1 模块与静态编译的对比
| 特性 | 内核模块(.ko) | 静态编译(built-in) |
|---|---|---|
| 加载方式 | 运行时动态加载/卸载 | 内核启动时自动包含 |
| 内核体积 | 按需加载,节省内存 | 所有功能常驻内存 |
| 开发调试 | 可独立编译、快速迭代 | 需重新编译整个内核 |
| 启动速度 | 不影响内核启动时间 | 增加内核镜像大小和加载时间 |
| 适用场景 | 驱动开发、文件系统、协议栈扩展 | 核心调度、内存管理、必须的驱动 |
4.1.2 模块的生命周期
一个内核模块从编写到运行经历以下阶段:
- 编写源码——实现模块入口和出口函数。
- 编译——通过 Makefile 生成
.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 |
设备不存在 | 硬件未检测到 |
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
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 表示正常运行。
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 编译与测试步骤
- 准备内核源码树:下载 Android 内核源码,执行
make defconfig和make modules_prepare。 - 编译模块:
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-android- -C $(KERNEL_DIR) M=$(PWD) modules - 推送模块到设备:
adb push hello_module.ko /data/local/tmp/ - 加载测试:
adb shell su -c "insmod /data/local/tmp/hello_module.ko" - 验证:
adb shell lsmod | grep hello和adb shell dmesg | tail - 卸载:
adb shell su -c "rmmod hello_module"
常见问题排查:
- insmod: failed to insert —— 检查 dmesg 查看具体错误(如符号未定义、版本不匹配)。
- rmmod: module not found —— 确认模块名正确(不带 .ko 后缀)。
- lsmod 显示 Used by 不为 0 —— 有其他模块依赖当前模块,需先卸载依赖模块。
4.6 本章小结
- 内核模块是 Linux 内核动态扩展的核心机制,Android 驱动开发中广泛使用。
- module_init 和 module_exit 是模块的入口和出口,必须正确实现。
- Makefile 通过 Kbuild 系统编译模块,Android 交叉编译需指定 ARCH 和 CROSS_COMPILE。
- insmod / lsmod / rmmod 是模块管理三板斧,配合 dmesg 查看内核日志。
- 掌握本章内容后,即可开始编写和测试自己的第一个 Android 内核驱动模块。