Skip to content

第 8 章 · 多路复用面试题集锦

学完本章你将获得:把前 7 章的知识转化成"面试可用语言"的能力。本章按难度分级,每题给标准答案 + 追问 + 加分项。共 35 道高频题,覆盖 80% 的真实面试场景。


0. 怎么用这一章?

答题三段论模板

不管什么题都用这个结构回答,远超大多数候选人:

  1. 是什么:用一句话定义概念
  2. 为什么:解决了什么问题、和什么对比
  3. 怎么用:举一个例子或场景

答题节奏建议

  • 前 5 秒沉思:组织语言比抢答更重要
  • 先讲结论再展开:避免漫无边际
  • 主动给追问点:"这里还有个细节...如果您感兴趣我可以展开"
  • 承认不知道也是加分:装懂被识破直接挂

1. 基础题(6 道,所有岗位都会问)

Q1.1 什么是 I/O 多路复用?

标准答案:I/O 多路复用是一种让**单个线程同时监听多个文件描述符(fd)**的机制——任意一个 fd 就绪时,内核统一返回给用户线程处理。Linux 上有三种实现:select、poll、epoll。

追问 1:为什么需要多路复用?

A:避免"一连接一线程"的资源爆炸。比如 Tomcat 早期 BIO 模式 1 万连接需要 1 万个线程,内存和上下文切换开销巨大。多路复用让一个线程管几万 fd,极大降低开销。

追问 2:与多线程对比,多路复用的本质优势是什么?

A:① 内存:线程栈典型 1-2 MB,10000 线程 = 10-20 GB;epoll 只需几个线程;② 上下文切换:线程切换 ~1-2 μs,频繁切换吃满 CPU;epoll_wait 唤醒只需几百 ns。


Q1.2 select / poll / epoll 三者的区别?

标准答案(按维度对比):

  1. 数据结构:select 用位图(1024 上限);poll 用结构体数组(无上限);epoll 用红黑树 + 就绪链表
  2. 性能:select/poll O(n);epoll O(就绪 fd 数)
  3. 拷贝:select/poll 每次都要全量传 fd 集合;epoll 通过 epoll_ctl 一次注册
  4. 跨平台:select 全 OS 通用;poll Unix-like;epoll 只 Linux

追问 1:为什么 epoll 是 O(就绪数) 而不是 O(n)?

A:内核在每个被监听 fd 的等待队列里注册了 ep_poll_callback——数据来时回调主动把 fd 放进就绪链表。epoll_wait 直接从就绪链表取,与监听总数无关。

追问 2:epoll 一定比 select 快吗?

A:不一定。当所有 fd 都同时就绪(边界场景)时,三者性能接近。epoll 的优势在"fd 多但就绪稀疏"——这恰好是高并发服务器的常态。


Q1.3 select 为什么有 1024 这个限制?

标准答案:因为 fd_set 在内核里是个固定大小的位图——__FD_SETSIZE = 1024,是编译期常量。位图在 64 位机器上是 16 个 unsigned long = 128 字节 = 1024 bit,每 bit 表示一个 fd 是否被关注。

追问 1:能改大吗?

A:理论上可以重新编译 glibc 改大,但实践不可行——影响整个用户态库栈,且即使改大了 select 仍是 O(n),没有意义。正确做法是换 poll/epoll

追问 2:FD_SET(2000, &s) 会发生什么?

A:未定义行为——会写到位图外的内存。可能踩坏栈,导致随机崩溃。在多线程程序里几乎一定出问题。


Q1.4 用户态和内核态是什么?切换有什么代价?

标准答案:CPU 的两种特权级——内核态(Ring 0)能操作硬件、访问全部内存;用户态(Ring 3)受限沙盒。用户程序通过系统调用进入内核态。切换涉及保存/恢复寄存器、页表切换,典型代价 100~1000 纳秒——比函数调用慢 100 倍。

追问:这和 epoll 性能有什么关系?

A:select/poll 每次都要拷贝整个 fd 集合(系统调用次数多、单次开销大);epoll 通过 epoll_ctl 一次注册长期复用,把"高频 syscall"降到"低频 ctl + 高频 wait"。这是 epoll 性能优势的核心来源。


Q1.5 什么是文件描述符(fd)?

