INDEX
- §1 从经典面试题开始
- §2 多路复用整体模型
- §3 概念
- §4 系统函数 `select`、`poll`、`epoll`
- §5 以 Redis 为例描述完成流程
§1 从经典面试题开始
经典面试题:“Redis 是单线程还是多线程?”
答:
- redis 的指令线程依然是单线程
- 与客户端交互的网络 IO 支持多线程
其实还可以更深一步考较:“Redis 为什么要这么设计?”
答:
- redis 的指令线程依然是单线程
本质上是 redis 的文件事件处理器中,基于事件分派器的对事件队列的 消费(包括分派和处理) 是单线程的
这里可以单线程还保证速度的本质是,事件队列的 消费过程是完全基于内存的,不存在 IO 问题,没有阻塞 - 与客户端交互的网络 IO 支持多线程
本质上是因为客户端对服务端的网络 IO 不可能是单线程的,服务端不可能同时只接待一个客服端的连接
但 redis 是通过 IO 多路复用程序对Socket
的监听和管理的,这个 IO 多路复用程序本体是单线程的
这里不能在单线程的同时保持高速,是因为网络通讯依赖网络 IO 其模型不可能单线程,同时 IO 部分存在阻塞
§2 多路复用整体模型
IO 多路复用也被称为事件驱动型 IO,整体模型如下
建议先了解 IO 模式的演进 后再阅读
IO 多路复用的作用(好在哪)
- 读写操作都是有成本的,即不可能收到请求立即返回,而是需要收集数据等,这造成一定的阻塞
- 阻塞就意味着当前线程 CPU 时间的浪费,而这些时间即使不能加快读写,也可以去处理更多请求
- IO 多路复用就是为了 充分利用 IO 阻塞时浪费的 CPU 时间
§3 概念
文件描述符(File Descriptor)
- 用于表达对文件引用的抽象化指向
- 通常只适用于 Linux/Unix
- 本身是一个非负整数
- 是内核对每条进程维护的 进程打开文件的记录表 的索引
- 打开或创建文件时,文件会加入上述记录表,并返回文件描述符
IO 多路复用机制
多路复用是一种机制
- 这种机制的核心是如何同时监听多个文件描述符(fd,File Description)
- 并在一个被监听的文件描述符就绪时(通常是可以读写了),通知应用程序进行响应的操作
这种机制需要 select
、poll
、epoll
配合
- 多个连接共用一个阻塞对象
- 应用程序只在一个阻塞对象上等待,而不需要在每个需要阻塞的连接上等待
- 当某连接的数据或请求就绪可以处理时
- 操作系统 会通知应用程序
- 线程 从阻塞状态返回,并继续处理请求
反应堆模式(Reactor)
反应堆模式(Reactor) 的详细信息参考 设计 | 设计模式 - [Reactor]
redis 的多路复用机制就是使用 Reactor 模式实现的
- redis 使用 Reactor 模式实现了一套网络事件处理器,称为 文件事件处理器(File Event Handler)
- 文件事件处理器将 一个网络连接 对应为 一个文件描述符
- 文件事件处理器由 4 部分组成
- 套接字,即上图的 Client
- IO 多路复用程序,即上图的 Reactor
- 文件事件分派器,即对上图 dispatch 的封装
- 事件处理器,即上图的 Handler
§4 系统函数 select
、poll
、epoll
下面这三个函数是 Linux 的函数,多路复用底层是基于它们之一实现的
select
尤其注意不是 Selector.select()
- 阻塞函数,无数据时会阻塞
- 调用时,需要将文件描述符数组复制到内核
- 文件描述符数组由 bitmap 实现
- 当数据就绪时,对应 bitmap 位置 1,select 返回可读文件描述符的个数
- 客户端读取数据时,遍历文件描述符数组获取对应的
Socket
缺点
- bitmap 有长度限制,每进程最大 1024,超出大的客户端无法接待
- bitmap 的标记位不可重用,会在有数据时被置位,但不会被重置
- 文件描述符数组需要复制进内核,高并发下每个线程都要复制一份,开销可观
- 客户端真实读取数据时,需要遍历文件描述符数组获取对应
poll
- 阻塞函数,无数据时会阻塞
- 调用时,需要将文件描述符数组复制到内核
- 文件描述符数组由 结构体数组 实现,不仅仅是用一个 bit 而是一个结构体表示了
结构体可以理解为一个对象,包括如下属性
- fd ,文件描述符
- revents ,就绪标志
- events ,就绪的事件,比如读写
- 当数据就绪时,对应 revents 位置 1,同时记录事件类型,poll 返回
- 客户端读取数据时,遍历文件描述符数组获取对应的
Socket
- 对获取到的文件描述符数组对应元素重置状态
- 客户端处理数据
缺点
bitmap 有长度限制,每进程最大 1024,超出大的客户端无法接待
是一个正经的数组实现的bitmap 的标记位不可重用,会在有数据时被置位,但不会被重置
文件描述符的元素时结构体,并在客户端处理数据前进行重置- 文件描述符数组需要复制进内核,高并发下每个线程都要复制一份,开销可观
- 客户端真实读取数据时,需要遍历文件描述符数组获取对应
epoll
epoll
由 3 部分组成
-
epoll_create
,相当于创建文件描述符数组 -
epoll_ctl
,控制文件描述符数组中元素,如增删改等(相当于Selector.regist()
) -
epoll_wait
,相当于原来的select
或poll
- 非阻塞 函数
- 调用时,会创建文件描述符数组(不存在时),并向其中注册元素
- 文件描述符数组由 红黑树 实现
- 当数据就绪时,对应元素变相置位,即将对应元素放到根节点
- 网卡数据到达后,数据进入 DMA
DMA = direct memory access,它允许不同速度的硬件直接访问 - 网卡发送中断给 CPU ,中断在内存中绑定回调函数
- 回调函数会将对应的
Socket
存入就绪链表 (就是红黑树的前几个元素)
-
epoll
返回就绪的文件描述符个数 n - 客户端处理数据时,处理前 n 个文件描述符
横比
|
|
| |
数据结构 | bitmap | 数组 | 红黑树 |
最大连接数、文件描述符数量 | 1024 / 2048(x64) | 无上限(65535) | 无上限(65535) |
文件描述符数组复制进内核 | 完整复制 | 完整复制 | 通过 |
客户端获取就绪文件描述符的方式 | 遍历 | 遍历 | 回调后处理前几个 |
客户端获取就绪文件描述符的复杂度 |
§5 以 Redis 为例描述完成流程
IO 多路复用机制的流程
- 客服端通过
Socket
连接到服务端,并发送请求 - 服务端开辟
Socket
的内存结构体,并返回对应的 文件描述符 - 服务端将请求和文件描述符注册到 多路复用器
- 多路复用器 将文件描述符和请求按请求与对应的 事件处理器 进行关联
- 多路复用器 默认通过
epoll
函数监管文件描述符和请求
-
epoll
将文件描述符和请求传输至内核 - 若内核没有 开辟文件描述符数组 (其实是个红黑树),则通过
epoll_create
创建 - 通过
epoll_ctl
将文件描述符和请求注册到 文件描述符数组 - 通过
epoll_wait
实现对整个 文件描述符数组 的监听 - 数据就绪时,对应文件描述符数组元素被前置,并返回就绪数量 n
- 多路复用器 获取前 n 个文件描述符数组元素,并生成对应的 文件事件
- 多路复用器 将 文件事件 放到 事件队列 中
- 事件队列 经由单线程的 文件事件分派器 依次消费
- 文件事件分派器 将事件分发给关联的 事件处理器 进行处理