网络io模型

本文详细解析了I/O模型的基础概念,包括阻塞和非阻塞的区别,以及同步和异步在网络通信中的作用。介绍了五种IO模型:同步阻塞、同步非阻塞、多路复用、信号驱动和异步模型,并重点剖析了Reactor模式和Proactor模式的原理和适用场景。

目录

一、基础概念

1.什么是I/O

2.阻塞和非阻塞

3.同步和异步

二、五种IO模型

1.同步阻塞I/O模型

2.同步非阻塞IO模型

3.多路复用IO模型

4.信号驱动IO模型

5.异步IO模型

三、IO模式

1.Reactor模式

1.为什么引入Reactor模式

2.Reactor模式实现原理

1. 单 Reactor 单进程 / 线程

2. 单 Reactor 多线程

3. 多 Reactor 多进程 / 线程

2.Proactor模式

1.为什么引入Proactor模式

2.Proactor 模式实现原理

一、基础概念

在《Unix网络编程》一书中提到了五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。

这些 名词我们好像都似曾相识,但这些I/O通信模型有什么区别?

同步和阻塞似乎是一回事,到底有什么不同?

等一下,在这之前你是不是应该问自己一个终极问题:什么是I/O?为什么需要这些I/O模型?

1.什么是I/O

所谓的I/O就是计算机内存与外部设备之间拷贝数据的过程。

我们知道CPU访问内存的速度远远高于外部设备,因此CPU是先把外部设备的数据读到内存里,然后再进行处理。

2.阻塞和非阻塞

请考虑一下这个场景,当你的程序通过 CPU向外部设备发出一个读指令时,数据从外部设备拷贝到内存往往需要一段时间,这个时候CPU没事干 了,你的程序是主动把CPU让给别人?还是让CPU不停地查:数据到了吗、数据到了吗…

我们平时说的阻塞或非阻塞是指应用程序在发起I/O操 作时,是立即返回还是等待。

3.同步和异步

对于一个网络I/O通信过程,比如网络数据读取,会涉及两个对象,一个是调用这个I/O操作的用户线程,另 外一个就是操作系统内核。一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空 间。

当用户线程发起I/O操作后,网络数据读取操作会经历两个步骤:

1.用户线程等待内核将数据从网卡拷贝到内核空间。

2.内核将数据从内核空间拷贝到用户空间。

同步和异步,是指应用程序在与内核通信时,数据从内核空间到应用空间的拷贝,是由内核主动发起还是由应用程序来触发。

二、五种IO模型

1.同步阻塞I/O模型

最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。

用户线程发起read调用后就阻塞了,让出CPU。内核等待网卡数据到来,把数据从网卡拷贝 到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒。

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。

过程如下:

2.同步非阻塞IO模型

用户线程不断的发起read调用,数据没到内核空间时,每次都返回失败,直到数据到了内 核空间,这一次read调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据 到了用户空间再把线程叫醒。

当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。

所以事实上,在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。

3.多路复用IO模型

多路复用IO模型是目前使用得比较多的模型。Java NIO实际上就是多路复用IO。

用户线程的读取操作分成两步了,线程先发起select调用,目的是问内核数据准备好了吗?等 内核把数据准备好了,用户线程再发起read调用。在等待数据从内核空间拷贝到用户空间这段时间里,线程 还是阻塞的。

那为什么叫I/O多路复用呢?

因为一次select调用可以向内核查多个数据通道(Channel)的状 态,所以叫多路复用。

在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

4.信号驱动IO模型

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

5.异步IO模型

异步IO模型才是最理想的IO模型,在异步IO模型中,用户线程发起read调用的同时注册一个回调函数,read立即返回,等内核将数据准备好后,再调 用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。

从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。

目前 Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下的 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 Reactor 模式为主。 

三、IO模式

Reactor模式实现了同步非阻塞模型,而Proactor模式实现了异步非阻塞模型

1.Reactor模式

1.为什么引入Reactor模式

我们可以思考一个问题:进程如何才能高效地处理多个连接的业务?

当一个连接一个进程时,进程可以采用“read -> 业务处理 -> write”的处理流程,如果当前连接没有数据可以读,则进程就阻塞在 read 操作上。

这种阻塞的方式在一个连接一个进程的场景下没有问题,但如果一个进程处理多个连接,进程阻塞在某个连接的 read 操作上,此时即使其他连接有数据可读,进程也无法去处理,很显然这样是无法做到高性能的。

解决这个问题的最简单的方式是将 read 操作改为非阻塞,然后进程不断地轮询多个连接。这种方式能够解决阻塞的问题,但解决的方式并不优雅。首先,轮询是要消耗 CPU 的;其次,如果一个进程处理几千上万的连接,则轮询的效率是很低的。

为了能够更好地解决上述问题,很容易可以想到,只有当连接上有数据的时候进程才去处理,这就是 I/O 多路复用技术的来源。

I/O 多路复用技术归纳起来有两个关键实现点:

  • 当多条连接共用一个阻塞对象后,进程只需要在一个阻塞对象上等待,而无须再轮询所有连接,常见的实现方式有 select、epoll、kqueue 等。

  • 当某条连接有新的数据可以处理时,操作系统会通知进程,进程从阻塞状态返回,开始进行业务处理。

