Linux 五大 I/O 模型深度解析
在构建高并发、高性能的后端系统时如各种中间件、Web 服务器我们不可避免地会接触到 I/OInput/Output模型。很多开发者对 BIO、NIO、AIO 以及多路复用等概念感到混淆。要真正从底层掌握这些模型我们需要褪去表层的框架封装回到操作系统的本质。本文将以严谨、清晰的技术视角全面梳理 Linux 系统下的五种经典 I/O 模型。一、 核心基石搞懂两个阶段与两对概念在探讨具体的 I/O 模型之前必须明确操作系统进行一次网络 I/O以读操作 read 为例所必须经历的两个关键阶段第一阶段等待数据准备就绪Waiting for the data to be ready。例如等待网络上的数据包到达网卡并被复制到操作系统的内核缓冲区Kernel Space。第二阶段将数据从内核拷贝到用户进程Copying the data from the kernel to the process。将内核缓冲区的数据拷贝到应用程序的内存空间User Space。基于这两个阶段我们才能准确定义以下两对常被混淆的概念阻塞Blocking与 非阻塞Non-blocking这主要关注的是第一阶段。在请求数据时如果数据没准备好当前线程是被挂起死等阻塞还是直接返回一个错误标志继续往下执行非阻塞。同步Synchronous与 异步Asynchronous这主要关注的是第二阶段。在进行内核到用户空间的数据拷贝时如果是用户线程自己主动去执行拷贝并被阻塞就是同步如果是由操作系统在后台完成拷贝再通知用户线程直接使用数据就是异步。理解了这些我们来看 UNIX 网络编程中定义的 5 种 I/O 模型。1. 同步阻塞 I/O (Blocking I/O - BIO)这是最传统、最基础的 I/O 模型。工作机制当用户进程发起 recvfrom 系统调用时进入内核态。如果这个时候网络数据还没到用户进程就会进入阻塞状态交出 CPU 执行权。直到数据到达内核缓冲区并且操作系统将数据成功拷贝到用户空间后这个系统调用才返回用户进程解除阻塞。特点在 I/O 执行的两个阶段都被阻塞。应用场景传统的 Java java.io 包。通常采用“一连接一线程”的模式One Thread per Connection。痛点在面对成千上万的并发连接时需要创建等量的线程线程的上下文切换开销极大容易导致系统资源耗尽。2. 同步非阻塞 I/O (Non-blocking I/O)为了解决线程被挂起等待的问题引入了非阻塞模型。工作机制用户进程发起 recvfrom 调用时如果内核数据还没准备好操作系统不会挂起线程而是立刻返回一个 EWOULDBLOCK 错误码。用户进程收到错误后知道数据没好可以去干点别的但通常会通过一个 while 循环不断地再次重试发起调用称为轮询 Polling。直到某次轮询时数据准备好了它就会阻塞在第二阶段等待数据拷贝完成。特点第一阶段不阻塞第二阶段阻塞。痛点虽然线程没有被挂起但频繁的系统调用和死循环轮询会极大地白白消耗 CPU 资源。在实际的高并发生产环境中这种纯粹的非阻塞 I/O 直接使用的概率极低。3. I/O 多路复用 (I/O Multiplexing)这是目前现代高并发系统最核心的基础模型大名鼎鼎的 Linux epoll、Java NIO、Redis、Nginx 的底层均是基于此模型实现。工作机制它引入了一个“代理”即 select、poll 或 epoll 系统调用。用户进程将自己关注的多个文件描述符FD可以理解为多个网络连接全部注册到这个代理上。此时单个用户线程阻塞在 select/epoll 调用上。只要这批连接中有一个或多个连接的数据到达了epoll 就会返回通知用户进程。随后用户进程再发起真正的 recvfrom 系统调用阻塞在第二阶段去拷贝数据。特点看似和阻塞 I/O 差不多甚至还多了一次系统调用。但它的核心优势在于一个线程可以同时监听和管理成千上万个连接。总结它是 Reactor 架构模式的基石。第一阶段阻塞在多路复用器上而不是具体的 I/O 调用上第二阶段依然同步阻塞在数据拷贝上。4. 信号驱动 I/O (Signal-Driven I/O)这是一个相对冷门的模型。工作机制用户进程向内核注册一个信号处理函数并立即返回线程继续执行其他任务第一阶段完全不阻塞。当内核数据准备就绪时操作系统会向该进程发送一个 SIGIO 信号。用户进程收到信号后在信号处理函数中发起 recvfrom 系统调用开始第二阶段的数据拷贝此时是阻塞的。特点第一阶段通过事件通知机制避免了阻塞和轮询第二阶段同步阻塞。痛点在 TCP 协议中产生信号的条件非常多且复杂导致该模型难以被广泛应用通常多用于 UDP 通信。5. 异步 I/O (Asynchronous I/O - AIO)这是理论上最高效的 I/O 模型实现了真正的全异步。工作机制用户进程发起 aio_read 调用后立刻返回去做其他事情完全不阻塞。操作系统内核接管了后续的所有工作它不仅负责等待数据到达还负责将数据从内核空间拷贝到用户空间。当这一切全部完成后内核会主动给用户进程发送一个通知如触发回调函数告诉进程“数据已经放在你的内存里了直接用吧”。特点在 I/O 的两个阶段都不阻塞。用户进程彻底从 I/O 操作中解放出来。这是 Proactor 架构模式的基础。现状Windows 系统通过 IOCP 提供了完善的 AIO 支持但在 Linux 系统下AIO 的演进较为缓慢传统的 glibc AIO 多是基于用户态线程池模拟的直到近些年 io_uring 的出现Linux 才有了一个真正高性能的内核级异步 I/O 方案。因此目前主流的 Linux 后端应用如 Netty仍以 I/O 多路复用为主。二、 总结与对比为了应对面试或技术选型我们可以通过以下判断逻辑来区分这 5 种模型看“第二阶段”数据拷贝阶段是否阻塞只要线程需要自己去调用 recvfrom 并在此期间被阻塞那就是同步 I/O包含 BIO、NIO、多路复用、信号驱动。如果数据拷贝全由内核代劳线程完全不阻塞那就是异步 I/OAIO。(注这是很多开发者容易踩坑的地方I/O 多路复用本质上依然属于同步 I/O 模型)。看主流应用落地低并发、简单业务BIO 代码最直观易于维护。高并发、海量连接毫不犹豫选择基于 epoll 的I/O 多路复用。它是当前工业界落地最成熟、性价比最高的方案兼顾了性能与实现的复杂度。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2478265.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!