静态自学实验 / 4 篇博客 / 6 个知识点

Redis I/O:
从就绪到完成

不背术语表。沿着“谁等待、谁读取、通知的是什么”这三个问题,串起 Redis 的单线程执行、I/O 多路复用、epoll 与 Reactor / Proactor。

面试复习 约 25 分钟 无需联网 不保存进度
02 / Scope

本次实验的边界

纳入

  • Redis 高性能因素及“单线程”的准确含义
  • 阻塞、非阻塞、多路复用、异步 I/O
  • select / poll / epoll,LT / ET
  • Reactor / Proactor 的事件语义
先想一下如果单线程不会并行执行命令,为什么它反而可能更快?它最怕哪类命令?

四个因素一起工作

博客把 Redis 的速度归因于内存访问、高效数据结构、命令顺序执行,以及非阻塞 I/O + 多路复用。单线程只描述大部分命令的执行路径,不等于整个 Redis 进程只有一条线程。

顺序执行减少命令路径中的锁竞争、调度和上下文切换,也让单条命令不被另一条命令插入。但同一性质也形成队头阻塞:一个大 Key 或慢命令占住主线程,后续客户端都要等。

常见误区“Redis 单线程,所以后台持久化和网络读写也都在同一个线程。”博客明确区分了主线程命令执行、后台任务和 Redis 6 起可选的 I/O 线程。
自检
为什么“没有锁竞争”不能推出“任何命令都快”?
因为顺序执行只减少并发协调成本;一条本身耗时的命令仍会独占主执行路径,并把等待传播给后面的请求。
请用一句话改写“Redis 是单线程”。
较准确的表述是:Redis 的大部分命令主要由主线程顺序执行,但进程还会用后台线程,并可用 I/O 线程分担部分网络读写。

来源:src/content/blog/redis-interview/index.md,第 13-32 行

先想一下recv() 在“数据还没来”和“数据已经到内核”时,各自在等什么?

不要把“不等待”与“异步”混为一谈

接收网络数据可以拆成两个阶段:第一阶段等待数据进入内核 socket 缓冲区;第二阶段把数据交到用户缓冲区。同步非阻塞 I/O 只让第一阶段在未就绪时立即返回 EAGAIN,应用仍需反复查询,并亲自调用 recv() 完成第二阶段。

网络数据从网卡经过内核缓冲区到用户缓冲区 网卡 内核 socket 接收缓冲区 用户缓冲区 recv() 的目标 处理
模型 等待数据 复制到用户区 主要代价
同步阻塞 当前线程挂起 当前调用完成 连接多时线程成本高
同步非阻塞 未就绪即返回 当前调用完成 轮询与大量无效系统调用
异步 I/O 系统处理 系统完成后通知 接口与编程模型更复杂
常见误区“调用立即返回就是异步。”立即返回只说明当前没有阻塞;判断异步的关键是完成通知到来时,数据是否已经进入指定用户缓冲区。

来源:src/content/blog/redis-interview/多路IO复用.md,第 14-59、83-102 行

先想一下如果有 10,000 个 socket,逐个 recv() 询问和一次等待“任意一个就绪”有什么本质差别?

复用的是等待者,不是数据通道

I/O 多路复用让一个线程只阻塞一次,同时等待一组 fd 中任意成员就绪。它避免应用不断逐个轮询所有 socket,也不要求“一连接一线程”。

epoll_wait()
  → 内核报告 fd3 ready
  → 用户线程 recv(fd3)
  → 数据进入用户缓冲区
  → recv() 返回

这里的分界点非常重要:epoll_wait() 交付的是“就绪事件”,不是已经读好的数据。应用收到事件后还必须调用 recv(),因此第二阶段仍由当前线程同步完成。

常见误区“epoll 是异步 I/O。”epoll 优化的是等待就绪的方式;就绪之后,用户线程仍要执行真正的同步读写。
一句话自检如果某 API 通知你“fd 可读”,Handler 接下来还要调用 read(),它更接近就绪模型,而不是完成模型。

来源:src/content/blog/redis-interview/多路IO复用.md,第 61-81 行

先想一下FD_SETSIZE 是 socket 数量、fd 编号范围,还是扫描上界?如果没有 fd 立即就绪,select 如何避免持续占用 CPU?

三个机制的差别,抓住“每次都交什么”

机制 关注集合 返回前的工作 关键限制
select 每次传完整 fd_set 遍历检查,O(n) 位图;默认 fd 编号须小于 1024
poll 每次传 pollfd 数组 仍遍历数组,O(n) 去掉固定 FD_SETSIZE 限制
epoll 先用 epoll_ctl 注册 epoll_wait 返回 ready 事件 Linux 机制;维护关注列表与 ready list

select 的数量限制,先区分三个概念