I/O 多路复用结合线程池,完美地解决了 PPC 和 TPC 的问题,而且“大神们”给它取了一个很牛的名字:Reactor,中文是“反应堆”。

联想到“核反应堆”,听起来就很吓人,实际上这里的“反应”不是聚变、裂变反应的意思,而是“事件反应”的意思,可以通俗地理解为“来了一个事件我就有相应的反应”,这里的“我”就是 Reactor,具体的反应就是我们写的代码,Reactor 会根据事件类型来调用相应的代码进行处理。Reactor 模式也叫 Dispatcher 模式(在很多开源的系统里面会看到这个名称的类,其实就是实现 Reactor 模式的),更加贴近模式本身的含义,即 I/O 多路复用统一监听事件,收到事件后分配(Dispatch)给某个进程。

2.Reactor模式实现原理

Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池或线程池),其中 Reactor 负责监听和分配事件,处理资源池负责处理事件。

在Reactor模式中,会先对每个client注册感兴趣的事件(只对连接事件注册,读写事件则直接唤醒对应的连接事件的处理器),容器可以是一个hashmap。key为clientID,value为事件消息集合。

在nio中是注册在选择器里面的,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询,如下图所示:

在这里插入图片描述
注意,上面的图中展示的 是顺序处理每个事件,当然为了提高事件处理速度,可以通过多线程或者线程池的方式来处理事件,即可以使用线程池来处理业务逻辑,以提高轮询线程的执行效率。流程则变为检查队列的线程如果发现了需要执行的事件,则就将它提交线程池里处理,然后继续轮训。

初看 Reactor 的实现是比较简单的,但实际上结合不同的业务场景,Reactor 模式的具体实现方案灵活多变,主要体现在:

  • Reactor 的数量可以变化:可以是一个 Reactor,也可以是多个 Reactor。

  • 资源池的数量可以变化:以进程为例,可以是单个进程,也可以是多个进程(线程类似)。

将上面两个因素排列组合一下,理论上可以有 4 种选择,但由于“多 Reactor 单进程”实现方案相比“单 Reactor 单进程”方案,既复杂又没有性能优势,因此“多 Reactor 单进程”方案仅仅是一个理论上的方案,实际没有应用。

最终 Reactor 模式有这三种典型的实现方案:

  • 单 Reactor 单进程 / 线程。

  • 单 Reactor 多线程。

  • 多 Reactor 多进程 / 线程。

以上方案具体选择进程还是线程,更多地是和编程语言及平台相关。例如,Java 语言一般使用线程(例如,Netty),C 语言使用进程和线程都可以。例如,Nginx 使用进程,Memcache 使用线程。

1. 单 Reactor 单进程 / 线程

即单线程处理连接、响应事件、业务逻辑

单 Reactor 单进程 / 线程的方案示意图如下(以进程为例):



注意,select、accept、read、send 是标准的网络编程 API,dispatch 和“业务处理”是需要完成的操作,其他方案示意图类似。

详细说明一下这个方案:

  • Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。

  • 如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。

  • 如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。

  • Handler 会完成 read-> 业务处理 ->send 的完整业务流程。

单 Reactor 单进程的模式优点就是很简单,没有进程间通信,没有进程竞争,全部都在同一个进程内完成。

但其缺点也是非常明显,具体表现有:

  • 只有一个进程,无法发挥多核 CPU 的性能;只能采取部署多个系统来利用多核 CPU,但这样会带来运维复杂度,本来只要维护一个系统,用这种方式需要在一台机器上维护多套系统。

  • Handler 在处理某个连接上的业务时,整个进程无法处理其他连接的事件,很容易导致性能瓶颈。

因此,单 Reactor 单进程的方案在实践中应用场景不多,只适用于业务处理非常快速的场景。

目前比较著名的开源软件中使用单 Reactor 单进程的是 Redis(因为redis纯内存操作,速度快单线程足以)。

需要注意的是,C 语言编写系统的一般使用单 Reactor 单进程,因为没有必要在进程中再创建线程;而 Java 语言编写的一般使用单 Reactor 单线程,因为 Java 虚拟机是一个进程,虚拟机中有很多线程,业务线程只是其中的一个线程而已。

2. 单 Reactor 多线程

即单线程处理连接和响应事件,多线程处理业务逻辑

为了克服单 Reactor 单进程 / 线程方案的缺点,引入多进程 / 多线程是显而易见的,这就产生了第 2 个方案:单 Reactor 多线程。

单 Reactor 多线程方案示意图是:



处理流程如下:

我来介绍一下这个方案:

  • 主线程中,Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行分发。

  • 如果是连接建立的事件,则由 Acceptor 处理,Acceptor 通过 accept 接受连接,并创建一个 Handler 来处理连接后续的各种事件。

  • 如果不是连接建立事件,则 Reactor 会调用连接对应的 Handler(第 2 步中创建的 Handler)来进行响应。

  • Handler 只负责响应事件,不进行业务处理;Handler 通过 read 读取到数据后,会发给 Processor 进行业务处理。

  • Processor 会在独立的子线程中完成真正的业务处理,然后将响应结果发给主进程的 Handler 处理;Handler 收到响应后通过 send 将响应结果返回给 client。

