4. HDF驱动消息机制:消息队列、驱动间通信与数据交互
驱动开发做到一定阶段,你会发现一个绕不开的问题——驱动之间怎么说话?驱动和用户程序怎么传数据?
HDF框架在这块做得挺成熟。它提供了三套机制:消息队列用于驱动内部或驱动间异步通信,ServiceManager用于服务发现与调用,用户态与内核态数据交互则解决了跨权限数据传递的难题。
我个人习惯把这三种机制看作一个通信工具箱。不同场景,选不同工具。下面我们一个一个拆开看。
4.1 HDF消息队列(HdfMessage)
消息队列,说白了就是一个异步通信管道。驱动A往队列里扔一条消息,驱动B在另一端取走处理。发送方不用等接收方处理完,接收方也不用一直轮询——有消息来了自然会被唤醒。
4.1.1 消息队列的核心结构
HDF的消息队列基于 HdfMessage 结构体。每个消息包含消息ID、数据指针、数据长度、以及一个回调函数(可选)。
struct HdfMessage {
uint32_t msgId; // 消息ID,用于区分消息类型
const void *data; // 消息数据指针
uint32_t dataLen; // 数据长度
HdfMsgCallback callback; // 可选,处理完成后的回调
void *priv; // 私有数据
};
我在项目中遇到过一个问题:消息队列的深度设得太小,导致高并发场景下消息丢失。后来我改成根据实际业务峰值来配置队列深度,才彻底解决。
4.1.2 消息队列的创建与使用
创建消息队列很简单,调用 HdfMessageQueueCreate 即可。你需要指定队列名称和最大消息数。
// 创建消息队列
struct HdfMessageQueue *queue = HdfMessageQueueCreate("sensor_queue", 64);
if (queue == NULL) {
HDF_LOGE("Failed to create message queue");
return HDF_FAILURE;
}
// 发送消息
struct HdfMessage msg;
msg.msgId = MSG_SENSOR_DATA_READY;
msg.data = sensorData;
msg.dataLen = sizeof(struct SensorData);
msg.callback = NULL;
int32_t ret = HdfMessageQueueSend(queue, &msg, HDF_WAIT_FOREVER);
if (ret != HDF_SUCCESS) {
HDF_LOGE("Send message failed, ret=%d", ret);
}
// 接收消息(通常在另一个线程或驱动中)
struct HdfMessage recvMsg;
ret = HdfMessageQueueRecv(queue, &recvMsg, HDF_WAIT_FOREVER);
if (ret == HDF_SUCCESS) {
// 处理消息
HandleSensorData(recvMsg.data, recvMsg.dataLen);
}
HDF_WAIT_FOREVER,一定要确保接收端不会死锁。我曾经在中断上下文里用这个参数,结果队列满了直接卡死整个系统。后来改成 HDF_NO_WAIT 配合重试机制才搞定。
4.1.3 消息队列的典型应用场景
| 场景 | 说明 | 推荐队列深度 |
|---|---|---|
| 传感器数据上报 | 驱动采集数据后异步通知上层 | 16~32 |
| 按键事件分发 | 按键驱动向多个监听者广播事件 | 8~16 |
| 电源状态切换 | 电源管理驱动通知各外设驱动 | 32~64 |
| 调试日志输出 | 驱动内部日志异步写入缓冲区 | 128~256 |
4.2 驱动间通信:ServiceManager
消息队列适合点对点或广播场景。但如果驱动A想调用驱动B的某个接口,比如读取温度传感器的值,用消息队列就有点绕了。这时候就需要 ServiceManager。
ServiceManager 是HDF框架里的服务注册中心。每个驱动可以把自己提供的服务注册进去,其他驱动通过服务名来查找并调用。
4.2.1 服务注册与发现流程
服务注册的流程其实很简单:驱动初始化时,调用 HdfServiceManagerRegister 把自己的服务接口暴露出去。其他驱动通过 HdfServiceManagerGetService 获取服务句柄,然后直接调用接口函数。
// 服务端:注册服务
struct IDeviceIoService *g_tempService = NULL;
int32_t TempDriverBind(struct HdfDeviceObject *deviceObject) {
static struct ITempService tempService = {
.GetTemperature = TempGetTemperature,
.SetThreshold = TempSetThreshold,
};
g_tempService = &tempService;
return HDF_SUCCESS;
}
int32_t TempDriverInit(struct HdfDeviceObject *deviceObject) {
// 注册到ServiceManager
if (HdfServiceManagerRegister("temperature_service", g_tempService) != HDF_SUCCESS) {
HDF_LOGE("Register temperature service failed");
return HDF_FAILURE;
}
return HDF_SUCCESS;
}
// 客户端:获取服务并调用
struct ITempService *tempService = NULL;
int32_t GetTemperature(void) {
if (tempService == NULL) {
tempService = (struct ITempService *)
HdfServiceManagerGetService("temperature_service");
if (tempService == NULL) {
HDF_LOGE("Get temperature service failed");
return HDF_FAILURE;
}
}
return tempService->GetTemperature();
}
"模块名_功能名" 的格式,比如 "sensor_temperature"。
4.2.2 服务调用的同步与异步
ServiceManager 默认提供同步调用。但如果你需要异步调用,可以结合消息队列来实现:客户端发一条消息给服务端,服务端处理完后通过回调返回结果。
我个人建议:简单查询用同步,耗时操作用异步。比如读取温度值这种毫秒级操作,同步调用完全够用。但如果是要启动一个固件升级流程,可能耗时几秒钟,那就必须用异步,否则会阻塞整个调用链。
4.3 用户态与内核态数据交互
这是驱动开发里最头疼的问题之一。用户程序跑在用户态,驱动跑在内核态,两者地址空间隔离。怎么把数据安全地传过去?
HDF 提供了几种方式,我挑最常用的两种讲:ioctl 和 mmap。
4.3.1 ioctl:控制命令与数据交换
ioctl 是最传统的方式。用户程序通过 ioctl() 系统调用向驱动发送命令,同时携带数据。驱动在 DevHandleIoctl 中解析命令并处理数据。
// 用户态代码
int fd = open("/dev/hdf_sensor", O_RDWR);
struct SensorConfig config = {
.sampleRate = 100,
.mode = SENSOR_MODE_CONTINUOUS,
};
ioctl(fd, CMD_SET_SENSOR_CONFIG, &config);
// 内核态驱动代码
int32_t SensorDevIoctl(struct HdfDeviceObject *device, int cmd, unsigned long arg) {
switch (cmd) {
case CMD_SET_SENSOR_CONFIG: {
struct SensorConfig config;
// 从用户态拷贝数据到内核态
if (copy_from_user(&config, (void *)arg, sizeof(config)) != 0) {
return HDF_ERR_INVALID_PARAM;
}
// 应用配置
return ApplySensorConfig(&config);
}
default:
return HDF_ERR_NOT_SUPPORT;
}
}
关键点:用户态传进来的指针不能直接解引用。必须用 copy_from_user 和 copy_to_user 做安全拷贝。这是内核开发的基本功,也是最容易出bug的地方。
4.3.2 mmap:大块数据的高效传输
如果每次传输的数据量很大,比如摄像头的一帧图像,用 ioctl 拷贝来拷贝去效率太低。这时候用 mmap 更合适。
mmap 的原理是:驱动在内核态分配一块物理连续的内存,然后映射到用户进程的地址空间。用户程序直接读写这块内存,零拷贝。
// 驱动端:实现mmap接口
int32_t SensorDevMmap(struct HdfDeviceObject *device, struct HdfMmapInfo *mmapInfo) {
// 分配物理连续内存
void *kernelBuf = DmaAllocCoherent(device, BUFFER_SIZE, &physAddr, GFP_KERNEL);
if (kernelBuf == NULL) {
return HDF_ERR_MALLOC_FAIL;
}
// 映射到用户态
mmapInfo->vaddr = kernelBuf;
mmapInfo->paddr = physAddr;
mmapInfo->size = BUFFER_SIZE;
return HDF_SUCCESS;
}
// 用户态:映射并使用
int fd = open("/dev/hdf_sensor", O_RDWR);
void *mapAddr = mmap(NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mapAddr == MAP_FAILED) {
perror("mmap failed");
return -1;
}
// 直接读写映射内存
struct FrameData *frame = (struct FrameData *)mapAddr;
ProcessFrame(frame);
4.3.3 三种交互方式对比
| 方式 | 适用场景 | 数据量 | 性能 | 复杂度 |
|---|---|---|---|---|
| ioctl | 控制命令、小数据量 | < 4KB | 中等 | 低 |
| mmap | 大块数据、实时性要求高 | 任意大小 | 高(零拷贝) | 中 |
| 消息队列 | 异步通知、事件分发 | < 64KB | 高 | 中 |
4.4 本章知识结构图
下面这张图把三种通信机制的关系和典型流程画出来了。你可以对照着理解整个消息传递的脉络。
4.5 避坑指南与实战建议
- 消息队列的线程安全:HDF的消息队列本身是线程安全的,但消息中的数据需要你自己保护。我习惯在消息结构体里放一个互斥锁指针,接收方拿到消息后先加锁再处理数据。
- ServiceManager的依赖顺序:如果驱动A依赖驱动B的服务,一定要确保B先初始化完成。我曾在系统启动阶段遇到服务找不到的问题,后来在HCS配置里加了
priority和stage字段来控制加载顺序。 - 用户态指针校验:永远不要信任用户态传进来的指针。即使它看起来合法,也要用
copy_from_user做安全拷贝。我曾经因为偷懒直接解引用用户态指针,结果系统崩溃了三次才找到原因。 - mmap的内存释放:mmap映射的内存必须在驱动卸载时释放。如果忘记释放,会造成内存泄漏。我建议在驱动的
Release接口里统一清理所有mmap资源。