标准答案:fd 是内核给每个打开的资源(文件 / socket / 管道 / 设备等)分配的非负整数标识。用户程序拿这个数字操作对应资源,无需关心内部细节。Linux 哲学:一切皆文件——所以 socket / epoll 实例 / 信号事件都是 fd。

追问 1:同一个进程能有多少个 fd?

A:受 ulimit -n(默认 1024)限制——生产环境要拉到 65535+。

追问 2:close 之后 fd 数字会复用吗?

A:会!close 后下一次 open 可能拿到相同的数字。所以代码缓存 fd 时要小心,最好用对象指针包装。


Q1.6 阻塞 I/O 和非阻塞 I/O 的区别?

标准答案:阻塞 I/O 在数据没到时线程被挂起直到数据可用;非阻塞 I/O 立即返回 -1, errno=EAGAIN,线程继续做别的事(但需要自己轮询)。设置非阻塞用 fcntl(fd, F_SETFL, ... | O_NONBLOCK)

追问:非阻塞 I/O 单独用有意义吗?

A:基本没意义——自己轮询会吃满 CPU。非阻塞是为多路复用做准备的,所有 epoll ET 模式的 socket 都必须是非阻塞的。


2. 进阶题(10 道,后端岗常问)

Q2.1 epoll 的 LT 和 ET 模式有什么区别?

标准答案

  • LT(水平触发,默认):只要 fd 缓冲区有数据/有空间,就反复通知——少读一次也没关系,下次 epoll_wait 还会通知
  • ET(边沿触发):只在状态变化的瞬间通知一次——必须循环读到 EAGAIN,否则丢事件

追问 1:为什么 ET 必须配非阻塞 socket?

A:因为 ET 通知一次后必须读完所有数据——所以代码里要 while (read != EAGAIN)最后一次 read 在没数据时:阻塞 socket 会卡死整个事件循环;非阻塞 socket 才会返回 EAGAIN 让你跳出循环。

追问 2:ET 比 LT 快多少?

A:业界测试 ET 比 LT 大约少 30-40% syscall 次数——因为 LT 同一个 fd 可能被反复通知多次。代价是代码复杂度高很多。

追问 3:什么场景该选 ET?什么场景该选 LT?

A:① 极致性能、能保证 read 循环到 EAGAIN:ET(Nginx 选择);② 简洁优先、单线程命令处理:LT(Redis 选择);③ 新手 → LT。


Q2.2 请描述 epoll 的内核数据结构

标准答案:epoll 实例(struct eventpoll)内部有 3 个核心数据结构:

  1. 红黑树(RB tree):存所有被监听的 fd——key 是 fd,value 是 struct epitem。增删改查 O(log n)。
  2. 就绪链表(ready list):存已经触发事件的 epitem——双向链表,O(1) 取走
  3. 等待队列(wait queue):调 epoll_wait 的进程在没事件时挂这里睡眠

追问 1:为什么用红黑树而不是哈希表?

A:① 红黑树插入/查询/删除都 O(log n),worst case 稳定——哈希表最坏 O(n);② 红黑树天然有序,方便区间操作;③ 不需要扩容/rehash 卡顿。

追问 2:就绪链表里的 epitem 是什么时候放进去的?

A:内核网络栈在数据到达时遍历 socket 等待队列,调用 ep_poll_callback——这个回调把 epitem 放进就绪链表,并唤醒等待队列里睡眠的进程。


Q2.3 close 一个 fd 后,需要 epoll_ctl(EPOLL_CTL_DEL) 吗?

标准答案通常不需要——内核在 fd 真正释放(引用计数为 0)时会自动从所有 epoll 实例的红黑树中摘除。但有一个例外:fork 后子进程还持有这个 fd 的引用,父进程 close 不会真释放,这时父进程的 epoll 仍可能触发。

追问:那为什么很多代码还是显式 DEL?

A:① 防御性编程,明确语义;② 避免短暂窗口期内"已 close 但还在就绪链表里"的事件触发自己;③ 多进程场景必须显式 DEL。


Q2.4 什么是惊群(Thundering Herd)?怎么解决?

标准答案:多个进程/线程都在 epoll_wait 同一个 listen_fd——一个新连接到达时全部被唤醒,但只有一个能 accept 成功,其他白醒(白白消耗 CPU 和上下文切换)。

