如何通俗的理解操作系统的IO多路复用
下面用“一个服务员照看很多桌子”的比喻起步,然后迅速落到技术细节。
一句话直观印象
传统阻塞 I/O = “一桌配一服务员”。
I/O 多路复用 = “一个服务员盯着大厅里所有桌子的呼叫灯(就绪事件),哪桌亮灯就先去服务”。
为什么需要
连接很多、单个连接大多时间在等数据。
省线程:避免“一连接一线程”的内存占用与上下文切换。
统一调度:一个事件循环就能管理成千上万的 socket/文件描述符(fd)。
它在内核里做了什么(本质)
进程把一批 fd 注册给内核,请求“当其中任意一个就绪时叫我一声”。
内核把这些 fd 加入监视结构(如 epoll 的红黑树 + 就绪链表)。
当驱动/协议栈把数据推到缓冲区,内核把对应 fd 标记为可读/可写,并放到就绪队列。
用户态调用
select/poll/epoll_wait/kqueue
“睡下”,队列非空就被唤醒,拿到就绪 fd 清单,随后尽可能把缓冲区一次性读干/写尽。
常见接口与复杂度(简表)
select
:位图,fd 上限受FD_SETSIZE
,每次 O(N) 扫描。poll
:数组,无上限但仍 O(N) 扫描。epoll
(Linux):就绪列表,回调驱动,平均 O(就绪数)。大规模连接首选。kqueue
(BSD/macOS):与 epoll 类似,语义更广。IOCP(Windows):完成模型(completion),直接投递完成事件。
io_uring(Linux):提交/完成环,更多真正的异步 I/O,减少系统调用开销;可替代部分“多路复用 + 同步 I/O”的用法。
就绪模型 vs 完成模型
就绪(readiness):告诉你“可以读/写了”,还需要你去读/写(epoll、kqueue)。
完成(completion):告诉你“我已经帮你读/写完了”(IOCP、io_uring 的部分操作)。
就绪模型简单易控;完成模型在磁盘 I/O、大量小 I/O 时更省切换。
触发方式(epoll 为例)
水平触发(LT):只要缓冲区里还有数据,每次
epoll_wait
都会再通知。简单但唤醒偏多。边沿触发(ET):状态由“不可读→可读”才触发一次,必须把数据循环读到
EAGAIN
,否则会饿死后续事件,唤醒更少、吞吐更高。
与“多线程 + 阻塞 I/O”的差别
多路复用把“等待”集中到少量线程;CPU 花在处理就绪事件,而不是大量线程的调度切换。
适合大量长连接、低至中等吞吐的网络场景;否则可搭配线程池处理重计算任务。
典型事件循环(Go 伪码,epoll + 非阻塞套接字)
fd := socketNonBlocking()
ep := epollCreate()
epollCtl(ep, EPOLL_CTL_ADD, fd, EPOLLIN|EPOLLET) // 边沿触发更省唤醒buf := make([]byte, 64*1024)
for {nReady, events := epollWait(ep, timeout)for i := 0; i < nReady; i++ {ev := events[i]if ev.Readable() {for {n := read(ev.FD, buf)if n > 0 { handleRead(buf[:n]) }else if n == 0 { close(ev.FD); break } // 对端关闭else if errno == EAGAIN { break } // 数据读干净else { handleError(); break }}}if ev.Writable() {for hasPending() {n := write(ev.FD, nextChunk())if n < 0 && errno == EAGAIN { wantWrite(ev.FD); break }if n < 0 { handleError(); break }}if !hasPending() { disableWriteInterest(ev.FD) }}}
}
要点:
fd 必须非阻塞。
ET 下读写都要循环到 EAGAIN。
写侧按需注册
EPOLLOUT
,避免持续可写导致的忙唤醒。业务耗时操作放到 worker 池,避免堵住事件循环。
常见坑
没有读到 EAGAIN(ET):后续不会再被唤醒,连接“假死”。
把计算/IO 混在主循环:长任务阻塞其它连接。
过度注册可写事件:socket 大多时间都可写,会造成无谓唤醒。
惊群:多进程/线程同时
accept
同一监听 fd;Linux 可用EPOLLEXCLUSIVE
缓解。小包写放大:频繁系统调用;合并写、写缓冲队列、
sendfile
/零拷贝可优化。
何时考虑 io_uring/IOCP
需要真正的异步文件 I/O 或极致系统调用削减。
网络 + 磁盘混合型高并发;或你想把“等待 + 完成”彻底交给内核完成队列处理。
但工程复杂度更高,监控与 backpressure 策略要同步升级。
结论(工程视角)
Linux 大多数高并发网络服务:
epoll(ET) + 非阻塞 socket + 事件循环 + 轻量任务池
是稳妥组合。磁盘/混合 I/O 极致场景:评估 io_uring。
保持两条铁律:非阻塞 + 读写到 EAGAIN;主循环只做分发,重活丢到旁路。