网络编程:IO模型与高并发
核心要点速览
- 五大 IO 模型:阻塞 IO(低并发)、非阻塞 IO(忙等)、IO 多路复用(高并发核心)、信号驱动 IO(极少用)、异步 IO(理想模型)
- IO 多路复用:select(位图,FD 上限 1024)、poll(动态数组,轮询)、epoll(Linux 首选,O (1) 事件驱动)
- 同步 vs 异步:同步需等待 IO 就绪 / 完成(阻塞 / 非阻塞 / IO 多路复用),异步无需等待(内核回调通知)
- 高并发模型:Reactor(事件驱动)、多线程 Reactor(主线程 epoll + 子线程池处理任务)
- 核心选择:百万级并发选「epoll + 线程池 + ET 模式」,中高并发选「epoll/poll + 有限线程」,低并发选「BIO + 线程池」
一、同步 IO 与异步 IO
核心定义
- 同步 IO:线程发起 IO 请求后,必须等待 IO 操作(就绪或数据拷贝)完成才能继续执行,线程主动参与等待过程。
- 典型:阻塞 IO、非阻塞 IO、IO 多路复用(select/poll/epoll)。
- 异步 IO:线程发起 IO 请求后,无需等待,可直接执行其他任务;内核完成 “数据拷贝” 后,通过回调 / 信号通知线程处理结果。
- 典型:POSIX AIO、Windows IOCP。
关键差异
- 同步 IO 的核心是 “线程等待 IO 就绪 / 完成”,即使非阻塞 IO 的轮询,线程仍在主动消耗 CPU;
- 异步 IO 的核心是 “线程不参与 IO 等待和数据拷贝”,完全由内核处理,效率最高但兼容性差。
- 易错点:IO 多路复用是同步 IO(需线程主动读取就绪数据,并非内核自动推送)。
二、五大 IO 模型详解
IO 操作核心流程:「发起 IO 请求→等待 IO 就绪→数据拷贝」,模型差异集中在 “等待就绪” 和 “数据拷贝” 的实现方式。
1. 阻塞 IO(BIO):最简单的低并发模型
- 原理:线程发起 IO 请求(如
recv/accept)后,内核阻塞线程,直到数据拷贝完成(从网卡→用户缓冲区)才唤醒线程。 - 流程:发起请求→内核阻塞线程→数据拷贝完成→线程唤醒返回。
- 优点:实现简单,无需处理非阻塞逻辑,开发成本低。
- 缺点:1 线程只能处理 1 连接,并发能力极差(线程阻塞期间无法做其他事)。
- 适用场景:连接数少(千级以下)、逻辑简单的场景(如本地工具、小规模服务)。
2. 非阻塞 IO(NIO):轮询式低效模型
- 原理:通过
fcntl设置 Socket 为非阻塞,线程发起 IO 请求后,内核立即返回(无论数据是否就绪);数据未就绪时返回EAGAIN,线程需主动轮询重试。 - 流程:发起请求→数据未就绪→返回
EAGAIN→轮询重试→数据就绪→数据拷贝→返回结果。 - 优点:线程不阻塞,理论可处理多个连接。
- 缺点:轮询导致 CPU 空转,资源利用率极低,极少单独使用。
- 适用场景:配合 IO 多路复用使用(避免单独轮询的 CPU 浪费)。
3. IO 多路复用:高并发核心模型
- 原理:通过 “中间组件”(select/poll/epoll)管理多个 Socket(FD),线程阻塞在中间组件上,而非单个 IO 请求;任一 FD 就绪时,中间组件通知线程处理该 FD 的 IO。
- 流程:注册 FD 到中间组件→线程阻塞在
epoll_wait/select→FD 就绪→中间组件返回就绪 FD→线程处理 IO(recv/send)。 - 核心优势:单线程 / 少量线程处理大量连接(避免线程阻塞在单个 IO),CPU 利用率高。
三种实现对比
| 实现 | 底层结构 | 最大 FD 限制 | 效率 | 核心缺陷 |
|---|---|---|---|---|
| select | 位图(bitmask) | 1024(固定) | O (n)(轮询) | FD 上限低、轮询开销大(高并发卡顿) |
| poll | 动态数组(fdset) | 无(理论) | O (n)(轮询) | 轮询开销大,无 FD 上限但高并发低效 |
| epoll | 红黑树 + 就绪链表 | 无(系统限制) | O (1)(事件驱动) | 仅支持 Linux(平台依赖) |
epoll 核心优化
- 事件驱动而非轮询:内核通过回调机制记录就绪 FD,无需遍历所有注册 FD,效率 O (1)。
- 共享内存:FD 注册信息存储在内核态,避免用户态与内核态频繁数据拷贝。
- 支持两种触发模式:
- 水平触发(LT,默认):FD 缓冲区有数据则持续通知,易用不易漏数据(适合大多数场景)。
- 边缘触发(ET):仅数据 “首次到来” 时通知一次,需一次性读完缓冲区数据,效率更高(适合高并发)。
4. 信号驱动 IO:极少使用的模型
- 原理:线程通过
sigaction注册SIGIO信号回调,发起 IO 请求后不阻塞;IO 就绪时,内核发送信号,线程在回调中处理 IO。 - 优点:无需轮询,CPU 利用率高。
- 缺点:信号处理逻辑复杂(竞态、嵌套),调试困难,实际场景极少使用。
5. 异步 IO:理想但难用的模型
- 原理:线程发起 IO 请求时指定回调函数,立即返回执行其他任务;内核完成 “数据拷贝” 后,调用回调函数通知线程。
- 流程:发起
aio_read(指定回调)→线程执行其他任务→内核完成数据拷贝→触发回调。 - 优点:线程完全不参与 IO 等待和数据拷贝,并发效率最高。
- 缺点:跨平台支持差(Linux AIO 不成熟,Windows IOCP 常用),开发复杂度高。
- 适用场景:Windows 高并发服务(如游戏服务器),Linux 场景下极少用。
三、高并发核心模型:Reactor 模式
1. 核心原理
- 事件驱动架构:通过
epoll监控所有 IO 事件(连接、读、写),事件就绪后分发到对应的处理器(连接处理器、读处理器、写处理器)。 - 核心组件:
- 事件多路分发器:
epoll,负责监控 FD 事件。 - 事件处理器:处理具体事件(如
accept新连接、recv数据、send响应)。 - 事件队列:存储就绪事件,供分发器调度。
- 事件多路分发器:
2. 多线程 Reactor(生产环境首选)
- 架构:主线程负责
epoll事件监控和连接建立(accept),新连接分配给子线程池;子线程处理 IO 读写和业务逻辑。 - 优势:
- 主线程不处理业务,避免 IO 阻塞影响事件监控。
- 线程池复用线程,降低线程创建 / 切换开销。
- 支持百万级并发(
epoll管理 FD + 线程池处理业务)。
四、高并发瓶颈与 IO 模型选择
1. 核心瓶颈
- 线程阻塞:BIO 模型中线程阻塞在 IO,限制并发数。
- 线程切换开销:多线程模型中,大量线程切换消耗 CPU 资源。
- 内核态 / 用户态拷贝:频繁数据拷贝(如 select 的 FD 集合拷贝)降低效率。
2. 分场景 IO 模型选择
| 并发量级 | 推荐模型 | 核心原因 |
|---|---|---|
| 百万级(如 Web/IM) | epoll(ET 模式)+ 多线程 Reactor + 线程池 | epoll O (1) 效率,ET 模式减少通知,线程池处理业务 |
| 万级(中高并发) | epoll/poll + 有限线程 | 平衡资源开销与并发能力,无需复杂架构 |
| 千级以下(低并发) | BIO + 线程池 | 开发简单,无需复杂 IO 管理 |
3. 高并发优化策略
- 采用 epoll ET 模式:减少事件通知次数,需一次性读完缓冲区数据。
- 非阻塞 IO 配合 ET 模式:避免 IO 操作阻塞线程。
- 线程池隔离业务逻辑:主线程仅处理 IO 事件,业务逻辑交给子线程。
- 减少数据拷贝:使用内存池、零拷贝技术(如
sendfile)。
五、问答
1. epoll 的 LT 和 ET 模式有什么区别?
- LT(水平触发):FD 缓冲区有数据则持续通知,直到数据读完;易用,不会漏数据,默认模式。
- ET(边缘触发):仅数据 “首次到来” 时通知一次,需一次性读完缓冲区;效率高,减少通知次数,适合高并发,但需处理非阻塞 IO 避免漏读。
2. 为什么 IO 多路复用是同步 IO 而非异步 IO?
因为 IO 多路复用仅解决 “等待 IO 就绪” 的阻塞问题,线程仍需主动调用recv/send完成数据拷贝(IO 操作的核心步骤);异步 IO 的核心是内核自动完成数据拷贝并通知线程,线程无需参与 IO 操作。
3. epoll 比 select/poll 高效的原因是什么?
- 事件驱动而非轮询:无需遍历所有注册 FD,仅处理就绪事件。
- 共享内存:FD 注册信息存储在内核,避免用户态与内核态频繁拷贝。
- 无 FD 上限:红黑树存储 FD,支持海量连接(仅受系统资源限制)。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 肖恩的博客!
评论