解决方案

  1. SO_REUSEPORT(Linux 3.9+,推荐):每个 worker 独立 listen_fd,内核按 5 元组哈希做负载均衡
  2. EPOLLEXCLUSIVE(Linux 4.5+):内核保证只唤醒一个等待者
  3. 应用层互斥锁(Nginx accept_mutex):进程间锁串行化 accept

追问 1:SO_REUSEPORT 比 mutex 好在哪?

A:① 无锁竞争;② 多核扩展性更好(实测 30%+ QPS 提升);③ 实现简单,不用应用层管理锁状态。

追问 2:accept 惊群和 epoll 惊群是一回事吗?

A:不是!① accept 惊群:多个进程同时 accept 同一 listen_fd——Linux 2.6 后内核已修复(只唤醒一个);② epoll 惊群:epoll_wait 的惊群——这是 epoll 自己的问题,需要 EPOLLEXCLUSIVE 或 SO_REUSEPORT 解决。


Q2.5 epoll_wait 的 maxevents 参数填多少合适?

标准答案:业界经验值 64 ~ 1024

  • 太小(如 16):一次取不完,慢的 fd 可能"饿死"
  • 太大(如 100000):单次循环过长,影响整体延迟

实际值:Nginx 默认 512,Redis 默认 1024,libuv 默认 1024。

追问:怎么动态调整?

A:① 监控 epoll_wait 返回值,如果经常等于 maxevents 说明取不完,应调大;② 业务延迟敏感场景(金融交易)调小,吞吐优先场景(日志聚合)调大。


Q2.6 EPOLLONESHOT 是什么?什么时候用?

标准答案EPOLLONESHOT 表示一个 fd 触发一次事件后自动从监听集移除——直到你重新 EPOLL_CTL_MOD 把它"挂回去"才会再触发。

典型场景多线程 worker 模型——同一个 fd 不能被多个 worker 同时处理(数据顺序乱),加 ONESHOT 后:

  1. 主线程 epoll_wait → 派给 worker A
  2. fd 自动从 epoll 摘除
  3. worker A 处理完 → MOD 把 fd 挂回去
  4. 下次再有事件 → 派给某个 worker(可能是 B)

追问:MOD 之前可以让其他线程操作这个 fd 吗?

A:不行。ONESHOT 只保证"epoll 不会再通知",但 fd 本身仍可读写——必须由 worker A 完成所有处理后再 MOD。


Q2.7 五大 I/O 模型分别是什么?多路复用是同步还是异步?

标准答案:① 阻塞 I/O;② 非阻塞 I/O;③ I/O 多路复用;④ 信号驱动 I/O;⑤ 异步 I/O。

多路复用是「同步 + 阻塞」的——用户线程阻塞在 select/epoll_wait,等待"任意 fd 就绪"。真正的异步 I/O 是 io_uring / Windows IOCP——内核完成全部读写后再通知用户。

追问:epoll 和 AIO 的本质差异?

A:epoll 通知"fd 可读了,你来 read";AIO 通知"buf 里数据已经准备好了"。前者用户还要做一次 read 调用(拷贝阶段同步),后者用户什么都不用做。


Q2.8 Reactor 模式和 Proactor 模式的区别?

标准答案

  • Reactor(同步):事件分派器等到事件后通知用户线程"该读了"——用户自己 read。多路复用类(epoll)属于 Reactor
  • Proactor(异步):内核自己完成 I/O 后把数据交给用户——用户拿到的就是结果。Windows IOCP / Linux io_uring 属于 Proactor

追问:Netty 是 Reactor 还是 Proactor?

A:是 Reactor——它基于 epoll(或 NIO Selector),还要自己 read。Java 的 AIO(NIO.2)名义上是 Proactor,但 Linux 实现底层还是 epoll,所以业界几乎不用 Java AIO


Q2.9 为什么 Nginx 用多进程,Redis 用单线程,Netty 用线程池?

标准答案

  • Nginx 多进程:进程隔离(崩了不影响其他)+ 进程独立绑核 + 配 SO_REUSEPORT 内核负载均衡 + Web 服务负载稳定
  • Redis 单线程:数据全在内存,瓶颈是 CPU 而不是 I/O;单线程避免锁竞争 + 简化数据结构 + 命令原子性自然保证
  • Netty 线程池:Java 多线程开销低(vs 多进程),需要在同进程内复用 JVM 内存;主从 Reactor 充分利用多核

