4. 资源管理器核心API:io_open、io_read、io_write、io_close等关键函数解析

好,咱们今天来聊聊资源管理器里最核心的几个API。说白了,一个设备驱动或者文件系统,能不能正常工作,就看这几个函数写得对不对。我刚开始接触QNX时,觉得这些API不就是打开、读写、关闭嘛,有啥难的?后来踩了坑才明白,里面的门道可不少。

4.1 io_open:一切交互的起点

io_open 是资源管理器被调用的第一个函数。当用户态程序调用 open() 时,消息最终会落到你注册的这个回调里。

它的原型长这样:

int io_open(resmgr_context_t *ctp, io_open_t *msg, 
            RESMGR_HANDLE_T *handle, void *extra);

这里有几个关键点,我一个个说。

第一,路径解析。 你收到的 msg->connect.path 是去掉挂载点之后的相对路径。比如你的资源管理器挂载在 /dev/mydev,用户打开 /dev/mydev/sensor0,你拿到的就是 /sensor0

第二,权限检查。 别偷懒。我见过有人直接在 io_open 里返回 EOK,结果用户随便一个进程都能打开你的设备。正确的做法是检查 msg->connect.ioflag 里的 O_RDONLYO_WRONLY 等标志。

核心要点: io_open 必须返回一个句柄。这个句柄会伴随整个会话周期,后续的 io_readio_write 都能拿到它。

举个例子,我习惯在 io_open 里分配一个上下文结构体:

typedef struct {
    int fd;          // 底层硬件描述符
    uint32_t flags;  // 打开标志
    void *priv;      // 私有数据
} mydev_context_t;

int io_open(resmgr_context_t *ctp, io_open_t *msg,
            RESMGR_HANDLE_T *handle, void *extra) {
    mydev_context_t *ctx = calloc(1, sizeof(mydev_context_t));
    if (!ctx) return ENOMEM;

    // 检查权限
    if ((msg->connect.ioflag & O_RDONLY) && !(perms & PERM_READ)) {
        free(ctx);
        return EACCES;
    }

    // 初始化硬件
    ctx->fd = open_hardware();
    if (ctx->fd < 0) {
        free(ctx);
        return ENODEV;
    }

    // 关键:把上下文挂到句柄上
    SETIOV(ctp->ioinfo, ctx, sizeof(ctx));
    return EOK;
}

我的经验: 曾经有个项目,我在 io_open 里忘记检查 O_NONBLOCK 标志。结果上层应用用非阻塞方式打开,我的驱动却一直阻塞在硬件初始化上。嗯,调试了一整天才发现。

4.2 io_read 与 io_write:数据的搬运工

这两个函数是资源管理器最繁忙的地方。它们的原型几乎一样:

int io_read(resmgr_context_t *ctp, io_read_t *msg,
            RESMGR_OCB_T *ocb);
int io_write(resmgr_context_t *ctp, io_write_t *msg,
             RESMGR_OCB_T *ocb);

注意看,这里多了一个 ocb 参数。它就是你在 io_open 里挂上去的那个上下文。每次读写操作,QNX内核都会把这个句柄传回来。

读操作的核心逻辑:

  1. msg->ocb->offset 获取当前读写位置
  2. 检查 msg->nbytes 确定要读多少字节
  3. 把数据拷贝到 ctp->msg->i 指向的缓冲区
  4. 返回实际读取的字节数

写操作类似,只是数据流向相反。

来看一个典型的 io_read 实现:

int io_read(resmgr_context_t *ctp, io_read_t *msg,
            RESMGR_OCB_T *ocb) {
    mydev_context_t *ctx = ocb->context;
    int nbytes = msg->nbytes;
    int offset = msg->ocb->offset;
    char buffer[256];

    // 从硬件读取数据
    int actual = read_from_hardware(ctx->fd, buffer, 
                                    min(nbytes, sizeof(buffer)), offset);
    if (actual < 0) return EIO;

    // 把数据返回给用户
    SETIOV(ctp->iov, buffer, actual);
    msg->ocb->offset += actual;  // 更新偏移量

    return actual;
}

注意: io_read 返回的字节数可以小于请求的字节数。这很正常,比如硬件FIFO里只有10个字节,用户要100个,你就返回10。上层应用会自己处理短读。

你想想看,如果每次都要从硬件读,性能肯定上不去。我建议在 io_read 里加个简单的缓存机制。比如读SD卡时,一次读一个扇区(512字节),然后缓存起来。下次读同一个扇区就直接从缓存拿。

4.3 io_close:善后工作不能马虎

io_close 在用户调用 close() 时被触发。它的原型:

int io_close(resmgr_context_t *ctp, io_close_t *msg,
             RESMGR_OCB_T *ocb);

这里有个容易忽略的点:io_close 可能会被调用多次。为什么?因为QNX的 close() 会先发一个 _IO_CLOSE 消息,如果资源管理器返回 EOK,内核还会再发一个 _IO_CLOSE_DUP 消息。所以你的清理逻辑要放在 _IO_CLOSE_DUP 里。

我一般这样处理:

int io_close(resmgr_context_t *ctp, io_close_t *msg,
             RESMGR_OCB_T *ocb) {
    mydev_context_t *ctx = ocb->context;

    if (msg->flag & _IO_CLOSE_DUP) {
        // 真正的清理工作
        close_hardware(ctx->fd);
        free(ctx);
        ocb->context = NULL;
    }

    return EOK;
}

避坑指南: 我曾经在 io_close 里直接 free(ctx),结果 _IO_CLOSE_DUP 再来时,访问了野指针。程序直接崩溃。记住:只有 _IO_CLOSE_DUP 才是最后一次调用。

4.4 其他关键API一览

除了上面四个,还有几个API也经常用到。我列个表,方便你对照:

API 触发时机 典型用途
io_lseek 用户调用 lseek() 修改文件读写位置
io_devctl 用户调用 devctl() 设备控制命令(如设置波特率)
io_mmap 用户调用 mmap() 内存映射设备寄存器
io_notify 用户调用 select()/poll() 事件通知机制

这些API的调用流程都一样:用户态发起系统调用 → 内核构造消息 → 资源管理器收到消息 → 你实现的回调被调用 → 返回结果。

4.5 实战建议:消息处理的性能优化

最后,分享几个我在项目中总结的优化技巧:

  • 减少内存拷贝: 能用 SETIOV 直接指向内核缓冲区的,就别自己 memcpy。我见过有人每次读写都多拷贝一次,性能直接掉30%。
  • 批量处理: 如果设备支持,尽量一次处理多个请求。比如读NAND Flash,一次读一页(2KB)比一次读一个字节快得多。
  • 异步处理: 对于慢速设备(如串口),考虑用 io_notify 配合线程池。别在主线程里阻塞等待硬件。
  • 错误码要准确: 返回 EAGAIN 表示可以重试,返回 EIO 表示硬件错误。上层应用会根据错误码做不同处理。

嗯,核心API就这些。说白了,资源管理器就是个消息处理器。你把 io_openio_readio_writeio_close 这四个函数写好了,一个基本的设备驱动就成型了。剩下的就是根据具体硬件特性,往里面填充业务逻辑。

下一章,我会讲讲资源管理器的多线程模型。到时候你会看到,怎么让这些API在并发环境下安全运行。