孤舟笔记 IO 与网络编程篇四 IO多路复用到底是什么?select/poll/epoll一篇搞懂
文章目录一、先说结论IO 多路复用核心事实二、为什么需要多路复用三、select最早的多路复用四、pollselect 的改进版五、epoll终极方案六、三种实现对比IO 多路复用 全景回答技巧与点评标准回答加分回答面试官点评个人网站面试官问谈谈你对 IO 多路复用的理解很多人只能说出一个线程处理多个连接但追问select 和 epoll 有什么区别、“epoll 为什么快”、“Java NIO 底层用的哪个”就答不上了。IO 多路复用是高并发网络编程的基石理解它才能理解 Netty、Redis、Nginx 的设计。今天咱们把 IO 多路复用从原理到演进彻底讲透。一、先说结论IO 多路复用核心事实维度说明是什么一个线程同时监听多个 IO 事件哪个就绪处理哪个解决什么避免一连接一线程的资源浪费三种实现select → poll → epoll越来越强核心思想把轮询交给内核用户线程只处理就绪事件一句话记住IO 多路复用像餐厅叫号器——不用每个顾客配一个服务员叫号器通知哪个桌准备好了服务员再去处理。二、为什么需要多路复用没有多路复用——一连接一线程// 1000 个连接 1000 个线程while(true){Socketclientserver.accept();newThread(()-{client.getInputStream().read();// 阻塞等待 }).start();}问题99% 的连接大部分时间都在等数据线程白白占用内存和 CPU。非阻塞轮询——忙等// 非阻塞模式 轮询channel.configureBlocking(false);while(true){intnchannel.read(buf);if(n0){/* 处理 */}// 没数据就继续循环 → CPU 空转}问题CPU 100% 空转比阻塞还惨。多路复用——把轮询交给内核SelectorselectorSelector.open();channel.register(selector,SelectionKey.OP_READ);while(true){selector.select();// 内核告诉你哪些 Channel 有数据 // 只处理就绪的 Channel}内核帮你轮询用户线程只处理有数据的连接——这就是多路复用的核心思想。三、select最早的多路复用原理用户把所有 fd文件描述符传给内核内核遍历检查是否有事件返回就绪的 fd 数量。// select 系统调用intselect(intnfds,fd_set*readfds,fd_set*writefds,fd_set*exceptfds,structtimeval*timeout);工作流程1. 用户态构建 fd_set所有监听的 fd 集合 2. 内核态遍历所有 fd检查是否有事件 O(n) 3. 内核态返回就绪 fd 数量修改 fd_set 4. 用户态遍历 fd_set 找出就绪的 fd O(n)三大问题问题说明fd 数量限制默认最大 1024FD_SETSIZE两次遍历 O(n)内核遍历 用户遍历fd 多时慢每次调用要重传fd_set 每次都要从用户态拷贝到内核态生活类比select 像点名——每次把全班名单念一遍谁举手了记下来下次还得重新念名单。四、pollselect 的改进版原理和 select 类似但用动态数组替代固定大小的 fd_set。intpoll(structpollfd*fds,nfds_tnfds,inttimeout);structpollfd{intfd;// 文件描述符shortevents;// 监听的事件shortrevents;// 返回的事件 区分了输入和输出};改进selectpollfd_set 固定 1024动态数组无数量限制输入输出混用 fd_set输入(events)输出(revents)分离但核心问题没解决仍然是 O(n) 遍历每次调用仍要拷贝。五、epoll终极方案epoll 彻底重新设计解决了 select/poll 的所有问题。三个系统调用intepoll_create(intsize);// 创建 epoll 实例intepoll_ctl(intepfd,...);// 注册/修改/删除 fd 只需注册一次intepoll_wait(intepfd,...);// 等待就绪事件核心改进一红黑树 事件驱动select/poll每次传入所有 fd内核遍历检查 → O(n) epollfd 注册到红黑树有事件时内核自动回调通知 → O(1) 注册 核心改进二就绪链表select/poll返回所有 fd 的状态用户遍历找就绪的 → O(n) epoll只返回就绪的 fd 列表 → O(k)k 是就绪数量 核心改进三一次注册多次使用select/poll每次调用都要传入全部 fd → 重复拷贝 epollepoll_ctl 注册一次epoll_wait 只等通知 → 无需重复拷贝 两种触发模式模式行为代表水平触发LT缓冲区有数据就通知Java NIO、默认 epoll边缘触发ET缓冲区状态变化才通知Nginx、Redis边缘触发更高效但更复杂——必须一次性读完缓冲区否则下次不会再通知。六、三种实现对比维度selectpollepoll最大连接数1024无限制无限制内核遍历O(n)O(n)O(1)通知用户遍历O(n)O(n)O(k)就绪数fd 拷贝每次全量每次全量一次注册触发模式LTLTLT ET适用规模几十几百几万~几百万Java NIO 在 Linux 上默认使用 epoll在 macOS 上使用 kqueue在 Windows 上使用 IOCP。// Java NIO 的 Selector 底层自动选择// Linux → EPollSelectorProvider// macOS → KQueueSelectorProvider// Windows → WindowsSelectorProviderIO 多路复用 全景IO 多路复用 全景 核心思想 ├── 一个线程监听多个 IO 事件 ├── 把轮询交给内核 └── 只处理就绪的连接 演进路径 ├── select ── 固定1024、O(n)遍历、每次全量拷贝 ├── poll ── 无限制、O(n)遍历、每次全量拷贝 └── epoll ── 红黑树注册、O(k)返回、一次注册 epoll 的三大改进 ├── 红黑树 ── 注册 O(log n)不用每次重传 ├── 就绪链表 ── 只返回就绪 fdO(k) └── 回调机制 ── 事件驱动不遍历 触发模式 ├── 水平触发(LT) ── 有数据就通知Java NIO 默认 └── 边缘触发(ET) ── 状态变化才通知Nginx/Redis Java NIO 底层 ├── Linux → epoll ├── macOS → kqueue └── Windows → IOCP 口诀select 限制一零二四poll 解限仍遍历 epoll 红黑树加回调一次注册多次用 就绪链表只返回 k水平边缘两触发 Java NIO 自动选Linux epoll 是王者。回答技巧与点评标准回答IO 多路复用是一个线程同时监听多个 IO 事件只处理就绪的连接避免一连接一线程的资源浪费。实现方式有三种select 有 1024 连接限制且每次 O(n) 全量遍历poll 取消了数量限制但仍然是 O(n) 遍历epoll 用红黑树注册 fd、回调机制通知就绪、只返回就绪列表 O(k)是最高效的实现。epoll 还支持边缘触发模式性能更高但编程更复杂。Java NIO 的 Selector 底层在 Linux 上使用 epollmacOS 上使用 kqueue。加分回答epoll 的惊群问题当多个线程/进程同时 epoll_wait 同一个 fd 时一个事件到来可能唤醒所有等待者但只有一个能处理——这就是惊群thundering herd。Nginx 通过 accept_mutex 解决惊群Linux 4.5 引入了 EPOLLEXCLUSIVE 标志从内核层面解决Reactor 模式和多路复用的关系IO 多路复用是机制Reactor 是基于它的模式。单 Reactor 单线程Redis、单 Reactor 多线程、主从 Reactor 多线程Netty是三种常见的 Reactor 模式。Netty 的 boss group 是主 Reactor负责 acceptworker group 是从 Reactor负责 IO 读写io_uringLinux IO 的未来Linux 5.1 引入了 io_uring是比 epoll 更先进的 IO 框架——基于共享环形缓冲区用户态和内核态通过环形缓冲区通信几乎零系统调用。io_uring 不仅支持网络 IO还支持文件 IO是 Linux IO 的统一解决方案面试官点评这道题考的是你对高并发 IO 底层机制的理解。能说出select/poll/epoll、epoll 最快是基本要求能讲清楚 epoll 的三大改进红黑树、就绪链表、回调机制、为什么比 select 快才算及格。如果你能提到 LT/ET 触发模式的区别、Java NIO 底层实现的选择、或 io_uring 等前沿技术面试官会认为你对 IO 多路复用的理解已经深入到了操作系统内核层面。原文阅读内容有帮助点赞、收藏、关注三连评论区等你