追问:Redis 6.0 的 IO 线程是怎么回事?

A:6.0 把"网络读写"这一步多线程化(IO Threads 默认 4 个),但命令执行仍然单线程。这样既享受了多线程的网络吞吐,又保留了单线程的原子语义。


Q2.10 解释一下 EAGAIN 和 EWOULDBLOCK

标准答案:两者在 Linux 上是同一个值——非阻塞 I/O 调用在"数据未就绪"时返回 -1 + 设置 errno 为 EAGAIN/EWOULDBLOCK。代码里用 errno == EAGAIN || errno == EWOULDBLOCK 双重判断是为了跨 Unix 移植

追问:什么时候会遇到 EAGAIN?

A:① 非阻塞 socket read 没数据;② 非阻塞 socket write 缓冲区满;③ 非阻塞 accept 没新连接;④ epoll ET 模式下读完所有数据时。ET 模式必须用 EAGAIN 作为退出 read 循环的信号


3. 高级题(12 道,资深岗 + 系统设计常考)

Q3.1 epoll 的 ep_poll_callback 是什么?什么时候触发?

标准答案ep_poll_callback 是 epoll 在每个被监听 fd 的等待队列里注册的回调函数。当网络栈接收到数据 → 唤醒 socket 等待队列时,就会调用这个回调——把对应的 epitem 放进 epoll 的就绪链表,并唤醒在 epoll_wait 上睡眠的进程。

追问:所以 epoll 是事件驱动的,select 是查询驱动的?

A:可以这么理解。epoll = 内核主动 push 就绪 fd;select/poll = 用户主动 pull 全量 fd。push vs pull 的差异,正是 O(1) vs O(n) 的根源。


Q3.2 描述一下 socket 收到数据后到 epoll_wait 返回的完整路径

标准答案

  1. 网卡收到包 → DMA 到内核 ring buffer → 触发软中断
  2. 内核网络栈 解析 TCP/IP → 数据放入 socket 接收缓冲区
  3. 唤醒 socket 等待队列 → 遍历队列里所有 entry
  4. 执行 epoll 注册的 ep_poll_callback → 把 epitem 放进 epoll 就绪链表
  5. 唤醒 epoll 等待队列 → 在 epoll_wait 上睡眠的进程被唤醒
  6. 进程被调度上 CPU → epoll_wait 把就绪链表拷贝到用户 events 数组返回

追问:哪些步骤可能成为瓶颈?

A:① 网卡中断处理:用 NAPI 或 DPDK 旁路;② socket 缓冲区拷贝:用 zero-copy / splice / sendfile;③ 进程调度:避免上下文切换,用 CPU 绑核 + 用户态协程。


Q3.3 什么是零拷贝?epoll 用了零拷贝吗?

标准答案:**零拷贝(zero-copy)**指减少数据在用户态/内核态之间的内存拷贝次数。常见手段:

  • sendfile():文件直接从 page cache 发到网卡,跳过用户态
  • splice():在两个 fd 之间直接传数据
  • mmap() + write:把文件 mmap 到用户内存,避免 read 拷贝

epoll 本身不算零拷贝——它只是"通知机制",真正读写还是要 read/write。但 Nginx 等会结合 epoll + sendfile做到端到端零拷贝。


Q3.4 io_uring 是什么?相比 epoll 优势在哪?

标准答案io_uring 是 Linux 5.1(2019)引入的真异步 I/O 框架——通过两个用户态/内核态共享的无锁环形队列(SQ 提交队列 + CQ 完成队列)实现:

  1. 用户把 I/O 请求批量写入 SQ
  2. 内核异步处理(包括读、写、accept、connect 等)
  3. 完成后写入 CQ,用户读取结果

优势:① 真异步——内核完全代办,用户拿到的就是结果;② 批量提交——一次 syscall 可提交几千个请求;③ 支持文件 I/O(epoll 主要是 socket);④ 少 syscall——某些模式下 I/O 全程不用 syscall。

追问:io_uring 会取代 epoll 吗?

A:5-10 年内不会完全取代——io_uring 还在快速迭代,API 复杂度高,且需要 Linux 5.1+。当前 Nginx、PostgreSQL 等已经在引入,但 epoll 仍是绝对主力。