glibc 默认的 fd_set 有 1024 位,能表示 fd 0-1023;这限制的是可放入集合的 fd 编号范围。nfds 应传三个集合中的最大 fd 编号加 1,它只是内核的扫描上界。一次调用实际监听的 socket 数量,则是集合中被置位的 socket fd 数量。

因此,“select 最多监听 1024 个 socket”只是一种不够严谨的简称。更准确的说法是:默认接口无法表示编号大于 1023 的 fd;进程可以拥有更多连接,只是不能把这些高编号 fd 交给这个 fd_set。

select 如何知道 fd 已就绪

Linux 内核先扫描集合中置位的 fd,查询每个 fd 对应对象当前是否满足可读、可写或异常条件。如果没有任何 fd 就绪且调用允许阻塞,内核会把当前任务挂到相关等待队列并让它睡眠;I/O 状态变化会唤醒任务,随后 select 再次扫描。

返回时,三个 fd_set 已被原地改写,只保留就绪的 fd。应用再用 FD_ISSET() 判断具体是哪几个。这里既不是用户态逐个调用 recv() 忙轮询,也不是内核执行用户态回调。

epoll_create1() 创建实例,epoll_ctl() 增删改关注的 fd,epoll_wait() 等待并返回一批就绪事件。它不是自动执行用户态回调,也不保证一次只返回一个 fd。

常见误区FD_SETSIZE 1024 限制的是传给 select 的 fd 编号范围,不宜直接说成“整个程序最多只能建立 1024 个连接”。
选择线索
  • 看到“位图 + 固定编号范围” → select
  • 看到“数组 + 仍线性扫描” → poll
  • 看到“注册关注 + 返回 ready list” → epoll

来源:src/content/blog/redis-interview/poll、select、epoll.md,select、poll 与 epoll 小节

先想一下一个 socket 缓冲区有 8 KB,ET 通知后你只读了 2 KB。没有新数据到来时,还会出现新的“边缘”吗?

LT 看状态,ET 看变化

LT(水平触发)在“仍然可读”这个条件持续成立时会继续通知;ET(边缘触发)只在状态变化时通知。ET 下若一次没读完,缓冲区依然处于可读状态,但没有新的边缘,应用可能等不到下一次通知。

收到 ET 可读事件
while true:
  n = read(nonblocking_fd)
  if n > 0: 处理数据,继续读
  if n == -1 and errno == EAGAIN: 本轮排空,停止
  if n == 0: 对端关闭

所以 ET 的配套纪律是:socket 设为非阻塞;每次事件到来后持续读取;直到 EAGAIN 明确表示当前已无数据。非阻塞保证循环不会在排空之后挂死。

故障诊断ET 模式偶发“连接还有数据却不再触发”,先检查 Handler 是否只 read 一次就返回,以及 fd 是否确实设置为非阻塞。

来源:src/content/blog/redis-interview/poll、select、epoll.md,第 58-62 行;流程为基于该规则的教学性展开

最可靠的问题:Handler 拿到事件后还要不要 read?

Reactor Proactor
事件语义 fd 已就绪 I/O 已完成
真正读写 Handler 调用 read/write 系统先把结果放入 Buffer
常见对应 epoll + 非阻塞 socket Windows Overlapped I/O + IOCP
主要代价 用户线程读写可能拖慢事件循环 编程复杂、平台支持差异

Reactor 的路径是“注册 → 等待就绪 → 分发 → Handler 读写 → 业务处理”。Proactor 的路径是“提交异步 I/O → 系统完成 → 完成事件入队 → 分发 → Handler 处理现成结果”。

io_uring 使用提交队列与完成队列,模型上接近 Proactor;但博客也强调,普通 Tokio 网络 I/O 主要仍是就绪模型,不能把“Rust async 语法”自动等同于内核完成式 I/O。

常见误区“用了 async/await,就是 Proactor。”语言层的异步控制流与操作系统 I/O 事件语义不是一回事;应检查底层通知的是 ready 还是 completed。
迁移判断

若现有 Handler 收到事件后调用 read(),迁移到完成式接口不只是把 API 名换掉:Buffer 所有权、提交队列、完成队列与错误处理都会改变。

来源:src/content/blog/redis-interview/Reactor 和 Proactor.md,第 14-52 行

03 / Self-check

先回答,再展开答案

题目从概念辨认走向故障诊断。页面不会记录结果,刷新后仍是初始状态。

Q1 · 知识点 01 · 基础

“Redis 单线程”最准确地指什么?

  1. 整个进程只能创建一个线程
  2. 大部分命令主要由主线程顺序执行
  3. 网络读写永远不能使用 I/O 线程
  4. 持久化必定阻塞主线程完成
查看答案与解析
B。“单线程”描述的是主要命令执行路径。A、C、D 都把局部模型错误扩张成了整个进程的绝对限制。
Q2 · 知识点 02 · 基础

