• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-07-18 23:58 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

overlapped i/o

概述

  1. Overlapped I/OWindows 平台特有的异步 I/O 模型,是 Windows 高性能网络编程(尤其是 IOCP)的基础

为什么需要它

  1. 同步 I/O(如 Linux 下常见的 recv/send)调用会阻塞线程,直到操作完成
  2. 要支持高并发,传统做法是"一个连接一个线程",但线程数量一多,上下文切换开销就会把系统拖垮

Overlapped I/O 的思路是

  1. 发起 I/O 请求后立即返回,不阻塞线程,操作在后台(内核)异步完成,完成后通过某种机制通知应用程序
    1. 这个"立即返回、后台完成"的语义,就是 "overlapped"(重叠)这个名字的来源
    2. I/O 操作与你的程序执行是重叠(并发)进行的,而不是顺序阻塞

核心数据结构:OVERLAPPED

  1. 每发起一个异步 I/O 操作,都要传入一个 OVERLAPPED 结构体实例,它相当于这次操作的"上下文/句柄"
    1. 操作完成后系统通过它告诉你结果

  1. 实践中通常会把它嵌入到自定义结构体的第一个字段,这样收到完成通知时可以通过指针强转拿到自己的上下文数据

使用 Overlapped I/O 的三种通知方式

