4. HDF消息机制:驱动与用户态通信的几种方式

驱动写好了,总得跟用户态程序打交道吧?

我刚开始接触HDF时,最困惑的就是这个问题——驱动在内核里跑,应用程序在用户空间,它们怎么互相传数据?

说白了,这就是我们今天要聊的驱动与用户态通信。在鸿蒙的HDF框架里,主要有两种方式:IOCTLHDF消息队列。嗯,咱们一个一个来看。

4.1 IOCTL:最传统的通信方式

IOCTL,全称是Input/Output Control。你想想看,它其实就是给驱动发个“控制命令”。

我在项目中遇到过不少新手,一上来就问:“IOCTL是不是只能传整数?” 其实不是的,它能传结构体,能传缓冲区,灵活得很。

4.1.1 IOCTL的基本流程

用户态调用ioctl(),内核态驱动里有个.ioctl回调函数来响应。大致流程是这样的:

  1. 用户态程序打开设备文件(比如/dev/hdf_xxx
  2. 调用ioctl(fd, cmd, arg),传入命令码和参数
  3. 驱动里的DispatchIoctl函数被触发
  4. 驱动处理完,把结果返回给用户态

我个人习惯把IOCTL命令码定义成枚举,清晰又好维护。给你看个例子:

// 用户态代码
int fd = open("/dev/hdf_led", O_RDWR);
if (fd < 0) {
    printf("open device failed\n");
    return -1;
}

struct led_config cfg = {
    .pin = 10,
    .brightness = 80,
};

int ret = ioctl(fd, LED_SET_BRIGHTNESS, &cfg);
if (ret != 0) {
    printf("ioctl failed\n");
}

close(fd);
// 驱动端代码(简化版)
static int HdfLedDriverIoctl(struct HdfDeviceObject *device, struct HdfSBuf *sbuf)
{
    struct led_config cfg;
    // 从sbuf中解析参数
    if (HdfSbufReadBuffer(sbuf, &cfg, sizeof(cfg)) != HDF_SUCCESS) {
        HDF_LOGE("failed to read buffer");
        return HDF_FAILURE;
    }

    // 根据命令码处理
    // ... 设置GPIO、调节亮度等

    return HDF_SUCCESS;
}
我的小技巧:IOCTL命令码最好用宏定义或枚举,别用裸数字。我曾经在一个项目里看到满屏的“0x100”、“0x101”,调试时根本分不清哪个是哪个。后来统一改成枚举,世界清静了。

4.1.2 IOCTL的优缺点

优点 缺点
简单直接,Linux/鸿蒙都支持 每次调用都要陷入内核,开销大
适合小数据量的控制命令 传大数据时效率低
调试方便,可以用strace跟踪 命令码容易冲突(如果定义不好)
注意:IOCTL传指针时一定要做合法性检查!我曾经见过一个驱动,用户态传了个空指针进来,驱动没判断就直接解引用,结果内核直接panic了。嗯,血的教训。

4.2 HDF消息队列:更高效的通信方式

IOCTL虽然好用,但如果你要频繁传数据,比如传感器每秒上报几百次数据,那IOCTL就不太行了。每次都要系统调用,性能扛不住。

这时候,HDF消息队列就派上用场了。

4.2.1 消息队列的原理

说白了,消息队列就是驱动和用户态之间共享一个缓冲区。驱动往里面写消息,用户态从里面读消息。两边各干各的,不用每次都“握手”。

HDF里消息队列的实现,底层用的是共享内存加上信号量做同步。我建议你把它理解成一个“环形缓冲区”,驱动写,用户读,谁也别抢。

4.2.2 怎么用消息队列

在HDF里,消息队列是通过HdfMessageQueue这个接口来操作的。大致步骤:

  1. 驱动端创建消息队列,注册到HDF框架
  2. 用户态通过HDF服务获取到消息队列的句柄
  3. 驱动调用HdfMessageQueueSend()发送消息
  4. 用户态调用HdfMessageQueueRecv()接收消息

给你看个驱动端的示例:

// 驱动端:发送消息
static int SendSensorData(struct HdfDeviceObject *device, struct sensor_data *data)
{
    struct HdfMessageQueue *queue = device->queue; // 假设已初始化
    if (queue == NULL) {
        HDF_LOGE("queue is null");
        return HDF_FAILURE;
    }

    struct HdfMessage msg = {0};
    msg.type = SENSOR_DATA_TYPE;
    msg.len = sizeof(struct sensor_data);
    msg.data = (uint8_t *)data;

    int ret = HdfMessageQueueSend(queue, &msg, HDF_WAIT_FOREVER);
    if (ret != HDF_SUCCESS) {
        HDF_LOGE("send message failed, ret=%d", ret);
        return ret;
    }

    return HDF_SUCCESS;
}

用户态这边,接收消息就更简单了:

// 用户态:接收消息
struct HdfMessageQueue *queue = HdfMessageQueueOpen("/dev/hdf_sensor");
if (queue == NULL) {
    printf("open queue failed\n");
    return -1;
}

struct HdfMessage msg;
while (1) {
    int ret = HdfMessageQueueRecv(queue, &msg, 1000); // 超时1秒
    if (ret == HDF_SUCCESS) {
        // 处理消息
        struct sensor_data *data = (struct sensor_data *)msg.data;
        printf("sensor value: %d\n", data->value);
        HdfMessageFreeData(&msg);
    } else if (ret == HDF_ERR_TIMEOUT) {
        // 超时,继续等
        continue;
    } else {
        printf("recv failed\n");
        break;
    }
}

HdfMessageQueueClose(queue);
核心区别:IOCTL是“一问一答”,消息队列是“生产-消费”。如果你的驱动需要频繁上报数据,消息队列是更好的选择。

4.3 怎么选?我的经验

你可能会问:“那我到底该用哪个?”

我个人的经验是这样的:

  • 控制类操作(比如开灯、关灯、设置参数)→ 用IOCTL。简单,够用。
  • 数据上报类(比如传感器数据、音频流)→ 用消息队列。性能好,不阻塞。
  • 混合场景 → 两个都用。IOCTL做控制,消息队列做数据通道。我在一个智能家居项目里就是这么干的,效果很好。
避坑指南:我曾经在一个项目里,消息队列的缓冲区设得太小,驱动写满了,用户态还没读完,结果数据丢了。后来我把缓冲区调大,并且加了水位线通知机制,问题就解决了。嗯,缓冲区大小一定要根据数据量来算,别拍脑袋。

4.4 小结

今天聊了两种通信方式:IOCTL和HDF消息队列。IOCTL适合控制命令,消息队列适合数据流。你想想看,其实它们各有各的用武之地,关键是要选对场景。

下一章,我们会聊聊HDF的事件回调机制——驱动怎么主动通知用户态“我有数据了”。嗯,那个也很有意思。