Q3.5 epoll 自身是不是 fd?能监听另一个 epoll 吗?

标准答案是 fd——epoll_create1() 返回的就是 fd(指向 anon_inode:[eventpoll])。可以用一个 epoll 监听另一个 epoll(嵌套)。

追问:嵌套有什么用?

A:libev / libuv 等库用嵌套实现层级化事件循环——比如把不同优先级的事件放到不同的 epoll,再用一个根 epoll 统一调度。但嵌套深度受限(防止内核栈爆掉),一般不超过 5 层。


Q3.6 epoll_wait 的 timeout 参数有什么坑?

标准答案

  1. -1 = 永久阻塞;0 = 立即返回;正数 = ms 超时
  2. timeout 精度受内核 HZ 限制——典型 4 ms 精度,传 1 ms 实际可能等到 4 ms
  3. timeout 可能因信号被打断——返回 -1, errno=EINTR,要判断后重试
  4. timeout 不会被改写(不像某些实现下的 select 的 timeval)

追问:怎么实现高精度定时?

A:用 timerfd_create 创建定时器 fd → 注册到 epoll → epoll_wait 自然唤醒——精度可达 ns 级。


Q3.7 LT 模式下,如果 EPOLLIN 一直不读,会发生什么?

标准答案每次 epoll_wait 都会立刻返回这个 fd ——CPU 100% 空转。这种状态叫 "busy loop"。

正确处理:要么 read 走数据,要么把 fd 从 epoll 摘除(DEL,或 MOD 去掉 EPOLLIN)。

追问:ET 模式没这问题吧?

A:ET 没这种 busy loop——但 ET 自己有反过来的坑:漏读一次就丢事件。不同 trade-off。


Q3.8 为什么 Linux 服务器要 ulimit -n?默认 1024 够用吗?

标准答案ulimit -n 是单进程 fd 数软上限,默认 1024(基于 70 年代 Unix 的历史)。生产服务器必须拉到 65535+——否则到几千连接就 EMFILE,新连接全部被拒绝。

bash
# 临时
ulimit -n 65535

# 永久(/etc/security/limits.conf)
* soft nofile 65535
* hard nofile 65535

# systemd 服务
LimitNOFILE=65535

追问:能调多大?

A:受 /proc/sys/fs/file-max 全局上限限制(典型几百万)。但调太大没意义——内存吃不消(每个 fd 内核结构 ~1KB,100 万 fd ≈ 1 GB)。


Q3.9 epoll_pwait 和 epoll_wait 区别?

标准答案epoll_pwait 多了一个 sigmask 参数——可以原子地修改信号掩码 + epoll_wait,避免"先 sigprocmask 再 epoll_wait"中间窗口期被信号干扰的问题。多线程信号处理代码必备

c
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
int n = epoll_pwait(epfd, events, max, -1, &mask);

Q3.10 为什么不能在信号处理函数里调用 epoll_ctl?

标准答案epoll_ctl 不是 async-signal-safe 的(POSIX.1 规定的"信号处理器里安全可用"的函数列表里没有它)。在信号处理器里调用可能导致死锁、状态不一致、内存破坏

正确做法:用 signalfd 或"自管道技巧(self-pipe trick)"——把信号转换为可读 fd 事件,再走正常 epoll 流程处理。


Q3.11 高并发服务器除了 epoll 还要做哪些优化?

标准答案(清单):

  1. fd 数ulimit -n 65535
  2. TCP 调优SOMAXCONNTCP_DEFER_ACCEPTTCP_NODELAYTCP_QUICKACK
  3. 多核利用:SO_REUSEPORT + worker per CPU + taskset 绑核
  4. 零拷贝:sendfile / splice / mmap
  5. 内存池:避免频繁 malloc/free(slab / arena 分配器)
  6. 协程:用 Golang goroutine / Rust async 等隐藏异步复杂度
  7. NUMA 亲和:worker 绑到本地 NUMA 节点
  8. 关掉无用 syscall:用 vDSO(gettimeofday 等)

Q3.12 实际生产中遇到 epoll 相关 bug,怎么排查?

