操作系统的 IO 模型有哪些?


一则或许对你有用的小广告

欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书

  • 《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于 Spring AI + Spring Boot 3.x + JDK 21...查看介绍

  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...查看介绍;演示链接:http://116.62.199.48:7070/

  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/

  • 新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍

截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 基础掌握度:面试官可不是想听你背 5 种模型的名字,他想搞清楚你明不明白每种模型的工作流程、阻塞点在哪里、数据是怎么从内核走到用户空间的。

  2. 底层理解深度:考察你是否理解 “同步/异步” 和 “阻塞/非阻塞” 的本质区别,以及 I/O 操作的两个阶段(等待数据、拷贝数据)。这块答得好不好,一下就能看出你是真懂还是临时背的。

  3. 工程联系能力:能否把 IO 模型和实际中间件对应起来,比如 Redis 为什么快、Nginx 为什么能扛高并发、Java NIO 底层用的是哪种模型。能联想到这一层,面试官会觉得你是真用过、真思考过。

核心答案

操作系统的 I/O 模型一共有 5 种,按同步/异步分类如下:

IO 模型 第一阶段(等数据) 第二阶段(拷数据) 同步/异步
阻塞 IO(BIO) 阻塞 阻塞 同步
非阻塞 IO(NIO) 非阻塞(轮询) 阻塞 同步
IO 多路复用 阻塞(可监听多个) 阻塞 同步
信号驱动 IO 非阻塞(等信号) 阻塞 同步
异步 IO(AIO) 不阻塞 不阻塞 异步

前 4 种都是同步 IO,只有最后一种 AIO 才是真正的异步 IO。判断标准就看数据拷贝阶段会不会阻塞线程,只要第二阶段还阻塞,它就是同步的。

深度解析

一、先搞清楚一次 IO 到底发生了什么

理解 5 种模型的前提,是先搞明白一次网络 read 操作的完整过程。应用程序发起一次读操作,内核要干两件事:

IO 两阶段流程
IO 两阶段流程

  • 阶段一:等待数据就绪。对于网络 IO 来说,数据要经过网卡 → 内核协议栈 → 内核接收缓冲区,这个过程是异步发生的,应用程序根本插不上手,只能等内核把数据准备好。

  • 阶段二:数据拷贝。数据到了内核缓冲区后,还得从内核空间拷贝到用户空间,这一步是 CPU 干的活,应用程序照样得等。

这 5 种 IO 模型,差别就在于 怎么处理这两个阶段:是傻等、轮询、等通知,还是交给内核全包。

IO 等数据和拷数据
IO 等数据和拷数据

二、5 种 IO 模型逐一拆解

IO 模型阻塞点
IO 模型阻塞点

1. 阻塞 IO(Blocking IO)

最朴素的模型,也是默认行为。应用程序调用 recvfrom(),整个流程都阻塞:

阻塞 IO 流程
阻塞 IO 流程

特点:全程阻塞,一个连接占一个线程。Java 最早的 ServerSocket.accept() 就是这种模型,所以传统 BIO 服务端要用线程池来支撑多个连接,连接数一上去,线程数也跟着爆炸。

2. 非阻塞 IO(Non-blocking IO)

把 socket 设置成 O_NONBLOCK,调用 recvfrom() 立刻返回:

  • 数据没来:返回 EWOULDBLOCK 错误码
  • 数据来了:才真正去拷贝

应用程序通常会写一个 while 循环不断轮询

while (true) {
    int ret = recvfrom(fd, buf, size, ...);
    if (ret != EWOULDBLOCK) {
        // 数据来了,处理
        break;
    }
    // 没数据,继续轮询
}

特点:第一阶段不阻塞了,但代价是 CPU 空转,效率反而更低。所以纯非阻塞 IO 在实际工程里几乎没人用,它存在的意义更多是为多路复用做铺垫。

3. IO 多路复用(IO Multiplexing)⭐ 重头戏

这是高并发场景的绝对主力。核心思路:一个线程同时监听多个 fd(文件描述符),哪个 fd 有数据了再去处理哪个。

关键系统调用有三个:selectpollepoll,一步步演进:

对比项 select poll epoll
数据结构 bitmap 链表 红黑树 + 双向链表(就绪队列)
最大连接数 1024(FD_SETSIZE) 无上限 无上限
时间复杂度 O(n) 遍历全部 fd O(n) 遍历全部 fd O(1),只返回就绪的
fd 拷贝 每次调用全量拷贝 每次调用全量拷贝 只在添加时拷贝一次
工作方式 每次都轮询 每次都轮询 事件驱动,回调通知
适用场景 连接数少 连接数少 高并发(千万级连接)

epoll 强就强在 不再每次都把所有 fd 拷到内核里轮询,而是:

  • 注册 fd 时,给每个 socket 绑一个回调
  • 网卡把数据送到内核缓冲区,内核顺手触发回调,把这个 fd 塞进 “就绪链表”
  • 应用程序调用 epoll_wait(),只拿就绪的 fd,不用遍历全部

epoll 还有两种触发模式:

