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_RDONLY、O_WRONLY 等标志。
核心要点: io_open 必须返回一个句柄。这个句柄会伴随整个会话周期,后续的 io_read、io_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内核都会把这个句柄传回来。
读操作的核心逻辑:
- 从
msg->ocb->offset获取当前读写位置 - 检查
msg->nbytes确定要读多少字节 - 把数据拷贝到
ctp->msg->i指向的缓冲区 - 返回实际读取的字节数
写操作类似,只是数据流向相反。
来看一个典型的 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_open、io_read、io_write、io_close 这四个函数写好了,一个基本的设备驱动就成型了。剩下的就是根据具体硬件特性,往里面填充业务逻辑。
下一章,我会讲讲资源管理器的多线程模型。到时候你会看到,怎么让这些API在并发环境下安全运行。