纳入
- Redis 高性能因素及“单线程”的准确含义
- 阻塞、非阻塞、多路复用、异步 I/O
- select / poll / epoll,LT / ET
- Reactor / Proactor 的事件语义
不背术语表。沿着“谁等待、谁读取、通知的是什么”这三个问题,串起 Redis 的单线程执行、I/O 多路复用、epoll 与 Reactor / Proactor。
顺序不是照抄文件目录,而是按先修关系重排。每一项只承担一个可检验的学习目标。
博客把 Redis 的速度归因于内存访问、高效数据结构、命令顺序执行,以及非阻塞 I/O + 多路复用。单线程只描述大部分命令的执行路径,不等于整个 Redis 进程只有一条线程。
顺序执行减少命令路径中的锁竞争、调度和上下文切换,也让单条命令不被另一条命令插入。但同一性质也形成队头阻塞:一个大 Key 或慢命令占住主线程,后续客户端都要等。
来源:src/content/blog/redis-interview/index.md,第 13-32 行
recv()
在“数据还没来”和“数据已经到内核”时,各自在等什么?
接收网络数据可以拆成两个阶段:第一阶段等待数据进入内核 socket
缓冲区;第二阶段把数据交到用户缓冲区。同步非阻塞 I/O
只让第一阶段在未就绪时立即返回
EAGAIN,应用仍需反复查询,并亲自调用
recv()
完成第二阶段。
| 模型 | 等待数据 | 复制到用户区 | 主要代价 |
|---|---|---|---|
| 同步阻塞 | 当前线程挂起 | 当前调用完成 | 连接多时线程成本高 |
| 同步非阻塞 | 未就绪即返回 | 当前调用完成 | 轮询与大量无效系统调用 |
| 异步 I/O | 系统处理 | 系统完成后通知 | 接口与编程模型更复杂 |
来源:src/content/blog/redis-interview/多路IO复用.md,第 14-59、83-102 行
recv()
询问和一次等待“任意一个就绪”有什么本质差别?
I/O 多路复用让一个线程只阻塞一次,同时等待一组 fd 中任意成员就绪。它避免应用不断逐个轮询所有 socket,也不要求“一连接一线程”。
epoll_wait()
→ 内核报告 fd3 ready
→ 用户线程 recv(fd3)
→ 数据进入用户缓冲区
→ recv() 返回
这里的分界点非常重要:epoll_wait()
交付的是“就绪事件”,不是已经读好的数据。应用收到事件后还必须调用
recv(),因此第二阶段仍由当前线程同步完成。
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 |
glibc 默认的 fd_set 有 1024 位,能表示 fd
0-1023;这限制的是可放入集合的 fd 编号范围。nfds
应传三个集合中的最大 fd 编号加
1,它只是内核的扫描上界。一次调用实际监听的 socket
数量,则是集合中被置位的 socket fd 数量。
因此,“select 最多监听 1024 个
socket”只是一种不够严谨的简称。更准确的说法是:默认接口无法表示编号大于
1023 的 fd;进程可以拥有更多连接,只是不能把这些高编号 fd
交给这个 fd_set。
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 个连接”。
来源:src/content/blog/redis-interview/poll、select、epoll.md,select、poll 与 epoll 小节
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
明确表示当前已无数据。非阻塞保证循环不会在排空之后挂死。
来源:src/content/blog/redis-interview/poll、select、epoll.md,第 58-62 行;流程为基于该规则的教学性展开
| 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。
若现有 Handler 收到事件后调用
read(),迁移到完成式接口不只是把 API
名换掉:Buffer 所有权、提交队列、完成队列与错误处理都会改变。
来源:src/content/blog/redis-interview/Reactor 和 Proactor.md,第 14-52 行
题目从概念辨认走向故障诊断。页面不会记录结果,刷新后仍是初始状态。
epoll_wait()
返回 fd 可读后,数据是否已经在应用指定的用户缓冲区中?
select
到底可以监听多少个 socket?
fd_set 能表示 fd
0-1023,限制的是 fd 编号范围;nfds
是最大 fd 编号加 1;实际监听数量取决于集合中置位了多少个
socket fd。进程可以拥有更多连接,但默认
fd_set
无法表示编号大于 1023 的 fd。
select 如何感知某个 fd 已经就绪?select
再扫描,并把传入集合改写成只包含就绪 fd 的结果。应用用
FD_ISSET()
读取结果。
read(),这个系统更接近哪种模式?
所有核心结论都锚定到本地博客。本页没有联网补充外部事实,也没有复制项目中的运行时状态逻辑。