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 函数。

这个函数的调用链是这样的:

  1. Framework调用 hw_get_module(HELLO_HARDWARE_MODULE_ID, &module)
  2. 加载器根据ID拼接路径:/vendor/lib64/hw/hello.default.so
  3. 用 dlopen 打开这个so文件
  4. 用 dlsym 查找 HAL_MODULE_INFO_SYM 符号
  5. 返回 hw_module_t 指针给调用者

说白了,就是一套动态库加载的封装。但这里有个坑:路径优先级。

我曾经踩过的坑: 系统会按 ro.product.board、ro.board.platform、ro.arch 等属性拼接不同的变体路径。比如 hello.$(TARGET_BOARD).so。如果你只放了 hello.default.so,但系统在找 hello.goldfish.so,那就加载失败。

加载流程可以用下面这张图来理解:

HAL模块加载流程 Framework 调用 hw_get_module 拼接模块路径 dlopen 打开 .so dlsym 查找 HAL_MODULE_INFO_SYM 返回 hw_module_t 指针 路径拼接规则 /vendor/lib64/hw/ <module_id>.<variant>.so variant = default | board | platform

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在内核态,它们之间怎么通信?

常用的方式有三种:

  1. ioctl:最常用,通过文件描述符发送控制命令
  2. sysfs:通过读写 /sys 下的文件节点
  3. 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交互链路 Framework / App HAL Module (用户态) Kernel Driver (内核态) 硬件设备 HIDL / hw_get_module ioctl / sysfs / mmap

最后说一个调试技巧。当你怀疑HAL和Kernel Driver通信有问题时,用 strace 是最快的:

adb shell strace -p $(pidof hello_service) -e ioctl,open,read,write

这样能直接看到HAL进程调用了哪些系统调用,传了什么参数,返回值是什么。比看日志高效多了。