Linux的5种IO模型梳理
文章导读
- 基本概念(相关系统调用函数,同步 & 异步,阻塞 & 非阻塞)
- 阻塞 IO 模型
- 非阻塞 IO 模型
- IO 多路复用模型
- 信号驱动 IO 模型
- 异步 IO 模型
- Java 中的 BIO、NIO、AIO
1. 基本概念
Linux 下的五种 IO 模型包括:阻塞 IO(Blocking IO)、非阻塞 IO(Non-blocking IO)、IO 多路复用(IO Multiplexing)、信号驱动 IO(Signal-driven IO)、异步 IO(Asynchronous IO)。
在深入模型之前,首先需要了解相关的系统调用函数及核心概念。
1.1 系统调用函数简介
以下系统调用函数基于 Linux 环境,参考了相关技术文档:
- recvfrom
Linux 系统提供给用户用于接收网络 IO 的系统接口。从套接字上接收一个消息,可同时应用于面向连接和无连接的套接字。
如果此系统调用返回值<0,并且errno为EWOULDBLOCK或EAGAIN(套接字已标记为非阻塞,而接收操作被阻塞或者接收超时)时,连接正常,阻塞接收数据(这很关键,前 4 种 IO 模型都涉及此系统调用)。 - select
select系统调用允许程序同时在多个底层文件描述符上,等待输入的到达或输出的完成。以数组形式存储文件描述符,64 位机器默认2048个。当有数据准备好时,无法感知具体是哪个流 OK 了,所以需要一个一个地遍历,函数的时间复杂度为 O(n)。 - poll
以链表形式存储文件描述符,没有长度限制。本质与select相同,函数的时间复杂度也为 O(n)。 - epoll
基于事件驱动,如果某个流准备好了,会以事件通知,知道具体是哪个流,因此不需要遍历,函数的时间复杂度为 O(1)。 - sigaction
用于设置对信号的处理方式,也可检验对某信号的预设处理方式。Linux 使用 SIGIO 信号 来实现 IO 异步通知机制。
1.2 同步 & 异步
同步和异步是针对应用程序和内核交互而言的,也可理解为从被调用者(操作系统) 的角度来说:
- 同步(Sync):用户进程触发 IO 操作并等待或轮询地去查看是否就绪。
- 异步(Async):用户进程触发 IO 操作以后便开始做自己的事情,而当 IO 操作已经完成的时候会得到 IO 完成的通知(需要 CPU 支持)。
1.3 阻塞 & 非阻塞
阻塞和非阻塞是针对于进程在访问数据的时候,也可理解为从调用者(程序) 角度来说,根据 IO 操作的就绪状态来采取不同的方式:
- 阻塞(Block):读取或写入方法将一直等待,直到操作完成。
- 非阻塞(Non-block):读取或写入方法会立即返回一个状态值,不会等待。
场景类比说明:
为了更直观地理解,我们引入一个生活场景:下午写代码饿了,决定去肯德基买全家桶。

我跑去肯德基买全家桶,但是很不巧,轮到我时,全家桶卖完了,我只能等着新做一份……不同的 IO 模型就对应着不同的“等待”策略。
2. 阻塞 IO 模型
学习过操作系统的知识后,可以知道:不管是网络 IO 还是磁盘 IO,对于读操作而言,都是等到网络的某个数据分组到达后/数据准备好后,将数据拷贝到内核空间的缓冲区中,再从内核空间拷贝到用户空间的缓冲区。
拓展阅读:操作系统相关文章
此时我已饥渴难耐,全程盯着后厨,等待着一分一秒。终于全家桶做好了,在此期间虽然什么事也没干,但是最后能吃到全家桶,我很幸福。
此处需要一个清晰的脑回路:我就是程序,我想要全家桶,于是发起了系统调用;而后厨加工的过程就是在做数据准备和拷贝工作。全家桶最终到手,数据终于从内核空间拷贝到了用户空间。
简单看下执行流程:

