PancrasL的博客

聊一聊IO通信模型

2021-11-24

img

1. 阻塞、非阻塞、同步、异步究竟是什么?

实际开发过程中,我们经常听到这些概念,但是网上的文章很少能清晰说明它们的区别,如果没有系统梳理过,很容易把这些概念搞混。

其实,这四个术语可以分成两对:

  • 阻塞和非阻塞
  • 同步和异步

它们之间两两组合可以拼凑出4种组合:

  • 同步阻塞
  • 同步非阻塞
  • 异步阻塞
  • 异步非阻塞

要搞清楚这些组合术语的意思,我们首先需要搞清楚同步和异步、阻塞和非阻塞的基本概念。

前提知识:首在我们的现代操作系统中,分为用户空间和内核空间,用户空间的应用程序(称为A)发起IO请求,内核空间的驱动程序(称为B)响应IO请求,应用程序A称为调用者驱动程序B称为被调用者

1.1 同步和异步是看结果获取的方式

同步:A发起调用请求后,多次问询B来获取数据。

异步:A发起调用请求后,等待B主动通知返回数据。

同步和异步

1.2 阻塞和非阻塞是看发起调用后,当前线程是挂起还是运行

阻塞:A发起调用后,当前线程被挂起,在收到调用结果后,才会继续执行。

非阻塞:A发起调用后,立即得到调用结果,但是该结果可能是一个error,需要重新进行问询。

阻塞和非阻塞

1.3 组合概念——以照相馆为例

去照相馆拍照的例子,洗照片需要30min:

同步阻塞:一直在照相馆等待,每过1min去问一下老板洗好了没有。

同步非阻塞:刷手机(去做别的事情),每过1min去问一下老板洗好了没有。

异步阻塞:一直在照相馆等待,直到老板通知你照片洗好了。

异步非阻塞:刷手机(去做别的事情),直到老板通知你照片洗好了。

  • 同步阻塞

应用程序调用IO函数后,线程被挂起,等待在该函数返回数据,同时,该函数不断问询内核来获取数据

  • 同步非阻塞

应用程序调用IO函数后,该函数立即返回执行结果,但结果可能是一个error,此时应用程序可以执行其他任务,但是需要时不时看一下结果是否已经准备好了

  • 异步阻塞

应用程序调用IO函数后,线程被挂起,一直等待该函数返回结果。

  • 异步非阻塞

应用程序调用IO函数后,线程可以先去干其他事情,等到内核通知线程数据已经准备好了,然后再去进行处理。

2. Linux系统中的五种IO模型

介绍完了基本概念,接下来我们要探究一下Linux系统发展出的5种IO模型,它们分别是:

  • 阻塞I/O模型(同步阻塞类型)
  • 非阻塞I/O模型(同步非阻塞类型)
  • 多路复用I/O模型(同步阻塞类型的改进版)
  • 信号驱动I/O模型(同步非阻塞类型的改进版)
  • 异步I/O模型(异步非阻塞类型)

其中,前4种属于同步I/O,第5种属于异步I/O.

前提知识:在现代操作系统中,当应用程序向内核发起IO请求后,IO交互可以分为两个阶段:
(1)等待数据:等待数据到达外部设备(如网卡、磁盘等),然后将数据读取到内核空间
(2)拷贝数据:将数据从内核空间拷贝到用户空间

2.1 阻塞I/O模型(同步阻塞)

用户线程调用recvfrom系统调用后会被内核阻塞,直到数据被拷贝到用户空间。

内核收到系统调用后,开始准备数据,将数据从外部设备拷贝到内核空间,若数据未到达,内核也会阻塞等待。

特点 在I/O执行的两个阶段(等待数据和拷贝数据)都被阻塞
典型应用 阻塞Socket、Java BIO
优点 (1)线程被挂起,不消耗CPU资源 (2)实现难度低 (3)适合并发量小的网络应用
缺点 (1)需要为每个请求分配一个线程,开销大 (2)不适合并发量大的应用
阻塞IO模型

2.2 非阻塞I/O模型(同步非阻塞)

用户线程发出read请求后,如果内核中的数据还没有准备好,它并不会阻塞用户线程,而是返回一个error,从用户线程角度看,它发起read后不需要等待,可以立即得到执行结果,并对结果进行判断,如果是一个error,它就直到结果没有准备好,可以重新发送read操作,一旦内核中的数据准备好了,并再次收到了用户线程的系统调用,那么它会将数据拷贝到用户内存,然后返回。

特点 用户线程需要不断地主动询问内核数据准备好了没有
典型应用 Socket设置为NON_BLOCK
优点 实现难度低,相对阻塞IO较难
缺点 (1)线程多次问询,消耗CPU资源
非阻塞IO模型

2.3 多路复用I/O模型(同步阻塞的改进版)

