主题
第 8 章 · 多路复用面试题集锦
学完本章你将获得:把前 7 章的知识转化成"面试可用语言"的能力。本章按难度分级,每题给标准答案 + 追问 + 加分项。共 35 道高频题,覆盖 80% 的真实面试场景。
0. 怎么用这一章?
答题三段论模板
不管什么题都用这个结构回答,远超大多数候选人:
- 是什么:用一句话定义概念
- 为什么:解决了什么问题、和什么对比
- 怎么用:举一个例子或场景
答题节奏建议
- 前 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 三者的区别?
标准答案(按维度对比):
- 数据结构:select 用位图(1024 上限);poll 用结构体数组(无上限);epoll 用红黑树 + 就绪链表
- 性能:select/poll O(n);epoll O(就绪 fd 数)
- 拷贝:select/poll 每次都要全量传 fd 集合;epoll 通过 epoll_ctl 一次注册
- 跨平台: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 个核心数据结构:
- 红黑树(RB tree):存所有被监听的 fd——key 是 fd,value 是
struct epitem。增删改查 O(log n)。- 就绪链表(ready list):存已经触发事件的 epitem——双向链表,O(1) 取走
- 等待队列(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 和上下文切换)。
解决方案:
SO_REUSEPORT(Linux 3.9+,推荐):每个 worker 独立 listen_fd,内核按 5 元组哈希做负载均衡EPOLLEXCLUSIVE(Linux 4.5+):内核保证只唤醒一个等待者- 应用层互斥锁(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 后:
- 主线程 epoll_wait → 派给 worker A
- fd 自动从 epoll 摘除
- worker A 处理完 → MOD 把 fd 挂回去
- 下次再有事件 → 派给某个 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 返回的完整路径
标准答案:
- 网卡收到包 → DMA 到内核 ring buffer → 触发软中断
- 内核网络栈 解析 TCP/IP → 数据放入 socket 接收缓冲区
- 唤醒 socket 等待队列 → 遍历队列里所有 entry
- 执行 epoll 注册的 ep_poll_callback → 把 epitem 放进 epoll 就绪链表
- 唤醒 epoll 等待队列 → 在 epoll_wait 上睡眠的进程被唤醒
- 进程被调度上 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 完成队列)实现:
- 用户把 I/O 请求批量写入 SQ
- 内核异步处理(包括读、写、accept、connect 等)
- 完成后写入 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= 永久阻塞;0= 立即返回;正数 = ms 超时- timeout 精度受内核 HZ 限制——典型 4 ms 精度,传 1 ms 实际可能等到 4 ms
- timeout 可能因信号被打断——返回 -1, errno=EINTR,要判断后重试
- 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 还要做哪些优化?
标准答案(清单):
- fd 数:
ulimit -n 65535- TCP 调优:
SOMAXCONN、TCP_DEFER_ACCEPT、TCP_NODELAY、TCP_QUICKACK- 多核利用:SO_REUSEPORT + worker per CPU +
taskset绑核- 零拷贝:sendfile / splice / mmap
- 内存池:避免频繁 malloc/free(slab / arena 分配器)
- 协程:用 Golang goroutine / Rust async 等隐藏异步复杂度
- NUMA 亲和:worker 绑到本地 NUMA 节点
- 关掉无用 syscall:用 vDSO(gettimeofday 等)
Q3.12 实际生产中遇到 epoll 相关 bug,怎么排查?
标准答案(思路):
- 看进程 fd 数:
ls /proc/<pid>/fd | wc -l——是否泄漏?- strace 看 syscall:
strace -p <pid> -e epoll_wait,epoll_ctl,read,write——是否卡在某个 syscall?- perf top:
perf top -p <pid>——CPU 热点在哪?- 检查 EAGAIN 处理:是否在 ET 模式下漏循环?
- dmesg 看内核日志:是否有 OOM、连接拒绝、TCP 包丢失?
- 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 偶尔会"漏掉事件",可能是什么原因?
标准答案(排查清单):
- ET 模式没循环到 EAGAIN ← 最常见
- EPOLLONESHOT 后忘了 MOD ← 多线程场景常见
- fd 已被 close 但还在缓存 ← 用对象指针而非裸 fd
- events 数组太小 ← 调大 maxevents
- EPOLL_CTL_DEL 后又触发 ← 删除前最后一次事件
- 多线程并发操作同一 fd ← 加 ONESHOT 或锁
Q4.3 业务报"客户端连不上",但 netstat 显示服务器还在监听端口,怎么排查?
标准答案(思路):
- 看 accept 队列:
ss -lnt——Recv-Q 是否打满(说明 backlog 溢出)- 看 fd 用尽:
/proc/<pid>/fd——已超 ulimit?- strace 看 accept 是否被调用:业务代码可能卡在某个 syscall 没回到 epoll_wait
- 看防火墙:iptables / 安全组是否新加了规则
- 看连接堆积:
ss -tan | grep CLOSE_WAIT | wc -l——大量 CLOSE_WAIT 说明业务忘了 close- 看 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 服务,有什么特别注意的?
标准答案:
- fd 上限:容器默认继承 host 的 ulimit,但 Docker 默认软上限只 1024 → 用
--ulimit nofile=65535:65535- conntrack 表:iptables 转发会经过 conntrack,默认 65536 → 拉到 200 万(
net.netfilter.nf_conntrack_max)- TIME_WAIT 重用:开
net.ipv4.tcp_tw_reuse- CPU 绑核:K8s 用
cpuManagerPolicy: static+ Guaranteed QoS- NUMA:HugePage + numactl
- 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 道面试题流畅复述——这块知识就吃透了。