阻塞 IO 模型流程解析:
阻塞 IO 的执行过程是进程进行系统调用,等待内核将数据准备好并复制到用户态缓冲区后,进程放弃使用 CPU 并一直阻塞在此,直到数据准备好。
3. 非阻塞 IO 模型
此时我每隔 5 分钟询问全家桶好了没,在数次盘问后,终于出炉了。在每一次盘问之前,对于程序来说是非阻塞的,占用 CPU 资源,可以做其他事情。
每次应用程序询问内核是否有数据准备好。如果就绪,就进行拷贝操作;如果未就绪,就不阻塞程序,内核直接返回未就绪的返回值,等待用户程序下一个轮询。

非阻塞 IO 模型流程解析:
大致经历两个阶段:
- 等待数据阶段:未阻塞。用户进程需要“盲等”,不停地去轮询内核。
- 数据复制阶段:阻塞。此时进行数据复制。
在这两个阶段中,用户进程只有在数据复制阶段被阻塞了,而等待数据阶段没有阻塞。但是用户进程需要不停地轮询内核,看数据是否准备好。
4. IO 多路复用模型
排了很长的队,终于轮到我支付后,拿到了一张小票,上面有号次。当全家桶出炉后,会喊相应的号次来取。KFC 营业员小姐姐打小票出号次的动作相当于操作系统多开了个线程,专门接收客户端的连接。我只关注叫到的是不是我的号,因此程序还需在服务端注册我想监听的事件类型。
多路复用一般都是用于网络 IO,服务端与多个客户端的建立连接。下面是神奇的多路复用执行过程:

IO 多路复用模型流程解析:
相比于阻塞 IO 模型,多路复用只是多了一个 select/poll/epoll 函数。select 函数会不断地轮询自己所负责的文件描述符/套接字的到达状态,当某个套接字就绪时,就对这个套接字进行处理。select 负责轮询等待,recvfrom 负责拷贝。当用户进程调用该 select,select 会监听所有注册好的 IO,如果所有 IO 都没注册好,调用进程就阻塞。
对于客户端来说,一般感受不到阻塞,因为请求来了,可以放到线程池里执行;但对于执行 select 的操作系统而言,是阻塞的,需要阻塞地等待某个套接字变为可读。
IO 多路复用其实是阻塞在 select、poll、epoll 这类系统调用上的,复用的是执行这些系统调用的线程。
5. 信号驱动 IO 模型
跑 KFC 嫌麻烦,刚好有个会员,直接点份外卖,美滋滋。当外卖送达时,会收到取餐电话(信号)。在收到取餐电话之前,我可以愉快地吃鸡或者学习。
当数据报准备好的时候,内核会向应用程序发送一个信号,进程对信号进行捕捉,并且调用信号处理函数来获取数据报。

信号驱动 IO 模型流程解析:
该模型也分为两个阶段:
- 数据准备阶段:未阻塞。当数据准备完成之后,会主动通知用户进程数据已经准备完成,对用户进程做一个回调。
- 数据拷贝阶段:阻塞用户进程,等待数据拷贝。
6. 异步 IO 模型
此时科技的发展已经超乎想象了,外卖机器人将全家桶自动送达并转换成营养快速注入我的体内,同时还能得到口感的满足。注入结束后,机器人会提醒我注入完毕。在这个期间我可以放心大胆地玩,甚至注射的时候也不需要停下来!
类比一下,就是用户进程发起系统调用后,立刻就可以开始去做其他的事情,然后直到 I/O 数据准备好并复制完成后,内核会给用户进程发送通知,告诉用户进程操作已经完成了。

