---
menu: Nginx 与 epoll 的协同工作
title: Nginx 与 epoll 的协同工作机制
taxonomy:
    category: docs
---

众所周知，epoll 是一种高性能的事件驱动的 I/O 多路复用机制。那么，相较于 select 这种原始的 I/O 多路复用机制，它具备哪些优势呢？简而言之：实现了从被动到主动的转变。

epoll 化被动为主动，以前 Apache 的 select 模型需要两次遍历才能实现的网络数据包和线程的匹配，现在通过事件驱动的方式主动献上指针，性能暴增。这就像云原生时代的 Prometheus 监控：化主动上传为被动查询，大幅提升了单个数据收集节点的性能上限，成功解决了监控领域的高并发性能问题。

在 5000 个 TCP 连接的情况下，每收到一个数据包，Nginx 找到对应线程的速度比 Apache 高了两个数量级，即便是 event 模式下的 Apache，性能依然远低于 Nginx，因为 Nginx 就是专门为“反向代理”设计的，而 Apache 本质是个 Web 应用容器，无法做到纯粹的事件驱动，其性能自然无法与 Nginx 相提并论。

### epoll 的技术原理

笔者将从两个方面讲解 epoll 的技术原理，分别是网络数据包的处理流程，和 I/O 多路复用的实现方式。

#### 1. 网络数据包的处理流程

![](/media/16892644064661.jpg)
<center>图 4-2 接收到网络数据包以后的处理流程图</center>

图 4-2 是网卡接收到信息后，把信息发送给用户态的进程处理的流程图。这个过程可以分为四步来理解：

1. 网卡接收到一段数据，通过 DMA 方式写入内存
2. NIC 向 CPU 发出硬件中断请求，告诉内核有新的数据过来了
3. Linux 内核响应中断，系统切换为内核态，处理 Interrupt Handler，从 RingBuffer 拿出一个 Packet，然后解析数据，找到这个端口对应的是哪个 PID，然后包装成 socket 发送给那个进程
4. 系统切换为用户态，用户进程处理内核传递过来的 socket 数据

#### 2. I/O 多路复用的实现方式

![](/media/16892648549835.jpg)
<center>图 4-3 I/O 多路复用原理图</center>

如图 4-3 所示。Nginx 在用户态中创建了一个线程池，池内的每个线程都需要负责处理多个 TCP 连接。通常情况下，这些线程处于休眠状态，不会占用 CPU 资源。当某个 TCP 连接接收到数据时，内核中的事件驱动机制会找到对应的休眠线程，主动调用相应的回调函数。

### epoll 真的是非阻塞的吗？

直接给出结论：不是。

首先，需要明确的是，在 Linux 系统下并不存在真正的非阻塞 I/O。

其次，客观上来说，网络连接是一个需要客户端和服务端共同确认的过程，并且由于光速的限制，信息的传递必然需要一定的时间。因此，单个网络连接的模型一定是“阻塞”的：即数据没有返回之前，需要进行等待。

其实我们的宇宙中根本不存在真正的非阻塞 I/O，因为我们的宇宙遵循因果律。

那 Windows IOCP 为什么号称是非阻塞的呢？IOCP 是 Windows 系统提供的一个系统级的非阻塞 I/O 运行库，这套 API 对于我们的调用者进程来说确实是非阻塞的，但实际上在内核层面它仍然是通过线程池实现的，只是这个线程池并没有留给我们自己来实现，而是由内核帮我们实现了而已。

因此，无论是在 Linux 下的 epoll，还是在 BSD 里的 Kqueue，或者是在 Windows 下的 IOCP，它们都只是一种“I/O 多路复用”技术。它们只在“连接数为 10000，但同时收发数据的客户端只有 500 个”的场景下才有意义。换句话说，它们解决的是“明明还有 CPU 和内存资源，为什么客户端无法与服务器建立 TCP 连接”的问题。如果同时有 10000 台客户端同时进行数据收发，那么 epoll 和 select 之间并没有太大的性能差异。

### C10K 问题

Nginx 是第一个解决了 C10K 问题的 Web Server。

C10K 指的是单机维持一万个 TCP 连接，首次由 Dan Kegel 在 1999 年提出，最初的那个网页依然可以访问：http://www.kegel.com/c10k.html。

这个问题是怎么出现的呢？因为当初设计 Unix 操作系统的时候没有想到计算机软件的规模会发展到如此的大。就像人类已经经历过的千年虫问题，以及从现在开始 15 年后就要经历的 Unix 时间戳耗尽的问题，Unix 的进程 ID（PID）以及单个进程最大打开文件数都被设定为了“有符号的 16 位整数”，即从 -32767 ~ 32767，所以一台机器在当时的理论极限只有 32767。

Nginx 在用户态利用事件驱动机制，让自己的一个线程可以根据需要绑定多个 TCP 连接，通过这种方式大幅增加了单台服务器所能保持的 TCP 连接数，解决了 C10K 问题。