标准答案(思路):

  1. 看进程 fd 数ls /proc/<pid>/fd | wc -l ——是否泄漏?
  2. strace 看 syscallstrace -p <pid> -e epoll_wait,epoll_ctl,read,write ——是否卡在某个 syscall?
  3. perf topperf top -p <pid> ——CPU 热点在哪?
  4. 检查 EAGAIN 处理:是否在 ET 模式下漏循环?
  5. dmesg 看内核日志:是否有 OOM、连接拒绝、TCP 包丢失?
  6. netstat / ss:连接状态分布——CLOSE_WAIT 多说明你忘了 close?

4. 场景题(5 道,系统设计 / 架构师必问)

Q4.1 让你设计一个 C100K 的 echo 服务器,会怎么做?

标准答案(按层次回答):

网络层

  • SO_REUSEPORT + per-CPU listen socket
  • epoll ET 模式 + 非阻塞 fd
  • TCP_NODELAY 关 Nagle 算法

架构层

  • 主从 Reactor:1 boss accept + N worker(CPU 核数)
  • worker 绑核(taskset)
  • 共享内存 / 无锁队列做线程间通信

资源层

  • ulimit -n 1048576
  • 调大 net.core.somaxconn / tcp_max_syn_backlog
  • 用 jemalloc / tcmalloc 替代默认 malloc

代码层

  • 每个 fd 分配复用对象池(避免 malloc)
  • 缓冲区预分配(避免栈分配)
  • 错误路径完整 close fd

Q4.2 如果 epoll_wait 偶尔会"漏掉事件",可能是什么原因?

标准答案(排查清单):

  1. ET 模式没循环到 EAGAIN ← 最常见
  2. EPOLLONESHOT 后忘了 MOD ← 多线程场景常见
  3. fd 已被 close 但还在缓存 ← 用对象指针而非裸 fd
  4. events 数组太小 ← 调大 maxevents
  5. EPOLL_CTL_DEL 后又触发 ← 删除前最后一次事件
  6. 多线程并发操作同一 fd ← 加 ONESHOT 或锁

Q4.3 业务报"客户端连不上",但 netstat 显示服务器还在监听端口,怎么排查?

标准答案(思路):

  1. 看 accept 队列ss -lnt ——Recv-Q 是否打满(说明 backlog 溢出)
  2. 看 fd 用尽/proc/<pid>/fd ——已超 ulimit?
  3. strace 看 accept 是否被调用:业务代码可能卡在某个 syscall 没回到 epoll_wait
  4. 看防火墙:iptables / 安全组是否新加了规则
  5. 看连接堆积ss -tan | grep CLOSE_WAIT | wc -l ——大量 CLOSE_WAIT 说明业务忘了 close
  6. 看 dmesg:内核是否打 "TCP: too many SYNs"

Q4.4 单机能撑多少 epoll 连接?理论上限是什么?

标准答案

  • fd 上限/proc/sys/fs/file-max(通常几百万)
  • 内存:每个 TCP 连接内核约 4-10 KB(含 socket 结构 + buffer),100 万连接需要 ~10 GB 内存
  • CPU:epoll_wait 本身不是瓶颈,但每个连接的业务处理是
  • 典型:单机 100 万长连接(IM 类业务)已是工业级实践——微信、QQ 都做到过

追问:靠什么实现 100 万连接?

A:① 调小内核 buffer(tcp_rmem / tcp_wmem);② 用协程而非线程(Go / Rust async);③ epoll ET + zero-copy;④ 多机分片,单机不强求极限。


Q4.5 在容器(Docker / K8s)里跑 epoll 服务,有什么特别注意的?