异步 IO 模型特点:
- 异步 I/O 执行的两个阶段都不会阻塞读写操作,由内核完成。
- 完成后内核将数据放到指定的缓冲区,通知应用程序来取。
7. Java 中的 BIO、NIO、AIO
操作系统的 IO 模型是底层基石,Java 对于 IO 的操作其实就是进一步的封装,适配一些系统调用方法,让开发更高效。
注:BIO、NIO、AIO 涉及相关实操代码已收录至我的 GitHub,欢迎 Star。
7.1 BIO -- 同步阻塞的编程方式
JDK 1.4 之前常用的编程方式。
实现过程:
首先在服务端启动一个 ServerSocket 来监听网络请求,客户端启动 Socket 发起网络请求。默认情况下 ServerSocket 会建立一个线程来处理此请求。如果服务端没有线程可用,客户端则会阻塞等待或遭到拒绝,并发效率比较低。
服务器实现的模式是一个连接一个线程。若有客户端有连接请求,服务端就需要启动一个线程进行处理。如果这个连接不做任何事情,会造成不必要的线程开销。当然,也可以通过线程池机制改善。
使用场景:
BIO 适用于连接数目比较小且固定的架构,对服务器资源要求高,并发局限于应用中。
7.2 NIO -- 同步非阻塞的编程方式
7.2.1 NIO 简介
NIO 本身是基于事件驱动思想来完成的。当 Socket 有流可读或可写入时,操作系统会相应地通知应用程序进行处理,应用再将流读取到缓冲区或写入操作系统。一个有效的请求对应一个线程,当连接没有数据时,是没有工作线程来处理的。
服务器实现模式为一个请求一个通道。即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。
使用场景:
NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器。并发局限于应用中,编程复杂,JDK 1.4 开始支持。
7.2.2 NIO 中的几种重要角色
有缓冲区 Buffer,通道 Channel,多路复用器 Selector。
7.2.2.1 Buffer
在 NIO 库中,所有数据都是用缓冲区(用户空间缓冲区) 处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作。
缓冲区实际上是一个数组,并提供了对数据的结构化访问以及维护读写位置等信息。
Buffer 的应用固定逻辑:
写操作顺序
clear()put()-> 写操作flip()-> 重置游标SocketChannel.write(buffer)-> 将缓存数据发送到网络的另一端clear()
读操作顺序
clear()SocketChannel.read(buffer)-> 从网络中读取数据buffer.flip()buffer.get()-> 读取数据buffer.clear()
相关的代码我会更新至 GitHub。
7.2.2.2 Channel
NIO 中对数据的读取和写入要通过 Channel,它就像水管一样,是一个通道。通道不同于流的地方就是通道是双向的,可以用于读、写和同时读写操作。
7.2.2.3 Selector
多路复用器,用于注册通道。客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 I/O 请求时才启动一个线程进行处理。
7.3 AIO -- 异步非阻塞编程方式
进行读写操作时,只须直接调用 API 的 read 或 write 方法即可。一个有效请求对应一个线程,客户端的 IO 请求都是 OS 先完成了再通知服务器应用去启动线程进行处理。
使用场景:
AIO 方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器。充分调用 OS 参与并发操作,编程比较复杂,JDK 1.7 开始支持。
总结
从效率上来说,可以简单理解为:阻塞 IO < 非阻塞 IO < 多路复用 IO < 信号驱动 IO < 异步 IO。
从同步和异步来说,只有异步 IO 模型是异步的,其他均为同步。
说明:
- 本文基于 Linux 通用 IO 模型理论整理。
- Java NIO 自 JDK 1.4 引入,AIO (NIO.2) 自 JDK 1.7 引入。在实际高并发场景中,NIO(配合 Netty 等框架)应用更为广泛,AIO 因生态及性能表现原因使用相对较少。
版权声明:本文为原创文章,版权归 戴老师的博客 所有,转载请联系博主获得授权。
本文地址:https://1diff.fun/archives/linux-de-5-zhong-io-mo-xing-shu-li.html
如果对本文有什么问题或疑问都可以在评论区留言,我看到后会尽量解答。