C++ | IOCP 完成端口,最高性能的网络编程模型原理

1,179 阅读6分钟

0. 前言

在C++的网络编程中,存在诸多模型,如基础 Socket 模型,同步非阻塞的 select 模型,以及本文要详细说明的 IOCP 模型等。

而随着业务的深入开发,应用场景的不断拓展,一般的 Socket 模型由于其采用阻塞模式,会有很严重的性能问题,只会在我们初学网络编程的时候进行简单的使用,如开发一对一或者一对少量客户景。

而 select 模型,则采用了同步非阻塞 I/O 机制,通过 select 函数来轮询处理客户端发送过来的 Socket,很好的解决了 Socket 模型中的性能问题。在 Linux 和 Windows 平台,该模型都有比较广泛的应用场景,比如常见的 libevent 库底层就是使用的 select 模型。

至于 IOCP 模型,则来源于微软为 Windows 开的“后门”,采用特有的并发异步非阻塞通信机制,与 Linux 中的 Epoll 并称为两大高效模型,在高并发、高压环境的服务器需求下,非常推荐使用 IOCP 来应对。比如大名鼎鼎的 Nginx 服务器,底层就是使用的 IOCP 模型实现。

在 Nginx 源码中,能看到使用完成端口的代码

在这篇文章中,您将逐步认识到 IOCP 性能强劲的秘密。

1. 什么是 IOCP?

IOCP:I/O completion ports(完成端口),一种用于处理多处理器系统中,多个异步 I/O 请求的线程模型。当进程创建 I/O 完成端口时,系统会为对应请求提供服务的线程创建关联的对象队列。利用 Windows 内核对象来对 I/O 进行调度。属于 C/S 通信模型中,性能最优秀的网络通信模型。

2. IOCP 为什么快?

在弄清楚这个问题之前,我们需要了解 IOCP 实现的基本原理,通过实现过程,我们便可以一步步探知 IOCP 快的原因:

  1. 系统根据 CPU 核心的数量来创建线程;
  2. 系统使线程保持等待,当存在客户端请求时,将客户端请求加入到公共消息队列中;
  3. 系统创建的线程逐一排队,从消息队列中取出消息并对其进行处理;
  4. 当线程完成消息,且后续没有消息需要完成时,CPU 才会将线程挂起,不占用 CPU 的使用周期。

通俗点讲,IOCP 的实现很类似于我们坐高铁进站过安检,人群是一个个的消息,而安检机则是一个个工作线程,我们排队经过身份识别后,选择合适的位置进行安检。

IOCP 的基本运行原理图

在 IOCP 的基本运行原理中,我们会发现,CPU 基本上都是处在工作状态的,由于我们根据 CPU 的核心数创建工作线程,因此每个线程需要执行时,都能保证有可用的 CPU 资源进行调度,而同样,也能保证在执行过程中,尽可能少的发生线程的上下文切换,这是 IOCP 快的第一层“秘诀”。

在 IOCP 调度的过程中,我们不能忽略的问题是,即使我们为每个 CPU 分配了工作线程,那谁来给工作线程派“任务”呢?

此时我们便可以把目光放到那条公共消息队列中,在 IOCP 中,所有的工作线程会轮询这条公共消息队列,并从中取出消息加以处理。在这个过程中,队列与多个工作线程非常优雅地实现了异步通信与负载均衡,在线程空闲时,IOCP 也会及时将线程挂起,防止 CPU 周期的占用。

而第二条“秘诀”,便是这条公共消息队列,因此完成端口其实本质上与我们常说的端口并没有什么关系,感觉叫完成队列更合适。

至于最后一层“秘诀”,则需要回到我们的基本网络通信机制中去。

由前文可知,IOCP 运用的是异步 I/O 通信机制,那么异步 I/O 与同步 I/O 最大的区别在哪呢?

前置知识:在操作系统中,外部设备的 I/O 速度,与 CPU 相比,是有非常大的差距的。

异步与同步的本质,在于主线程与通信线程是否能够“并行”。

同步 I/O 机制,在发起 I/O 的时候,顾名思义,用户与设备的数据需要进行同步,将数据在内核缓冲区同步后,再经过拷贝返回到用户进程,此时会导致进程阻塞,影响通信效率。

同步 I/O 机制

而异步 I/O ,则不需要等待操作完成,当用户进程发送请求之后,便可进行其他的操作,当内核将数据准备好之后,会将数据从内核缓冲区拷贝到用户进程。此时内核发送消息通知 I/O 请求完成即可。

异步 I/O 机制

由上文我们可知,异步与同步,同时具备在内核缓冲区拷贝到用户进程这个过程,而在等待外部 I/O 设备拷贝的时间里,异步可以执行其他的操作,而同步只能干等着。因此在高性能网络服务器中,使用异步通信机制,是必不可少的。这也是 IOCP 快的另一层“秘诀”。

因此我们可以稍微总结一下,IOCP 快的原因:

  1. 根据 CPU 内核数量创建工作线程,使用公共消息队列进行调度,保证 CPU 资源的合理利用;
  2. 使用公共消息队列,保证工作线程之间的负载均衡,防止 CPU 周期被浪费;
  3. 利用异步 I/O 通信机制,是主线程与网络线程”并行“。

3. 使用 IOCP 的基本流程

据上文分析之后,大家也基本上对 IOCP 的原理有了一个认识,那下一步,我们就稍微了解一下 IOCP 使用的基本流程。

与基础的 Socket 通信类似,IOCP 其实也有绑定端口,创建连接,接受数据等一系列操作。简单地,使用 IOCP 的基本流程如下:

  1. 初始化完成端口和工作线程
  2. 创建 Socket,并绑定到完成端口上,监听消息连接
  3. 接受并监听数据
  4. 关闭完成端口

一般地,我们创建CPU核心数*2的工作线程,使得在某个线程 Sleep() 或WSAWaitForMultipleEvents() 将线程挂起时(此时不占用CPU时间片),CPU的内核仍旧有线程在工作,减少线程调度的时间,保证程序的执行效率)。

而具体的 IOCP 执行,诸位可以由这张流程图,直观的看到: IOCP 执行流程图

4. 尾声

完成端口的理解,其实相对来说没那么复杂,主要是需要对其性能强劲的各个组成进行详细的了解和认识,只有逐步拆分逐步深入,才能更精确的了解到其性能强劲的原因。而在使用的过程中,只要遵循完成端口的基本流程,并将其加入项目逐步拓展即可。在众多优秀的网络库中,诸如 AISO 网络库,其底层实现其实也用到了 IOCP 完成端口,利用封装好的网络库,有时候也比使用底层库要来得更轻松易用。因此重点在如何理解其原理,从业务入手,简化开发流程与压力,这才是上上策。

本文由 SoGeek_Studio 发布,有任何问题请留言评论,欢迎指正。