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 模式的演进 后再阅读

redis的io多路复用详解 redis的io多路复用机制_redis的io多路复用详解

IO 多路复用的作用(好在哪)

  • 读写操作都是有成本的,即不可能收到请求立即返回,而是需要收集数据等,这造成一定的阻塞
  • 阻塞就意味着当前线程 CPU 时间的浪费,而这些时间即使不能加快读写,也可以去处理更多请求
  • IO 多路复用就是为了 充分利用 IO 阻塞时浪费的 CPU 时间

§3 概念

文件描述符(File Descriptor)

  • 用于表达对文件引用的抽象化指向
  • 通常只适用于 Linux/Unix
  • 本身是一个非负整数
  • 是内核对每条进程维护的 进程打开文件的记录表 的索引
  • 打开或创建文件时,文件会加入上述记录表,并返回文件描述符

IO 多路复用机制
多路复用是一种机制

  • 这种机制的核心是如何同时监听多个文件描述符(fd,File Description)
  • 并在一个被监听的文件描述符就绪时(通常是可以读写了),通知应用程序进行响应的操作

这种机制需要 selectpollepoll 配合

  • 多个连接共用一个阻塞对象
  • 应用程序只在一个阻塞对象上等待,而不需要在每个需要阻塞的连接上等待
  • 当某连接的数据或请求就绪可以处理时
  • 操作系统 会通知应用程序
  • 线程 从阻塞状态返回,并继续处理请求

反应堆模式(Reactor)
反应堆模式(Reactor) 的详细信息参考 设计 | 设计模式 - [Reactor]

redis 的多路复用机制就是使用 Reactor 模式实现的

  • redis 使用 Reactor 模式实现了一套网络事件处理器,称为 文件事件处理器(File Event Handler)
  • 文件事件处理器将 一个网络连接 对应为 一个文件描述符
  • 文件事件处理器由 4 部分组成
  • 套接字,即上图的 Client
  • IO 多路复用程序,即上图的 Reactor
  • 文件事件分派器,即对上图 dispatch 的封装
  • 事件处理器,即上图的 Handler

§4 系统函数 selectpollepoll

下面这三个函数是 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,相当于原来的 selectpoll
  • 非阻塞 函数
  • 调用时,会创建文件描述符数组(不存在时),并向其中注册元素
  • 文件描述符数组由 红黑树 实现
  • 当数据就绪时,对应元素变相置位,即将对应元素放到根节点
  • 网卡数据到达后,数据进入 DMA
    DMA = direct memory access,它允许不同速度的硬件直接访问
  • 网卡发送中断给 CPU ,中断在内存中绑定回调函数
  • 回调函数会将对应的 Socket 存入就绪链表 (就是红黑树的前几个元素)
  • epoll 返回就绪的文件描述符个数 n
  • 客户端处理数据时,处理前 n 个文件描述符

横比

select

poll

epoll

数据结构

bitmap

数组

红黑树

最大连接数、文件描述符数量

1024 / 2048(x64)

无上限(65535)

无上限(65535)

文件描述符数组复制进内核

完整复制

完整复制

通过 epoll_ctl 增量控制

客户端获取就绪文件描述符的方式

遍历

遍历

回调后处理前几个

客户端获取就绪文件描述符的复杂度

redis的io多路复用详解 redis的io多路复用机制_数据库_02

redis的io多路复用详解 redis的io多路复用机制_数据库_02

redis的io多路复用详解 redis的io多路复用机制_文件描述符_04

§5 以 Redis 为例描述完成流程

IO 多路复用机制的流程

  • 客服端通过 Socket 连接到服务端,并发送请求
  • 服务端开辟 Socket 的内存结构体,并返回对应的 文件描述符
  • 服务端将请求和文件描述符注册到 多路复用器
  • 多路复用器 将文件描述符和请求按请求与对应的 事件处理器 进行关联
  • 多路复用器 默认通过 epoll 函数监管文件描述符和请求
  • epoll 将文件描述符和请求传输至内核
  • 若内核没有 开辟文件描述符数组 (其实是个红黑树),则通过 epoll_create 创建
  • 通过 epoll_ctl 将文件描述符和请求注册到 文件描述符数组
  • 通过 epoll_wait 实现对整个 文件描述符数组 的监听
  • 数据就绪时,对应文件描述符数组元素被前置,并返回就绪数量 n
  • 多路复用器 获取前 n 个文件描述符数组元素,并生成对应的 文件事件
  • 多路复用器文件事件 放到 事件队列
  • 事件队列 经由单线程的 文件事件分派器 依次消费
  • 文件事件分派器 将事件分发给关联的 事件处理器 进行处理