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

📅 2026/8/23 8:34:35
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 函数#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可以封装一个将文件描述符设为非阻塞的工具函数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 轮询方式读取标准输入下面的例子将标准输入设为非阻塞然后循环读取#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则把两个阶段都交给内核实现了真正的全异步。理解这些模型的区别是学习网络编程和高性能服务器开发的基础。