同步非阻塞 I/O 为什么仍然是“同步”的?

用要点自评
必要要点:未就绪时立即返回只改变等待阶段;应用仍要主动调用 recv;数据复制到用户缓冲区仍在该调用中完成。若只说“因为它会轮询”,抓到了代价但没有回答分类依据。
Q3 · 知识点 03 · 基础

epoll_wait() 返回 fd 可读后,数据是否已经在应用指定的用户缓冲区中?

  1. 一定已经完成复制
  2. 只要是 ET 就已经完成
  3. 不一定;应用仍需调用 recv/read
  4. 只有 fd 小于 1024 时才没有完成
查看答案与解析
C。epoll 提供的是就绪通知。A 混淆了完成式 I/O;B 把触发模式当作读取语义;D 把 select 的编号限制错误套到 epoll。
Q4 · 知识点 04 · 区分

哪个机制解决了固定 FD_SETSIZE,但每次调用仍需遍历整个数组?

  1. epoll
  2. IOCP
  3. select
  4. poll
查看答案与解析
D。poll 用 pollfd 数组替代位图,去掉固定编号上限,但没有消除线性遍历。select 仍受位图范围限制;epoll 采用注册与 ready list 思路。
Q5 · 知识点 04 · 纠错

默认情况下,select 到底可以监听多少个 socket?

用要点自评
不要只答“1024 个”。glibc 默认的 fd_set 能表示 fd 0-1023,限制的是 fd 编号范围;nfds 是最大 fd 编号加 1;实际监听数量取决于集合中置位了多少个 socket fd。进程可以拥有更多连接,但默认 fd_set 无法表示编号大于 1023 的 fd。
Q6 · 知识点 04 · 基础

select 如何感知某个 fd 已经就绪?

用要点自评
内核扫描集合中置位的 fd,查询其当前就绪状态。若没有 fd 就绪且允许阻塞,当前任务会进入相关等待队列并睡眠;I/O 状态变化将其唤醒后,select 再扫描,并把传入集合改写成只包含就绪 fd 的结果。应用用 FD_ISSET() 读取结果。
Q7 · 知识点 05 · 应用

ET 模式收到可读事件后只读取一次便返回,最可能出现什么问题?

  1. 剩余数据可能因没有新边缘而不再通知
  2. 内核自动切换成 LT
  3. epoll_wait 自动继续执行 read
  4. fd 编号会被重新分配
查看答案与解析
A。ET 通知状态变化,不重复通知持续状态。正确做法通常是非阻塞读取直到 EAGAIN。
Q8 · 知识点 06 · 区分

Handler 收到事件后仍要自己调用 read(),这个系统更接近哪种模式?

  1. Proactor
  2. Reactor
  3. 纯异步完成模型
  4. AOF Rewrite
查看答案与解析
B。Reactor 分发的是就绪事件;真正 I/O 由 Handler 完成。Proactor 的 Handler 接到的是完成结果。
Q9 · 知识点 02 + 06 · 迁移

为什么把 Tokio 代码写成 async/await,不能单凭语法就断言底层是 Proactor?

用要点自评
必要要点:语言异步控制流与内核 I/O 模型是两个层次;普通 Tokio 网络 I/O 主要使用就绪模型;判断标准应是底层交付 ready 事件还是 completed 结果。可补充:io_uring 需要相应支持或额外依赖。
Q10 · 知识点 01 + 03 · 诊断

网络层使用 epoll,但 Redis 仍出现全局延迟尖峰。给出一个来自课程材料的非网络解释。

用要点自评
可接受:大 Key、慢命令、持久化或系统调度。核心逻辑是:主命令路径顺序执行,一项长任务会让后续请求排队;使用多路复用并不能消除命令执行阶段的阻塞。
Q11 · 综合 · 应用

一个服务“注册 fd → 等待可读 → Handler 非阻塞读到 EAGAIN”。请为三段分别命名。

查看答案与解析
注册/等待部分可对应 epoll 驱动的 Reactor;“可读”是就绪事件;读到 EAGAIN 是 ET 模式下排空当前数据的典型处理纪律。仅凭这段不能称为 Proactor,因为 Handler 仍亲自 read。
04 / Sources

来源覆盖

所有核心结论都锚定到本地博客。本页没有联网补充外部事实,也没有复制项目中的运行时状态逻辑。

  1. Redis 面试笔记src/content/blog/redis-interview/index.md,使用第 13-32 行;第 35-48 行持久化内容本次排除
  2. Redis 中的 I/O 多路复用src/content/blog/redis-interview/多路IO复用.md · 全文覆盖
  3. select、poll 和 epollsrc/content/blog/redis-interview/poll、select、epoll.md · 全文覆盖
  4. Reactor 和 Proactorsrc/content/blog/redis-interview/Reactor 和 Proactor.md · 全文覆盖