当前位置: 首页 > news >正文

如何通俗的理解操作系统的IO多路复用

下面用“一个服务员照看很多桌子”的比喻起步,然后迅速落到技术细节。

一句话直观印象

传统阻塞 I/O = “一桌配一服务员”。
I/O 多路复用 = “一个服务员盯着大厅里所有桌子的呼叫灯(就绪事件),哪桌亮灯就先去服务”。

为什么需要

  • 连接很多、单个连接大多时间在等数据。

  • 省线程:避免“一连接一线程”的内存占用与上下文切换。

  • 统一调度:一个事件循环就能管理成千上万的 socket/文件描述符(fd)。

它在内核里做了什么(本质)

  1. 进程把一批 fd 注册给内核,请求“当其中任意一个就绪时叫我一声”。

  2. 内核把这些 fd 加入监视结构(如 epoll 的红黑树 + 就绪链表)。

  3. 当驱动/协议栈把数据推到缓冲区,内核把对应 fd 标记为可读/可写,并放到就绪队列。

  4. 用户态调用 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主循环只做分发,重活丢到旁路

http://www.dtcms.com/a/355184.html

相关文章:

  • H5 本地跨域设置
  • “帕萨特B5钳盘式制动器结构设计三维PROE模型7张CAD图纸PDF图“
  • UE5.5模型导入FBX强制x轴向前Force Front XAxis
  • 上线问题——Mac系统下如何获取鸿蒙APP证书公钥和MD5指纹
  • 密码管理中
  • 多线程 【详解】| Java 学习日志 | 第 14 天
  • Ansys Icepak AEDT 中的后处理脚本
  • 护网面经总结(三)
  • 三维细节呈现核心技术:法线、凹凸与置换贴图全解析与应用指南
  • 物业满意度调查数据分析——从 “数据杂乱” 到 “精准改进” 的落地经验(满意度调查问卷)
  • Linux系统资源分配算法在VPS云服务器调优-性能优化全指南
  • ​突破RAG知识库中的PDF解析瓶颈:从文本错乱到多模态处理的架构跃迁​
  • 【C++成长之旅】C++入门基础:从 Hello World 到命名空间与函数重载的系统学习
  • NV002NV003美光固态闪存NV026NV030
  • 数组替代map实现性能优化
  • Multimodal Transformer Training in Personalized Federated Learning
  • 配送算法17 AFramework for Multi-stage Bonus Allocation in meal delivery Platform
  • 替换数字(字符串算法)
  • 宋红康 JVM 笔记 Day08|堆
  • SMTPman,smtp协议是什么协议的核心功能!
  • 大数据毕业设计选题推荐-基于大数据的存量房网上签约月统计信息可视化分析系统-Hadoop-Spark-数据可视化-BigData
  • MySQL 8.0 事务深度解析:从核心特性到实战应用
  • 国产化Excel开发组件Spire.XLS教程:Python 将 CSV 转换为 Excel(.XLSX)
  • 【重磅发布】flutter_chen_updater-版本升级更新
  • 【开题答辩全过程】以 汽车售后管理系统的设计与实现为例,包含答辩的问题和答案
  • 首次创建Django项目初始化
  • Spring Boot 启动优化实战指南:从原理到落地的全链路性能调优
  • 我的6年!
  • Vue 组件循环 简单应用及使用要点
  • 算法加训 动态规划熟悉30题 ---下