4、HAL实现基础:编写第一个HAL模块、HAL模块加载机制、HAL Stub实现、HAL与Kernel Driver交互
好,我们正式开始动手了。这一章是HAL开发的“地基”。你想想看,如果连一个最基本的HAL模块都写不出来,后面那些性能调优、调试技巧全是空中楼阁。
我个人习惯把HAL模块拆成三块来看:模块本身的结构、系统怎么把它加载进来、以及它怎么跟底层驱动对话。这三块搞明白了,HAL开发就算入门了。
4.1 第一个HAL模块:从hello_world开始
先别想太复杂。我们写一个最简单的HAL模块,就叫它 hello。它的功能就一个:返回一个字符串“Hello from HAL!”。
一个标准的HAL模块,需要包含三个核心文件:
| 文件 | 作用 | 说明 |
|---|---|---|
hello.h |
定义接口 | 暴露给上层Framework的API |
hello.c |
实现逻辑 | 真正的业务代码 |
Android.bp |
编译配置 | 告诉构建系统怎么编译这个模块 |
先看头文件。这里有个关键点:HAL模块必须继承 hw_module_t 结构体。
// hello.h
#include <hardware/hardware.h>
#define HELLO_HARDWARE_MODULE_ID "hello"
typedef struct hello_device {
struct hw_device_t common;
int (*get_message)(struct hello_device* dev, char* buffer, int size);
} hello_device_t;
嗯,这里要注意。 hw_device_t 是HAL的“身份证”,每个硬件设备都必须用它打头。我刚开始写的时候,经常忘记把 common 放在结构体第一个字段,结果加载时直接崩溃。
再看实现文件:
// hello.c
#include <hardware/hardware.h>
#include <cutils/log.h>
#include "hello.h"
static int hello_close(struct hw_device_t* device) {
free(device);
return 0;
}
static int hello_get_message(struct hello_device* dev, char* buffer, int size) {
strncpy(buffer, "Hello from HAL!", size - 1);
buffer[size - 1] = '\0';
return 0;
}
static int hello_open(const struct hw_module_t* module, const char* id,
struct hw_device_t** device) {
struct hello_device* dev = malloc(sizeof(struct hello_device));
memset(dev, 0, sizeof(*dev));
dev->common.tag = HARDWARE_DEVICE_TAG;
dev->common.version = 1;
dev->common.module = (struct hw_module_t*)module;
dev->common.close = hello_close;
dev->get_message = hello_get_message;
*device = &dev->common;
return 0;
}
static struct hw_module_methods_t hello_methods = {
.open = hello_open,
};
struct hw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.version_major = 1,
.version_minor = 0,
.id = HELLO_HARDWARE_MODULE_ID,
.name = "Hello HAL Module",
.author = "Your Name",
.methods = &hello_methods,
};
核心要点: HAL_MODULE_INFO_SYM 这个符号名是固定的。HAL加载器就是通过这个符号找到你的模块入口。拼写错了,模块就加载不上。
最后是编译文件:
// Android.bp
cc_library_shared {
name: "hello.default",
relative_install_path: "hw",
proprietary: true,
srcs: ["hello.c"],
shared_libs: ["liblog"],
}
编译出来就是一个 hello.default.so,放到 /vendor/lib64/hw/ 目录下。
4.2 HAL模块加载机制:hw_get_module 是怎么工作的
模块写好了,Framework怎么找到它?答案是 hw_get_module 函数。
这个函数的调用链是这样的:
- Framework调用
hw_get_module(HELLO_HARDWARE_MODULE_ID, &module) - 加载器根据ID拼接路径:
/vendor/lib64/hw/hello.default.so - 用
dlopen打开这个so文件 - 用
dlsym查找HAL_MODULE_INFO_SYM符号 - 返回
hw_module_t指针给调用者
说白了,就是一套动态库加载的封装。但这里有个坑:路径优先级。
我曾经踩过的坑: 系统会按 ro.product.board、ro.board.platform、ro.arch 等属性拼接不同的变体路径。比如 hello.$(TARGET_BOARD).so。如果你只放了 hello.default.so,但系统在找 hello.goldfish.so,那就加载失败。
加载流程可以用下面这张图来理解:
4.3 HAL Stub实现:为什么需要Stub
Stub这个词,说白了就是“桩”。在HAL里,Stub是一个最小可运行的实现版本。
为什么需要它?我遇到过好几次这种情况:硬件还没回来,但上层Framework已经要联调了。这时候Stub就派上用场了——它模拟硬件行为,返回固定数据,让上层能先跑起来。
Stub的实现其实很简单,就是在 hello_open 里返回模拟数据:
static int hello_get_message_stub(struct hello_device* dev, char* buffer, int size) {
// 模拟硬件返回
strncpy(buffer, "Stub: No hardware available", size - 1);
buffer[size - 1] = '\0';
return 0;
}
我的建议: 在Stub里加一个 ALOGI 日志,打印“Stub mode activated”。这样调试时一眼就能看出当前用的是Stub还是真硬件。
Stub和真实现的切换,通常通过编译配置或运行时属性来控制:
| 切换方式 | 优点 | 缺点 |
|---|---|---|
| 编译时宏 | 无运行时开销 | 需要重新编译 |
| 系统属性 | 动态切换,无需编译 | 有轻微性能损耗 |
| 设备节点检测 | 自动适配,最智能 | 实现复杂 |
4.4 HAL与Kernel Driver交互:打通用户态和内核态
这是HAL最核心的部分。HAL在用户态,Kernel Driver在内核态,它们之间怎么通信?
常用的方式有三种:
- ioctl:最常用,通过文件描述符发送控制命令
- sysfs:通过读写
/sys下的文件节点 - mmap:共享内存,适合大数据量传输
以ioctl为例,HAL里是这样调用的:
static int hello_ioctl(struct hello_device* dev, int cmd, void* arg) {
int fd = dev->fd; // 设备文件描述符
return ioctl(fd, cmd, arg);
}
static int hello_open_device(const struct hw_module_t* module, const char* id,
struct hw_device_t** device) {
struct hello_device* dev = malloc(sizeof(struct hello_device));
memset(dev, 0, sizeof(*dev));
// 打开设备节点
dev->fd = open("/dev/hello", O_RDWR);
if (dev->fd < 0) {
ALOGE("Failed to open /dev/hello: %s", strerror(errno));
return -errno;
}
// ... 其他初始化
*device = &dev->common;
return 0;
}
我曾经踩过的坑: 设备节点权限问题。HAL进程可能没有 /dev/hello 的读写权限。解决方案是在 init.rc 里设置 chmod 0666 /dev/hello,或者把HAL进程加入对应的用户组。
Kernel Driver那边,需要实现对应的 unlocked_ioctl 函数:
static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case HELLO_GET_MESSAGE:
// 处理获取消息的命令
if (copy_to_user((void __user *)arg, "Hello from Kernel!", 18)) {
return -EFAULT;
}
return 0;
default:
return -EINVAL;
}
}
static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = hello_ioctl,
.open = hello_open,
.release = hello_release,
};
整个交互链路是这样的:
最后说一个调试技巧。当你怀疑HAL和Kernel Driver通信有问题时,用 strace 是最快的:
adb shell strace -p $(pidof hello_service) -e ioctl,open,read,write
这样能直接看到HAL进程调用了哪些系统调用,传了什么参数,返回值是什么。比看日志高效多了。