多个线程的I/O可以注册到一个复用器(Selector)上,Selector线程不断轮询多个Socket的状态,只有当真正的Socket有读写事件时,才会执行IO操作,仅需一个线程就可以管理多个Socket,不必为每个Socket维护一个线程,大大减少了资源占用,同时,Selector的轮询是在内核中进行的,减少了有用户线程轮询所产生的系统调用开销。

select被调用后,线程会被阻塞,内核监视所有select负责的socket,当有任何一个socket的数据准备好了,select就会返回套接字可读,我们就可以调用recvfrom处理数据。

多路复用IO模型

特点 对于每一个Socket,一般都设置成非阻塞,但是整个用户的线程一直被阻塞,只不过是被select函数所阻塞,而不是被Socket I/O阻塞
典型应用 Java NIO,epoll,poll,select
优点 (1)一个线程可以管理多个Socket连接,性能好,Reactor模式(2)适合高并发服务开发,一个线程响应多个请求
缺点 开发难度大

2.3.1 select、epoll、poll

Unix系统下多路复用IO模型的三种实现

(1)select

知道有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

(2)poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.

(3)epoll

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。

epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。

(38条消息) 深入理解select、poll和epoll及区别_$好记性还是要多记录$-CSDN博客

2.3.2 Reactor设计模式

虽然多路复用I/O模型允许单线程内处理多个IO请求,但是每个IO请求的过程还是阻塞的(在select函数上阻塞),平均时间甚至比同步阻塞IO模型还要长,为了解决这一问题,引入了Reactor设计模式,Java NIO就采用了此方法。

2.4 信号驱动I/O模型(同步非阻塞的改进版)

线程预先告知内核,向内核注册一个信号处理函数,然后用户线程返回不阻塞,当内核数据就绪时会发送一个信号给线程,用户线程便在信号处理函数中调用IO读取数据。

实际上,信号驱动I/O模型并没有实现真正的异步,因为将数据从内核拷贝到用户空间的过程仍然是阻塞的,需要由用户线程来完成IO操作。

信号驱动IO模型

2.5 异步I/O模型(异步非阻塞)

用户线程发起aio_read后可以去做其他事情。

内核收到aio_read后立即返回,待数据准备好后会将数据拷贝到用户空间,然后告诉用户线程数据准备好了,用户线程可以直接对数据进行处理。和信号驱动IO模型的区别是:信号驱动IO模型是由内核告诉我们何时可以启动一个IO操作,这个IO操作由用户自定义的信号函数来实现,而异步IO模型由内核告诉我们IO操作何时完成。

特点 真正实现了异步IO,是五种IO模型中唯一的异步模型
典型应用 Java7 AIO
优点 (1)不阻塞,数据一步到位(2)适合高性能、高并发应用
缺点 (1)需要操作系统底层支持,Linux 2.5内核首次实现(2)开发难度大

异步IO模型

3. Java中的IO方式

BIO (Blocking I/O): 同步阻塞 I/O 模式,数据的读取写⼊必须阻塞在⼀个线程内等待其完 成。在活动连接数不是特别⾼(⼩于单机 1000)的情况下,这种模型是⽐较不错的,可以让每 ⼀个连接专注于⾃⼰的 I/O 并且编程模型简单,也不⽤过多考虑系统的过载、限流等问题。线 程池本身就是⼀个天然的漏⽃,可以缓冲⼀些系统处理不了的连接或请求。但是,当⾯对⼗万甚 ⾄百万级连接的时候,传统的 BIO 模型是⽆能为⼒的。因此,我们需要⼀种更⾼效的 I/O 处理 模型来应对更⾼的并发量。

NIO (Non-blocking/New I/O): NIO 是⼀种同步⾮阻塞的 I/O 模型,在 Java 1.4 中引⼊了 NIO 框架,对应 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可 以理解为 Non-blocking,不单纯是 New。它⽀持⾯向缓冲的,基于通道的 I/O 操作⽅法。 NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都⽀持阻塞和⾮阻塞两种模 式。阻塞模式使⽤就像传统中的⽀持⼀样,⽐较简单,但是性能和可靠性都不好;⾮阻塞模式正 好与之相反。对于低负载、低并发的应⽤程序,可以使⽤同步阻塞 I/O 来提升开发速率和更好 的维护性;对于⾼负载、⾼并发的(⽹络)应⽤,应使⽤ NIO 的⾮阻塞模式来开发

AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引⼊了 NIO 的改进版 NIO 2,它是 异步⾮阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应⽤操作之后会直接返回,不会堵塞在那⾥,当后台处理完成,操作系统会通知相应的线程进⾏后续的操作。AIO 是异 步 IO 的缩写,虽然 NIO 在⽹络操作中,提供了⾮阻塞的⽅法,但是 NIO 的 IO ⾏为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程⾃ ⾏进⾏ IO 操作,IO 操作本身是同步的。查阅⽹上相关资料,我发现就⽬前来说 AIO 的应⽤还 不是很⼴泛,Netty 之前也尝试使⽤过 AIO,不过⼜放弃了。