事件通知(Event-based

  1. 每个 OVERLAPPED 关联一个事件对象,I/O 完成后事件被置位,通过 WaitForSingleObject/WaitForMultipleObjects 等待

  1. WSA_IO_PENDING
    1. WSA_IO_PENDING 是关键返回值,表示"操作已受理但尚未完成,稍后会通知你",这不是错误,是正常的异步流程
  2. 局限:
    1. WaitForMultipleObjects 最多只能等待 64 个句柄,这种方式无法支撑大规模并发连接,通常只用于少量句柄的简单场景

完成例程(Completion Routine / APC

  1. I/O 完成后,系统在发起调用的线程里排队一个 APC (Asynchronous Procedure Call),线程进入可警醒等待状态时
    1. 如调用 SleepExWaitForSingleObjectExalertable=TRUE就会执行回调函数:

  1. 局限:
    1. 回调只在发起 I/O 的那个线程里执行(APC 是线程关联的),且线程必须主动进入 alertable 状态
    2. 编程模型较别扭,实践中较少用于高并发服务器

IOCP(I/O Completion Port)—— 生产级方案

  1. 这是 Windows 下做高并发服务器的标准方案,本质是一个内核维护的队列
    1. 多个线程可以从同一个 IOCP 句柄上等待"任意一个"已完成的 I/O 操作,无需为每个 socket 关联专属线程

IOCP

IOCP 是什么

  1. IOCP 本质是 Windows 内核提供的一个多生产者-多消费者的任务队列:
    1. 生产者:各种异步 I/O 操作完成后,内核把"完成包"塞进这个队列
    2. 消费者:应用程序开若干个工作线程,反复从队列里取"已完成的包"来处理

解决的核心问题

  1. 如何用少量线程,高效地服务成千上万个并发连接,同时充分利用多核 CPU,且避免线程频繁切换的开销

各种异步 I/O 操作

核心 API

核心概念

  1. 完成端口(Completion Port):
    1. 一个内核对象,socketI/O操作完成后,结果会投递到这个端口
  2. 工作线程池:
    1. 固定数量的线程从完成端口取"完成包"来处理,线程数一般设为 CPU核数 × 2
  3. 重叠结构(OVERLAPPED):
    1. 每次异步I/O操作都要传一个OVERLAPPED结构,用来追踪这次操作

工作流程图

完整工作流程

  1. 第一步:创建 IOCP 对象
    1. 第四个参数 NumberOfConcurrentThreads 是关键调优点

  1. 第二步:创建工作线程池

  1. 第三步:把 socket 关联到 IOCP
    1. CreateIoCompletionPort 这个函数身兼两职
    2. 第一次调用(ExistingCompletionPort=NULL)是"创建"
    3. 之后每次调用(传入已有的 iocp 句柄)都是"关联"一个新句柄

  1. 第四步:投递异步操作

  1. 第五步:工作线程循环取任务

关键设计点深入

  1. NumberOfConcurrentThreads:并发线程数控制
    1. 这是 IOCP 最容易被忽视但很重要的调优参数
    2. 它不是"最多能有多少个线程等在这个IOCP上"(等待的线程数不受限制),而是"同一时刻允许同时处于运行状态、处理完成包的线程数"
    3. 这个机制的目的是避免过度并行导致的上下文切换开销
      线程数超过 CPU 核心数并不会带来性能提升,反而会增加调度开销
      这是 IOCP 内建的"活跃线程数节流阀",是 epoll 模型里没有的、需要应用层自己控制的能力
    4. 设为 0 时系统默认按 CPU 核心数处理

  1. 完成包的处理顺序:LIFO 而非 FIFO
    1. IOCP 唤醒等待线程的顺序是后进先出(LIFO,类似栈),而不是按等待的先后顺序
    2. 这是刻意设计的:
    3. 最近被唤醒过的线程,它的线程栈、TLB、缓存数据更可能还"热"在CPU缓存里,唤醒它比唤醒一个"冷"了很久的线程效率更高(缓存局部性优化)

  1. AcceptEx:预投递接受连接
    1. 标准 accept() 是阻塞调用,IOCP 生态提供 AcceptEx 让接受新连接也纳入异步框架
    2. AcceptEx 还支持"接受连接的同时一并接收第一批数据",减少一次额外的系统调用往返,这是标准 accept()+recv() 组合做不到的优化
    3. 实践中通常会预先投递多个 AcceptEx(比如同时挂 10 个),这样多个新连接同时涌入时不会因为"处理完一个才投递下一个accept"而错过

  1. 内存管理:Context 池化
    1. 生产级 IOCP 服务器几乎都会用内存池来管理 PER_IO_CONTEXTPER_SOCKET_CONTEXT,而不是每次 malloc/free
    2. 原因:高并发场景下频繁 malloc/free 本身就是性能瓶颈(涉及堆锁竞争),且内存碎片化会拖慢长期运行的服务

  1. 缓冲区生命周期陷阱
    1. 这是 IOCP 编程最容易踩坑的地方
    2. 从投递 WSARecv/WSASend 到收到对应的完成通知之间,这段时间的 buffer 内存绝对不能动(不能释放、不能复用给另一个操作)
    3. 因为内核(甚至网卡DMA)可能正在往这块内存写数据,你如果提前释放或复用,会导致:
      内存破坏(另一个操作的数据被写乱)
      Use-after-free 崩溃
    4. 这也是为什么每个"未完成的操作"都要有独立的 context+buffer,不能共享
  2. 连接关闭与优雅退出
    1. IOCP 服务器关闭连接时有一个常见陷阱:
    2. closesocket() 调用后,之前投递的未完成的 overlapped 操作不会立即消失,它们仍然会在稍后产生完成通知(通常带错误码),必须在收到这些"迟到的"完成包后才能真正释放对应的 context 内存,否则会内存泄漏或崩溃
    3. 生产代码通常给每个连接维护一个"未完成操作计数器",归零后才真正释放该连接的所有资源

服务端code

客户端code

AcceptEx相比accept做了什么不同的事

  1. 普通accept()的顺序是
    1. "先有一个握手完成的连接在队列里 → 调用accept → 内核帮你创建新socket并返回",也就是socket的创建是在accept这一刻由内核完成的
  2. AcceptEx的顺序反过来了
    1. 你需要提前用WSASocket()创建好一个空的socket,把这个空socket的句柄传给AcceptEx,让内核在真正有新连接握手完成时,把这个连接"绑定"到你预先准备好的这个socket
  3. 这么设计的好处是:
    1. 可以提前把socket创建的开销和"等待新连接"这个动作解耦,配合IOCP的异步通知机制,能做到"连接一到,几乎立刻可用",减少了同步accept()那种"来一个连接现造一个socket"的实时开销,这也是AcceptEx比普通accept在高并发场景下性能更好的原因之一
  4. 另,AcceptEx完成后,必须调用
    1. 因为AcceptEx创建连接的方式比较特殊,这个新socket需要显式"继承"监听socket的一些属性(比如SO_REUSEADDR这类选项),不然一些依赖监听socket属性的功能可能不会正常生效

为什么iocp性能强

  1. 线程池模型

    1. 少量线程(通常等于 CPU 核心数)就能服务海量并发连接,避免"一连接一线程"的开销
  2. 内核调度

    1. GetQueuedCompletionStatus 会尽量让"最近运行过的线程"优先取到任务(利用 CPU 缓存局部性),并且会限制"同时活跃"的线程数不超过 CPU 核心数(可通过 CreateIoCompletionPort 第四个参数配置并发值),避免过度调度
  3. 无锁队列

    1. 完成包在内核里排队,多线程竞争走内核同步机制,比用户态自己维护队列+锁更高效

区别于epoll

  1. epoll 是"就绪通知"(告诉你fd可读/可写了,你自己再去读写,可能读不满预期字节数)
  2. IOCP 是"完成通知"(告诉你I/O已经做完了,数据已经在缓冲区里,字节数是确定的)

Overlapped I/O 使用中的注意事项

  1. 缓冲区生命周期
    1. 异步操作发起后,在完成通知到达之前,你传给 WSARecv/WSASendbuffer 绝对不能释放或复用
    2. 否则内核可能正在往一块已经被回收的内存写数据,导致内存破坏
    3. 这也是为什么通常要为每个未完成的操作单独 malloc 一块 context+buffer
  2. 一个 socket 可以有多个未完成的 overlapped 操作
    1. 比如同时发起多个 WSARecv(预投递多个接收缓冲区)来提升吞吐量,这是常见的性能优化手段
    2. 预先 post 多个 recv 请求,减少"处理完一个再投递下一个"之间的空档期
  3. 取消操作
    1. CancelIoEx 可以取消尚未完成的 overlapped 操作(比如连接关闭时清理未完成的I/O
    2. 但取消是异步的,取消请求本身也会通过完成端口通知你"这个操作被取消了"(错误码 ERROR_OPERATION_ABORTED),而不是立即生效
  4. AcceptEx / ConnectEx 的预投递
    1. Windows 提供了 AcceptEx(相比标准 accept 效率更高,支持提前投递多个"待接受连接"的 overlapped 请求)
    2. 也支持在 accept 的同时一并接收第一批数据,减少一次系统调用往返

难点

  1. 需要自己管理每个连接的上下文(Per-IO Data / Per-Handle Data

  1. 处理"粘包/半包"问题(TCP流式协议的通病)
    1. 粘包/半包是 TCP 流式协议本身的特性,跟用不用 IOCP 没有直接关系
    2. 但放在 IOCP 语境下确实有个特有的坑:
    3. 因为完成通知里给你的是"这次 WSARecv 完成收到了 N 字节",你必须在应用层自己维护一个按连接的累积缓冲区,把这次收到的数据 append 进去,再尝试从头解析出完整消息,剩余的不完整数据留到下次完成通知再拼接
    4. 而且因为 IOCP 是多线程并发处理完成包的,同一个连接的多次 recv 完成通知,理论上可能被不同的工作线程"乱序"处理
      这会让粘包处理的复杂度进一步增加
      通常需要给每个连接的数据处理加锁或者用序号机制保证处理顺序

  1. 连接的生命周期管理容易出现野指针/内存问题(比如AcceptEx时的引用计数)

    1. 一个连接可能同时有多个未完成的操作(比如同时挂着一个 recv 和一个 send),如果客户端在这时候断线,你不能立即释放这个连接的 context,必须等所有挂起的操作都返回完成通知(哪怕是带错误码的)之后才能真正释放
    2. 常见做法是维护一个"未决操作计数器",每次投递+1,每次完成通知(无论成功失败)-1,归零后才 freePer-Handle Data,通常还需要原子操作
    3. AcceptEx 还有个特有的坑:预先创建的 socket 如果 AcceptEx 迟迟没有新连接到达,而你的服务器要关闭了,这个挂起的 AcceptEx 需要用 CancelIoEx 显式取消,否则会一直挂着阻碍资源回收
  2. Overlapped结构的内存管理要小心,容易内存泄漏

    1. 从投递到完成通知之间 buffer 不能动
    2. 容易内存泄漏的具体场景包括:
    3. 忘记处理 GetQueuedCompletionStatus 返回 FALSElpOverlappedNULL 的情况
      表示这次具体的I/O操作失败了,但完成包本身是有效的,仍然需要释放对应context
    4. 服务器关闭时,还有大量"已投递但未完成"的操作,如果不逐一 CancelIoEx 并等待其完成通知,直接强行退出进程虽然OS会兜底回收,但如果是长时间运行、需要优雅重启的服务,这就是实打实的资源泄漏和潜在崩溃点
  3. 错误码处理的复杂性

    1. WSARecv/WSASend 返回 SOCKET_ERROR 时,必须准确区分 WSA_IO_PENDING(正常,异步处理中)和真正的错误(如 WSAECONNRESET),处理不当会把正常的异步流程误判为错误断开连接
  4. NumberOfConcurrentThreads 和实际线程池大小的调优

    1. 这个参数设置不合理(比如设置过大或过小)会导致 CPU 利用率不理想,需要结合压测调参,没有放之四海而皆准的固定值
  5. 优雅关闭(Graceful Shutdown)的时序问题

    1. 服务器要退出时,"停止接受新连接 -> 等待现有连接处理完 -> 取消所有挂起IO -> 关闭IOCP句柄"这个顺序如果搞错,容易出现工作线程卡死在 GetQueuedCompletionStatus 上退不出去,或者提前释放了还在使用的资源

Reactor 模式

概述

  1. 告诉你 "fd 已就绪,你可以读/写了",实际的 I/O 操作(数据从内核缓冲区拷到用户缓冲区)由应用程序自己发起
  2. 是"就绪通知"

工作流程

code

关键特征

  1. 应用程序主导 I/O 时机:
    1. 内核只负责"通知你能干活了",真正搬运数据的 syscall(read/write)由应用程序自己调用
    2. 且这次调用是同步阻塞在用户态视角下很快返回的
  2. 本质上是同步非阻塞 I/O + I/O多路复用的组合
  3. Linuxepoll、select、poll,以及 macOS/BSDkqueue 都是 Reactor 模式的具体实现

Reactor 单线程

  1. 所有 accept、读、写、业务处理都在一个线程里,简单但无法利用多核
    1. Redis 早期就是这个模型,用单线程规避锁竞争换取简单性

Reactor 多线程

  1. I/O 事件的监听和分发仍在主线程,但实际业务处理丢给线程池处理,避免慢业务逻辑阻塞事件循环

主从 Reactor 多线程

  1. Nginx、Netty 的典型模型
  2. 这样 acceptI/O 处理都能利用多核,是目前高性能服务器最常见的架构

Proactor 模式

概述

  1. 告诉你 "I/O 操作已经完成了",数据已经在你的缓冲区里,实际的 I/O 操作由操作系统内核完成
  2. 是"完成通知"

工作流程

code

关键特征

  1. 内核主导实际的数据搬运
    1. 应用程序只负责"发起请求+提供缓冲区"和"处理已完成的结果",中间的 I/O 执行过程完全交给内核
  2. 是真正意义上的异步 I/O(AIO)
    1. 系统调用发起后立即返回,不需要等到数据就绪再手动去读
  3. Windows IOCP 是最经典的 Proactor 实现
    1. Linuxio_uring 和早期的 POSIX AIO 也属于 Proactor 范畴

为什么 Linux 长期没有真正的 Proactor 模型

  1. LinuxPOSIX AIOaio_read/aio_write)名义上是异步 I/O,但实现上有很多限制:
    1. 早期 POSIX AIO 只能配合 O_DIRECT(绕过 page cache)才能做到真正意义的异步,否则很多实现内部其实是用线程池模拟的(内核偷偷起个线程帮你同步执行,再回调通知你,本质是"伪异步")
  2. 网络 socket 长期不支持 POSIX AIO 的完整异步语义

io_uring 改变了这个局面

  1. Linux 5.12019年)引入的 io_uring 才算真正意义上补齐了这块短板,其设计明显借鉴了 IOCP 的思路:

  1. io_uring 用两个环形缓冲区(SQ/CQ)实现用户态和内核态之间无需系统调用的双向通信
    1. 借助 io_uring_enter 批量提交/收割,甚至配合 SQPOLL 模式可以做到完全无系统调用开销
  2. 这比 IOCP 更进一步
    1. IOCP 每次 GetQueuedCompletionStatus 仍然是一次系统调用,而 io_uring 可以做到用户态直接从共享内存里读完成结果
    2. 可以说:epoll 代表了 Linux 过去二十年 Reactor 模式的巅峰,io_uring 则是 Linux 补上 Proactor 模式这一课,而且反超了 Windows IOCP 的效率

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2026-07-19
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享