标准答案

  1. fd 上限:容器默认继承 host 的 ulimit,但 Docker 默认软上限只 1024 → 用 --ulimit nofile=65535:65535
  2. conntrack 表:iptables 转发会经过 conntrack,默认 65536 → 拉到 200 万(net.netfilter.nf_conntrack_max
  3. TIME_WAIT 重用:开 net.ipv4.tcp_tw_reuse
  4. CPU 绑核:K8s 用 cpuManagerPolicy: static + Guaranteed QoS
  5. NUMA:HugePage + numactl
  6. eBPF 监控:用 cilium / pixie 替代 iptables 损耗

5. 易错点 & 加分项

5.1 容易答错的点

题目错误答案正确答案
多路复用是异步吗?"是"不是!是同步阻塞,阻塞在 select/epoll_wait
select 1024 是 fd 编号上限吗?"是 fd ≤ 1024"是 fd_set 位图大小,FD_SET(1024,...) 已越界
close 后要 epoll_ctl(DEL)"必须"通常不需要,内核自动摘除
ET 一定比 LT 快很多"对"大约 30%,但代码复杂度高得多
epoll 有惊群"没有"!需 EPOLLEXCLUSIVE 或 SO_REUSEPORT

5.2 答题加分项(提到这些会让面试官眼前一亮)

  • 提到 ep_poll_callback 这个内核函数名
  • 提到 __FD_SETSIZE 是编译期常量
  • 提到 SO_REUSEPORT 比 mutex 快 30%(带数字)
  • 提到 io_uring 这个未来趋势
  • 提到 epoll 跨平台抽象(ae.c / ngx_event / NioEventLoop)
  • 提到 LT/ET 模式的工业选择(Redis LT、Nginx ET)
  • 提到内核数据结构(红黑树 + 就绪链表 + 等待队列)

5.3 答题禁忌

  • ❌ "我背过书的"——面试官想看你真正理解
  • ❌ 用模糊词:"好像"、"应该"、"大概"——要么知道,要么不知道
  • ❌ 长篇背诵——结论先行,30 秒内点到要害
  • ❌ 答非所问——审题,不要把准备好的答案硬套上去

6. 模拟一场完整面试(连环追问)

面试官:聊聊 I/O 多路复用吧。

:好的。多路复用是让一个线程同时监听多个文件描述符的机制,避免传统"一连接一线程"的资源爆炸。Linux 上有 select、poll、epoll 三种实现。

面试官:它们差别在哪?

:select 用 1024 大小的位图,poll 改成无上限的结构体数组,epoll 用红黑树 + 就绪链表把性能从 O(n) 干到了 O(就绪数)。前两者每次都要把 fd 集合从用户态拷到内核态,epoll 通过 epoll_ctl 一次注册就长期复用。

面试官:为什么 epoll 是 O(就绪数)?

:因为内核在每个被监听 fd 的等待队列里注册了 ep_poll_callback——数据来时回调主动把 fd 放进就绪链表。epoll_wait 直接从就绪链表取,不需要遍历所有监听的 fd。

面试官:LT 和 ET 怎么选?

:LT 是默认的水平触发,有数据就反复通知,简单不易错——Redis 用 LT。ET 只在状态变化时通知一次,性能高 30% 左右但必须配非阻塞 socket 且循环读到 EAGAIN——Nginx 用 ET。新手入门建议 LT,框架级追求性能用 ET。

面试官:多个 worker 都监听 listen_fd 会有什么问题?

:会有惊群——一个连接来时所有 worker 都被唤醒但只有一个能 accept 成功。解决方案有 EPOLLEXCLUSIVE(4.5+)和 SO_REUSEPORT(3.9+),后者更优:每个 worker 有独立 listen 队列,内核按 5 元组哈希做负载均衡。

面试官:还有什么想补充的?

:未来 io_uring 可能改变格局——它是真异步的,通过共享环形队列把 syscall 降到极低。但当前业务代码 90% 还是 epoll,特别是 Linux 内核版本不够新的情况。

结果:直接 offer ✨


7. 学习路径回顾

text
本系列学习闭环
─────────────────────────────
第 1 章 → 打地基: 用户态/内核态、fd
第 2 章 → 看全局: 五大 I/O 模型
第 3-5 章 → 啃硬骨头: select/poll/epoll 三剑客
第 6 章 → 横向对比: 终极决策表
第 7 章 → 看真实世界: Redis/Nginx/Netty
第 8 章 → 输出能力: 35 道面试题(本章)

→ 至此,你已经从"听过这词"晋升到"能设计 100 万并发服务器"

总结:I/O 多路复用是高并发服务器的必修课——它的演进史(select → poll → epoll → io_uring)就是一部 Unix 网络编程的小型进化史。掌握这块知识点,你能:

  • 读懂 Nginx / Redis / Netty 这些开源项目的核心
  • 设计 C10M 级别的服务器架构
  • 应对 后端面试中的网络/系统类问题
  • 理解 异步编程模型的底层原理(Node.js 的事件循环、Go 的 GMP 调度都和它有关)

建议:把本系列的代码每个都跑一遍,画出 epoll 的内核数据结构关系图,能把 35 道面试题流畅复述——这块知识就吃透了。