4. 非阻塞IO在内核中的实现

非阻塞IO是驱动开发中处理高速数据流或避免进程挂起的核心机制。当用户空间以 O_NONBLOCK 标志打开设备文件时,驱动必须放弃“等待数据就绪”的传统阻塞行为,转而立即返回一个特殊错误码 -EAGAIN,告知应用层“现在没有数据,请稍后再试”。本章深入剖析内核侧的实现细节。

4.1 O_NONBLOCK标志的传递路径

O_NONBLOCK 标志从用户空间的 open() 系统调用开始,经过虚拟文件系统(VFS)层,最终存入文件描述符对应的 struct file 结构体的 f_flags 字段。驱动开发者无需关心标志的传递过程,只需在读写方法中检查该字段即可。

关键数据结构: struct file 中的 unsigned int f_flags 成员存储了打开文件时的所有标志,包括 O_NONBLOCK。
O_NONBLOCK标志传递路径 用户空间 open() VFS sys_open() struct file.f_flags 标志存储位置 f_flags 是 unsigned int 类型,O_NONBLOCK 对应位被置 1 驱动 read/write 中检查 filp->f_flags

4.2 驱动中检查 O_NONBLOCK 的标准方法

在驱动的 read() 和 write() 方法中,通过检查 filp->f_flags & O_NONBLOCK 来判断当前操作是否为非阻塞模式。如果条件成立且数据未就绪(或缓冲区满),则立即返回 -EAGAIN。

// 驱动 read 方法中的典型检查
static ssize_t my_read(struct file *filp, char __user *buf,
                       size_t count, loff_t *f_pos)
{
    struct my_device *dev = filp->private_data;

    // 检查是否有数据可读
    if (dev->data_available == 0) {
        // 非阻塞模式:立即返回 -EAGAIN
        if (filp->f_flags & O_NONBLOCK) {
            return -EAGAIN;
        }
        // 阻塞模式:等待数据就绪
        if (wait_event_interruptible(dev->read_queue,
                                     dev->data_available != 0)) {
            return -ERESTARTSYS;
        }
    }

    // 执行实际的数据拷贝
    // ...
    return bytes_read;
}
最佳实践: 始终在检查数据是否就绪 之后、在可能引起阻塞的等待操作 之前 检查 O_NONBLOCK 标志。这样可以避免不必要的资源消耗。

4.3 -EAGAIN 与 -EWOULDBLOCK 的关系

在 Linux 内核中,-EAGAIN 和 -EWOULDBLOCK 在数值上完全相等(均为 11),且语义几乎相同。驱动开发者通常使用 -EAGAIN,但两种写法在内核中均可接受。

错误码 数值 含义 推荐使用场景
-EAGAIN 11 资源暂时不可用,请重试 非阻塞 IO 操作、资源锁获取失败
-EWOULDBLOCK 11 操作会阻塞,但当前不允许阻塞 非阻塞 socket 操作(网络子系统常用)
注意: 虽然两者数值相同,但在跨平台代码或用户空间库中,某些实现可能只检查 EWOULDBLOCK。为保险起见,驱动中统一使用 -EAGAIN 即可。

4.4 非阻塞 write 的实现要点

非阻塞写操作与读操作对称,但检查的是“缓冲区是否可写”。当写缓冲区已满且设置了 O_NONBLOCK 时,驱动应返回 -EAGAIN。

static ssize_t my_write(struct file *filp, const char __user *buf,
                        size_t count, loff_t *f_pos)
{
    struct my_device *dev = filp->private_data;
    size_t space_avail = dev->buf_size - dev->write_pos;

    if (space_avail == 0) {
        // 非阻塞模式:缓冲区满,立即返回
        if (filp->f_flags & O_NONBLOCK) {
            return -EAGAIN;
        }
        // 阻塞模式:等待缓冲区有空闲空间
        if (wait_event_interruptible(dev->write_queue,
                                     space_avail > 0)) {
            return -ERESTARTSYS;
        }
    }

    // 执行数据写入
    // ...
    return bytes_written;
}

4.5 非阻塞 IO 的完整决策流程

下图展示了驱动在非阻塞模式下处理读写请求的完整决策树:

非阻塞IO决策流程图 驱动 read/write 入口 检查数据是否就绪 / 缓冲区是否有空间 资源就绪? 执行数据拷贝 检查 O_NONBLOCK O_NONBLOCK 置位? 返回 -EAGAIN 阻塞等待

4.6 常见陷阱与避坑指南

  • 检查时机错误: 在等待队列上休眠 之后 才检查 O_NONBLOCK,导致进程已经休眠,违背非阻塞语义。
  • 遗漏写操作检查: 只对 read() 实现了非阻塞逻辑,而 write() 中忘记检查,导致写操作在缓冲区满时仍然阻塞。
  • 错误码混淆: 在非阻塞模式下返回 -EINVAL 或 -ENODATA,用户空间无法正确识别“资源暂时不可用”的语义。
  • 忽略 O_NONBLOCK 在 open() 中的影响: 某些驱动在 open() 中也需要检查 filp->f_flags,例如阻止非阻塞方式打开独占设备。
内核调试技巧: 使用 strace 工具跟踪系统调用,观察返回值是否为 EAGAIN (11)。如果应用层收到 Resource temporarily unavailable 错误,说明驱动正确实现了非阻塞逻辑。

4.7 本章小结

非阻塞IO的实现核心在于两点:检查 filp->f_flags & O_NONBLOCK 和 返回 -EAGAIN。驱动开发者需要将这一检查逻辑嵌入到读写方法的正确位置——在确认资源不可用之后、在进入休眠之前。掌握这一模式后,后续学习 poll 机制时将能更深刻地理解“事件通知”与“非阻塞轮询”的协同关系。