Linux I/O 模型怎么区分:阻塞、非阻塞、多路复用与异步
比较阻塞式 I/O、非阻塞式 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O,重点区分数据准备阶段与数据复制阶段。
相关工具
先把一次 I/O 拆成两个阶段
网络 I/O 或磁盘 I/O 看起来像一个动作,实际上通常包含两个阶段。第一阶段是等待数据准备好,例如网络数据报到达内核缓冲区;第二阶段是把数据从内核缓冲区复制到进程自己的用户空间缓冲区。
不同 I/O 模型的主要差异,不只是“会不会阻塞”,还在于进程等待的是哪一个阶段、谁负责通知阶段完成,以及数据复制发生时进程是否仍然需要参与。先分清这两个阶段,再比较各种模型,就不容易把非阻塞、多路复用和异步 I/O 混成一件事。
数据先在内核侧准备,再复制到进程缓冲区;不同模型改变的是等待和通知方式。
阻塞式 I/O:等待条件满足再继续
阻塞式 I/O 是最直观的方式。进程调用读取接口后,如果内核还没有准备好数据,调用就停在原地,线程进入等待状态;当数据准备完成并复制到用户空间后,调用返回,进程继续执行下一步。
等待期间线程通常不会持续占用 CPU,而是把处理器让给其他可运行任务。它的好处是程序结构简单,调用结果和控制流程比较自然;代价是一个线程被某个慢 I/O 占住时,不能同时推进其他独立连接,除非使用更多线程或进程。
非阻塞式 I/O:先问一次,暂时没有就返回
非阻塞式 I/O 不会让进程一直停在读取调用上。数据尚未准备好时,内核立即返回一个表示暂时不可用的结果,进程可以去做其他工作,稍后再尝试。数据准备好后,读取操作才真正把内容复制到用户缓冲区。
如果程序用紧密循环不断重试,就会变成轮询,消耗大量 CPU。非阻塞本身只解决“调用不被卡住”,并没有自动解决“什么时候再来尝试”。通常需要配合定时策略、I/O 多路复用或事件循环,才能避免空转。
阻塞调用停在内核等待,非阻塞调用立即返回,程序需要安排后续检查。
I/O 多路复用:把等待集中到一个入口
I/O 多路复用通过 select、poll 或 epoll 同时监视多个文件描述符。进程阻塞在多路复用调用上,而不是阻塞在某一个具体读取调用上;当任意一个描述符变为可读或可写时,多路复用调用返回,进程再对就绪描述符执行真正的读写。
它适合连接数量多、同时活跃连接较少的场景。一个线程可以等待许多对象,减少为每个连接单独创建线程的需要。但数据复制仍然需要由进程调用读取或写入完成,因此多路复用属于同步 I/O 范畴,不能等同于内核完成全部数据传输后再通知的异步 I/O。
信号驱动 I/O:由信号提醒可以开始读取
信号驱动 I/O 先为套接字安装 SIGIO 处理方式,随后读取调用可以立即返回。等数据报准备好后,内核向进程发送 SIGIO,信号处理函数或主循环据此知道现在值得进行读取。
这种模型把“数据已经准备好”的通知从主动查询变成事件提醒,等待期间进程可以继续工作。不过信号处理存在异步上下文限制,多个事件的合并、信号丢失后的状态确认以及处理函数的复杂度,都需要谨慎设计。
异步 I/O:提交后等待完成通知
异步 I/O 的关键是把整个操作交给内核。进程提交读取请求时,同时提供描述符、用户缓冲区、长度和偏移等信息,然后立即去做其他工作。内核负责等待数据准备并完成向用户空间的复制,操作真正完成后再通知进程。
它与信号驱动 I/O 的区别在通知时机:信号驱动 I/O 主要告诉进程“现在可以开始读取”,异步 I/O 告诉进程“这次读取已经完成”。这两个时机看起来接近,实际却决定了数据复制由谁发起、进程何时能够直接使用结果。
阻塞、非阻塞、多路复用、信号驱动和异步 I/O 在等待阶段与数据复制阶段承担的工作不同。
同步与异步到底在说什么
同步 I/O 强调调用方需要参与完成数据读取:数据准备好后,进程发起的读取操作还要把数据复制到用户空间,调用才能完成。阻塞、非阻塞和多路复用虽然等待方式不同,但都属于这种同步关系。
异步 I/O 则把等待和数据复制都交给内核,调用方在提交请求后不必参与中间过程,完成时再读取结果状态。这里的同步与异步不是“是否创建线程”的同义词,而是描述 I/O 操作完成责任和通知时机。
为什么轮询容易浪费 CPU
非阻塞 I/O 如果配合无限循环检查,就会形成轮询:程序不停调用读取接口,反复得到“暂时没有数据”,直到某一次终于成功。等待期间没有有效工作,但 CPU 仍在执行检查指令。
轮询可以提供较直接的响应路径,却要用计算资源换取低等待延迟。实际系统通常会根据场景选择事件通知、定时等待或多路复用,让线程在没有事件时休眠,事件到来后再被唤醒。延迟和 CPU 使用率之间没有脱离场景的绝对答案。
怎样根据场景选择 I/O 模型
程序简单、并发较低时,阻塞式 I/O 便于理解和维护;需要在一个线程中管理多个连接时,可以使用 I/O 多路复用;需要被动接收数据准备通知时,可以考虑信号驱动方式;确实希望由系统完成整个操作并在完成后通知时,才适合讨论异步 I/O。
选择之前还要看设备类型、平台支持、数据量、超时要求和团队维护能力。模型名称不是性能保证,错误处理、缓冲区管理、连接数量、业务计算和内核接口实现同样会影响结果。先明确等待目标,再决定机制,通常比先追逐某个术语更可靠。
用一条主线记住五种 I/O 模型
阻塞式 I/O 让进程等待数据和复制完成;非阻塞式 I/O 让调用立即返回,进程自行安排重试;I/O 多路复用让进程等待多个描述符的就绪状态;信号驱动 I/O 用信号提醒可以开始读取;异步 I/O 则由内核完成整个操作后通知结果。
它们都在处理同一件事:外部设备和网络数据比 CPU 慢,程序需要决定怎样等待、怎样被通知、谁来完成复制。只要抓住“数据何时准备好、数据何时进入用户缓冲、调用方何时继续工作”这三个问题,五种模型的差异就能自然展开。
常见问题
非阻塞 I/O 就是异步 I/O 吗?
不是。非阻塞只表示调用暂时不能完成时立即返回,数据准备好后仍需要程序发起读取;异步 I/O 则由内核完成整个操作后通知结果。
I/O 多路复用是不是让数据自动复制到用户空间?
不是。多路复用只报告哪些描述符已经就绪,程序仍要调用读取或写入接口完成数据交换。
为什么阻塞式 I/O 也可以支持并发?
可以通过多个线程或进程分别等待不同连接实现,但线程和进程数量、上下文切换以及共享状态管理会带来额外成本。