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则把两个阶段都交给内核,实现了真正的全异步。

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

相关推荐
hweiyu001 小时前
Linux命令:column
linux·运维·服务器
oooo_z1 小时前
WebSocket实时行情推送API实战:用iTick搭一个盯盘小工具
网络·websocket·网络协议
Csxyzj1 小时前
kubernetes集群部署方法
java·linux·kubernetes
SQL-First布道者1 小时前
全面解构传统持久层框架,拥抱真正的 SQL-First
java·数据库·spring boot·sql·spring·mybatis·spring jdbc
深念Y1 小时前
OnlyOffice 打开来自 Windows/WPS 的 Office 文档时,出现排版错乱、文字空白、字体显示异常。
linux·windows·debian·onlyoffice·字体·wps·office
ShineWinsu2 小时前
对于 C++:C++20中Concept(概念) 与 Coroutine(协程)的解析
linux·开发语言·网络·c++·c++20·epoll
翼龙云_cloud2 小时前
阿里云国际版代理商:OSS自定义域名绑定与HTTPS配置排障教程
数据库·阿里云·https·云计算
日常筹谋记2 小时前
ATS选不对运维两行泪:数据中心市电与柴发切换场景的双电源自动转换开关选型笔记
运维·数据库·笔记
绿蚁新亭2 小时前
Markdown Wiki 网页代码块复制至欧拉 Linux Shell 脚本隐形字符故障完整复盘
linux·运维·服务器