Linux—五种IO模型与非阻塞IO

一、IO的两个阶段

任何一次IO操作,本质上都经历两个阶段:等待数据就绪将数据从内核拷贝到用户空间

在实际场景里,等待往往占据了IO的绝大部分时间。因此,提升IO效率的思路,就是尽可能减少进程在等待阶段的空耗。围绕"如何等待"这一问题,操作系统演化出了五种经典的IO模型。


二、五种IO模型

2.1 阻塞IO

阻塞IO是最朴素也最常见的模型。进程调用 recvfrom 后,如果内核中数据尚未准备好,该调用就不会返回,进程被挂起,直到数据就绪并完成拷贝,才得到成功指示。

整个过程中,进程从发起调用到拿到数据全程处于阻塞状态,无法执行其他任务。

2.2 非阻塞IO

与阻塞IO不同,非阻塞IO下,如果内核数据未就绪,recvfrom 会立即返回,并带回 EWOULDBLOCK 错误码,而不是让进程挂起。

程序需要通过循环反复调用 recvfrom 来探测数据是否就绪,这一机制称为轮询

2.3 信号驱动IO

信号驱动IO的思路是:进程提前通过 sigaction 注册一个 SIGIO 信号处理函数,然后继续执行自身任务。

当内核数据准备完毕时,主动向进程递送 SIGIO 信号,进程在信号处理函数中调用 recvfrom 完成数据拷贝。

相比轮询,信号驱动IO避免了反复探测,进程在等待期间可以正常运行。

注意:数据从内核拷贝到用户空间的阶段,进程仍然是阻塞的。

2.4 IO多路转接

IO多路转接(IO多路复用)的核心在于:一个进程可以同时监视多个文件描述符,通过 select 等系统调用等待其中任意一个变为就绪状态。

一旦有描述符可读,进程再针对该描述符执行 recvfrom。

从单条流程看,它与阻塞IO颇为相似,但本质区别在于---多路转接能以一个线程的代价同时管理大量连接。

2.5 异步IO

异步IO是最彻底的非阻塞方案。进程调用 aio_read 后立即返回,内核自行完成等待数据和拷贝数据两个阶段,全部结束后再通过信号或回调通知进程。

注意差异:信号驱动IO通知的是"数据已就绪,可以开始拷贝了";而异步IO通知的是"数据已经拷贝到用户空间,可以直接使用了"。


三、模型对比

模型 等待阶段 拷贝阶段 核心机制 CPU利用率 适用场景
阻塞IO 阻塞 阻塞 调用即等待 简单、连接数少
非阻塞IO 不阻塞 阻塞 循环反复尝试 特定专用场景
信号驱动IO 不阻塞 阻塞 SIGIO信号通知 连接数中等
IO多路转接 阻塞 阻塞 select/poll/epoll 高并发网络服务
异步IO 不阻塞 不阻塞 内核完成后通知 最高 高性能异步框架

前四种模型在数据拷贝阶段都需要进程阻塞参与,只有异步IO实现了真正意义上的全流程非阻塞。


四、高级IO的概念

4.1 同步通信 vs 异步通信

同步与异步关注的是消息通信机制

  • 同步:调用发出后,在拿到结果之前不返回;一旦返回,必然带回结果

  • 异步:调用发出后立即返回,没有结果;被调用者后续通过状态、通知或回调函数来告知调用者

注意:这里的"同步"与多线程中"同步与互斥"的同步是完全不同的概念。后者描述的是线程间为协调执行次序而产生的直接制约关系,尤其是在访问临界资源时。

4.2 阻塞 vs 非阻塞

阻塞与非阻塞关注的是进程在等待结果时的状态

  • 阻塞:结果返回之前,当前线程被挂起,只有得到结果后才会返回

  • 非阻塞:不能立刻得到结果时,调用不会阻塞当前线程,立即返回

4.3 四者关系