单 Reator 多线程方案能够充分利用多核多 CPU 的处理能力,但同时也存在下面的问题:

  • 多线程数据共享和访问比较复杂。例如,子线程完成业务处理后,要把结果传递给主线程的 Reactor 进行发送,这里涉及共享数据的互斥和保护机制。以 Java 的 NIO 为例,Selector 是线程安全的,但是通过 Selector.selectKeys() 返回的键的集合是非线程安全的,对 selected keys 的处理必须单线程处理或者采取同步措施进行保护。

  • Reactor 承担所有事件的监听和响应,只在主线程中运行,瞬间高并发时会成为性能瓶颈。因为要等一个连接分发完后才能接受新的连接,这样即便NIO线程的CPU负荷达到100%,也无法满足海量的事件,当NIO线程负载过重之后,等待的事件逐渐累积增多,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,所以单NIO线程会成为系统的性能瓶颈(得想办法减轻单NIO线程的压力);

  • 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

你可能会发现,我只列出了“单 Reactor 多线程”方案,没有列出“单 Reactor 多进程”方案,这是什么原因呢?

主要原因在于如果采用多进程,子进程完成业务处理后,将结果返回给父进程,并通知父进程发送给哪个 client,这是很麻烦的事情。因为父进程只是通过 Reactor 监听各个连接上的事件然后进行分配,子进程与父进程通信时并不是一个连接。如果要将父进程和子进程之间的通信模拟为一个连接,并加入 Reactor 进行监听,则是比较复杂的。而采用多线程时,因为多线程是共享数据的,因此线程间通信是非常方便的。虽然要额外考虑线程间共享数据时的同步问题,但这个复杂度比进程间通信的复杂度要低很多。

3. 多 Reactor 多进程 / 线程

即单线程处理连接事件、多个线程处理响应事件(线程数一般设置较小,因为处理速度快)、多线程业务逻辑(线程数设置较大,因为业务逻辑处理速度较慢)

为了解决单 Reactor 多线程的问题,最直观的方法就是将单 Reactor 改为多 Reactor,这就产生了第 3 个方案:多 Reactor 多进程 / 线程。

多 Reactor 多进程 / 线程方案示意图是(以进程为例):



处理流程可参考下图: 

方案详细说明如下:

  • 父进程中 mainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 接收,将新的连接分配给某个子进程。

  • 子进程的 subReactor 将 mainReactor 分配的连接加入连接队列进行监听,并创建一个 Handler 用于处理连接的各种事件。

  • 当有新的事件发生时,subReactor 会调用连接对应的 Handler(即第 2 步中创建的 Handler)来进行响应。

  • Handler 完成 read→业务处理→send 的完整业务流程。

多 Reactor 多进程 / 线程的方案看起来比单 Reactor 多线程要复杂,但实际实现时反而更加简单,主要原因是:

  • 父进程和子进程的职责非常明确,父进程只负责接收新连接,子进程负责完成后续的业务处理。

  • 父进程和子进程的交互很简单,父进程只需要把新连接传给子进程,子进程无须返回数据。

  • 子进程之间是互相独立的,无须同步共享之类的处理(这里仅限于网络模型相关的 select、read、send 等无须同步共享,“业务处理”还是有可能需要同步共享的)。

目前著名的开源系统 Nginx 采用的是多 Reactor 多进程,采用多 Reactor 多线程的实现有 Memcache 和 Netty。

2.Proactor模式

1.为什么引入Proactor模式

Reactor 是非阻塞同步网络模型,因为真正的 read 和 send 操作都需要用户进程同步操作。这里的“同步”指用户进程在执行 read 和 send 这类 I/O 操作的时候是同步的,如果把 I/O 操作改为异步就能够进一步提升性能,这就是异步网络模型 Proactor。

2.Proactor 模式实现原理

Reactor 可以理解为“来了事件我通知你,你来处理”,而 Proactor 可以理解为“来了事件我来处理,处理完了我通知你”。

这里的“我”就是操作系统内核,“事件”就是有新连接、有数据可读、有数据可写的这些 I/O 事件,“你”就是我们的程序代码。

Proactor 模型示意图是:



详细介绍一下 Proactor 方案:

  • Proactor Initiator 负责创建 Proactor 和 Handler,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核。

  • Asynchronous Operation Processor 负责处理注册请求,并完成 I/O 操作。

  • Asynchronous Operation Processor 完成 I/O 操作后通知 Proactor。

  • Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理。

  • Handler 完成业务处理,Handler 也可以注册新的 Handler 到内核进程。

理论上 Proactor 比 Reactor 效率要高一些,异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠,但要实现真正的异步 I/O,操作系统需要做大量的工作。

目前 Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下的 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 Reactor 模式为主。

所以即使 Boost.Asio 号称实现了 Proactor 模型,其实它在 Windows 下采用 IOCP,而在 Linux 下是用 Reactor 模式(采用 epoll)模拟出来的异步模型。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值