epoll 就绪队列
epoll 就绪队列

  • LT(水平触发,默认):只要缓冲区还有数据没读完,每次 epoll_wait 都会通知你。容易写,但可能多次唤醒。
  • ET(边缘触发):只在状态变化时通知一次,后面不再提醒。要求应用程序必须用非阻塞 IO + 循环读到 EAGAIN,不然数据就漏了。Nginx 就是这种模式

顺便说一句,Java NIO 的 Selector 底层在 Linux 上就是用的 epoll,Netty 也是基于它做的封装。Redis、Nginx 高性能的根本原因,也是 epoll + 单线程事件循环。

4. 信号驱动 IO(Signal-driven IO)

思路是:先告诉内核 “数据来了就给我发个 SIGIO 信号”,然后应用程序该干嘛干嘛,不阻塞。等信号真的来了,再调用 recvfrom() 把数据拷过来。

信号驱动 IO
信号驱动 IO

这个模型看着挺优雅,但实际用得很少,因为信号处理本身就比较麻烦,而且信号可能会合并、丢失,可靠性不如 epoll。

5. 异步 IO(Asynchronous IO)

真正的异步模型。应用程序告诉内核 “我要读这个 fd,读完了把数据放到这个 buffer,然后用回调通知我”,然后就彻底不管了。内核 两个阶段全包,包括数据拷贝。

异步 IO 流程
异步 IO 流程

Linux 下对应 aio_read(),但 Linux 的 AIO 实现并不算成熟,性能也不突出。真正把异步 IO 玩明白的是 Windows 的 IOCP——IIS 服务器就是靠它支撑高并发的。Java 7 引入了 AIO(NIO.2),但在 Linux 上的底层实现还是 epoll 模拟的,并没有真正用上 Linux 原生 AIO,这也是为什么 Netty 直接放弃了 AIO,死磕 NIO。

三、一张图对比 5 种模型的阻塞点

IO 模型对比
IO 模型对比

判断一个 IO 是同步还是异步,看的就是 数据拷贝阶段(第二阶段)是否需要应用程序自己处理、是否会阻塞。前 4 种第二阶段都要自己 recvfrom() 把数据从内核拷到用户空间,所以都是同步;AIO 的拷贝由内核完成,应用程序只等回调,所以才是真正的异步。

面试高频追问

  1. 追问一:epoll 为什么比 select/poll 高效?

核心是 事件驱动 + 共享内存。select/poll 每次都要把全部 fd 拷到内核里遍历一遍,复杂度 O(n);epoll 只在注册时拷贝一次 fd,数据就绪时由内核通过回调把 fd 放进就绪链表,epoll_wait() 只取就绪的,复杂度 O(1)。连接数越多,差距越大。

  1. 追问二:epoll 的 LT 和 ET 模式怎么选?

LT 模式简单不易出错,但可能多次唤醒;ET 模式效率更高,但要求 非阻塞 IO + 循环读取直到 EAGAIN,编码复杂度更高。Redis 主要用 LT(编程简单、单线程瓶颈不在网络),Nginx 用 ET(追求极致吞吐)。新手项目建议先 LT 稳着来。

  1. 追问三:Java NIO、Netty 对应哪种 IO 模型?

Java NIO 的 Selector 底层在 Linux 上就是 epoll(其他平台用 kqueue 或 select)。Netty 默认也是基于 epoll 的 NIO,单线程事件循环 + 就绪事件分发,和 Redis、Nginx 的架构思路一模一样。

  1. 追问四:AIO 在 Java 里为什么没火起来?

主要是 Linux 原生 AIO 实现不成熟,Java AIO 在 Linux 上还是用 epoll 模拟,享受不到真正异步的红利。Netty 团队评估后干脆放弃了 AIO,全力优化 NIO,这也反过来让 AIO 在 Java 生态里没什么存在感。

常见面试变体

  • "同步和异步、阻塞和非阻塞有什么区别?"
  • "selectpollepoll 的区别是什么?"
  • "为什么 Redis 单线程还能这么快?"(答案里必然有 epoll)
  • "Java NIO 的核心是什么?"(Channel + Buffer + Selector,底层就是多路复用)

记忆口诀

5 种模型:阻塞 → 非阻塞 → 多路复用 → 信号驱动 → 异步 IO

判同步异步看第二阶段:数据拷贝阶段要不要自己干,要的就是同步,不要的就是异步。

epoll 三大优势:红黑树管 fd、就绪链表取数据、回调驱动不轮询。

总结

操作系统的 IO 模型一共 5 种,差别就在于 怎么处理 “等数据” 和 “拷数据” 这两个阶段。前 4 种都是同步 IO(拷数据阶段都会阻塞),只有 AIO 才是真正的异步。实际工程里,IO 多路复用(epoll)是绝对主力,Redis、Nginx、Netty 都靠它撑起高并发。把 epoll 的原理、select/poll/epoll 的对比、LT/ET 的差异讲清楚,再配上 Java NIO 和 Netty 的关联,面试官基本就稳了。