后端开发—一文详解网络 IO 模型
网络 IO:
网络 IO 会涉及到两个系统对象 一个是 用户空间 调用 IO 的进程或者线程,另一个是 内核
空间的 内核系统, 比如 发生 IO 操作 read 时,它会经历两个阶段
1. 等待数据准备就绪
2. 将数据 从内核拷贝到进程 或者线程 中。
服务器模型 Reactor 与 Proactor
对高并发编程网络连接上的消息处理,可以分为两个阶段:等待消息准备好、消息 处理。当使用默认的阻塞套接字时(例如上面提到的 1 个线程捆绑处理 1 个连接),往往是把这两个阶段合而为一,这样操作套接字的代码所在的线程就得睡眠来等待消息准备好,这导致了高并发下线程会频繁的睡眠、唤醒,从而影响了 CPU 的使用效率。高并发编程方法当然就是把两个阶段分开处理。即,等待消息准备好的代码段,与处理消息的代码段是分离的。当然,这也要求套接字必须是非阻塞的,否则,处理消息的代码段很容易导致条件不满足时,所在线程又进入了睡眠等待阶段。那 么问题来了,等待消息准备好这个阶段怎么实现?它毕竟还是等待,这意味着线程还是要睡眠的!解决办法就是,线程主动查询,或者让 1 个线程为所有连接而等待!这就是 IO 多路复用了。多路复用就是处理 等待消息准备好这件事的,但它可以同时处理多个连接!它也可能 等待 ””,所以它也会导致线程睡眠,然而这不要紧,因为它一对多、它可以监控所有连接。这样,当我们的线程被唤醒执行时,就一定是有一些连接准备好被我们的代码执行了。
作为一个高性能服务器程序通常需要考虑处理三类事件: I/O 事件,定时事件及信号。两种 高效 的事件处理模型: Reactor 和 Proactor。
Reactor 模型
Reactor 释义 反应堆 ””,是一种事件驱动机制。和普通函数调用的不同之处在于:应用程序不是主动的调用某个 API 完成处理,而是恰恰相反, Reactor 逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor 上,如果相应的时间发生, Reactor 将主动调用应用程序注册的接口,这些接口又称为 回调函数 。
Reactor 模式是处理并发 I/O 比较常见的一种模式,用于同步 I/O,中心思想是将所有要处理的 I/O 事件注册到一个中心 I/O 多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事先注册的相应 I/O 事件分发到对应的处理器中。
Reactor 模型有三个重要的组件:
多路复用器:由操作系统提供,在 linux 上一般是 select, poll, epoll 等系统调用。
事件分发器:将多路复用器中返回的就绪事件分到对应的处理函数中。
事件处理器:负责处理特定事件的处理函数。
具体流程如下:
注册读就绪事件和相应的事件处理器;
事件分离器等待事件;
事件到来,激活分离器,分离器调用事件对应的处理器;
事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权;
Reactor 模式是编写高性能网络服务器的必备技术之一,它具有如下的优点:
响应快;编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且 避免了多线程进程的切换开销可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源;可复用性, reactor 框架本身与具体事件处理逻辑无关,具有很高的复用性;
实际 Reactor 模型可能是这样:
Proactor 模型
具体流程如下:
处理器发起异步操作,并关注 I/O 完成事件
事件分离器等待操作完成事件
分离器等待过程中,内核并行执行实际的 I/O 操作,并将结果数据存入用户自定义缓冲区,最后通知事件分离器读操作完成 I/O 完成后,通过事件分离器呼唤处理器事件处理器处理用户自定义缓冲区中的数据
我们可以发现 proactor 模型最大的特点就是 Proactor 最大的特点是使用异步 I/O。所有的 I/O 操作都交由系统提供的异步 I/O 接口去执行。工作线程仅仅负责业务逻辑。在 Proactor 中,用户函数启动一个异步的文件操作。同时将这个操作注册到多路复用器上。多路复用器并不关心文件是否可读或可写而是关心这个异步读操作是否完成。异步操作是操作系统完成,用户程序不需要关心。多路复用器等待直到有完成通知到来。当操作系统完成了读文件操作——将读到的数据复制到了用户先前提供的缓冲区之后,通知多路作系统完成了读文件操作——将读到的数据复制到了用户先前提供的缓冲区之后,通知多路复用器相关操作已完成。多路复用器再调用相应的处理程序,处理数据。
Proactor 增加了编程的复杂度,但给工作线程带来了更高的效率。Proactor 可以在系统态将读写优化,利用 I/O 并行能力,提供一个高性能单线程模型。在 windows 上,由于没有 epoll 这样的机制,因此提供了 IOCP 来支持高并发, 由于操作系统做了较好的优化,windows 较常采用 Proactor 的模型利用完成端口来实现服务器。在 linux 上,在 2.6 内核出现了 aio 接口,但 aio 实际效果并不理想,它的出现,主要是解决 poll 性能不佳的问题,但实际上经过测试,epoll 的性能高于 poll+aio,并且 aio 不能处理 accept,因此 linux 主要还是以 Reactor 模型为主。
Libevent libev libuv
libevent
名气最大,应用最广泛,历史悠久的跨平台事件库;
libev :
较较 libevent 而言,设计更简练,性能更好,但对而言,设计更简练,性能更好,但对 Windows 支持不够好
libuv :
开发 nodenode 的过程中需要一个跨平台的事件库,他们首选了的过程中需要一个跨平台的事件库,他们首选了 libev,但又要支持 Windows,故重新封装了一套,linux 下用 libev 实现,Windows 下用 IOCP 实现;
总结:
掌握了 Reactor 原理,为以后学习其他网络框架底层代码,打下了基础。
技术参考
C/C++Linux服务器开发/C++后台开发架构师-学习视频
内容包括 C++工程化、高性能及分布式、深入浅出。性能调优、TCP,协程,Nginx 源码分析 Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,Linux 内核,P2P,K8S,Docker,TCP/IP,协程,DPDK 多个高级知识点。以下视频获取点击:C++后台架构师
评论