同步/异步回答的是"结果怎么拿",阻塞/非阻塞回答的是"等的时候在干嘛"。二者可以组合:

  • 同步阻塞:发起调用后干等,结果到手才返回

  • 同步非阻塞:发起调用后立即返回,但要自己反复去查结果

  • 异步阻塞:发起调用后立即返回,但用另一个阻塞操作去等通知

  • 异步非阻塞:发起调用后立即返回,完事了对方主动通知,全程不卡

五、非阻塞IO的实现

5.1 fcntl 函数

一个文件描述符默认是阻塞IO。要将其改为非阻塞,需要借助 fcntl 函数

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

fcntl 根据 cmd 的不同具备多个功能:

cmd 功能
F_DUPFD 复制一个现有的描述符
F_GETFD / F_SETFD 获得/设置文件描述符标记
F_GETFL / F_SETFL 获得/设置文件状态标记
F_GETOWN / F_SETOWN 获得/设置异步IO所有权
F_GETLK / F_SETLK / F_SETLKW 获得/设置记录锁

设置非阻塞用到的是第三种功能------获取/设置文件状态标记。

5.2 SetNoBlock 函数

基于 fcntl,可以封装一个将文件描述符设为非阻塞的工具函数:

cpp 复制代码
void SetNoBlock(int fd) {
    int fl = fcntl(fd, F_GETFL);   // 取出当前文件状态标记(位图)
    if (fl < 0) {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);  // 叠加O_NONBLOCK标志后写回
}

先用 F_GETFL 读出原有标记,再通过按位或加上 O_NONBLOCK,最后用 F_SETFL 写回。

5.3 轮询方式读取标准输入

下面的例子将标准输入设为非阻塞,然后循环读取:

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

void SetNoBlock(int fd) {
    int fl = fcntl(fd, F_GETFL);
    if (fl < 0) {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main() {
    SetNoBlock(0);  // 将标准输入设为非阻塞
    while (1) {
        char buf[1024] = {0};
        ssize_t read_size = read(0, buf, sizeof(buf) - 1);
        if (read_size < 0) {
            perror("read");   // 无数据时返回EWOULDBLOCK,打印错误
            sleep(1);
            continue;
        }
        printf("input: %s\n", buf);
    }
    return 0;
}

当用户没有输入时,read 会立即返回 -1 并将 errno 置为 EWOULDBLOCK,程序打印错误后休眠1秒再继续尝试。


六、总结

五种IO模型的差异,本质上集中在"如何等待数据就绪"和"谁来完成数据拷贝"这两个问题上。

阻塞IO简单但效率低,非阻塞IO靠轮询避免挂起却浪费CPU,信号驱动IO用通知替代轮询,IO多路转接以单线程管理多连接成为高并发首选,异步IO则把两个阶段都交给内核,实现了真正的全异步。

理解这些模型的区别,是学习网络编程和高性能服务器开发的基础。

相关推荐
逐米时代8 小时前
工控网络安全:安全与连续性双目标下的精准防护
网络
Yang96118 小时前
通信广电与无线电管理:鼎讯信通ZN-080A手持式信号综合分析仪能做什么
网络
宽海智能仓储物流8 小时前
航发自动化立体库异构系统改造:宽海智能30天联调技术全解析
运维·数据仓库·自动化
鹤落晴春9 小时前
【K8s】从 ConfigMap 到 Argo CD:Kubernetes 配置管理链路
运维·云原生·容器·kubernetes
2601_949499949 小时前
DT‑1414PTZ如何百分百兼容(安华高)HFBR‑1414PTZ
运维·网络·人工智能·科技·光模块
fengyehongWorld9 小时前
Linux 安装frp实现内网穿透
linux·运维·服务器
一_个前端9 小时前
[TailScale]-打通你的局域网服务器到公网电脑
linux
香菜TTT9 小时前
Redis的哨兵机制
java·数据库·redis
DO_Community9 小时前
Omarchy 将研发基础设施迁移至 DigitalOcean 云平台
linux·人工智能·llm·agent·omarchy
sibylyue9 小时前
WebSocket双向长连接传输通道
网络·websocket·网络协议