IO多路复用
·
一、核心一句话
单个线程/进程,同时监听多个IO文件描述符(socket/文件),任何一个IO就绪(可读/可写)就去处理,不用开多线程阻塞等待每一个连接,解决「多连接场景下线程资源爆炸」的问题。
先铺垫基础IO模型对比
- 阻塞IO(BIO)
一个连接必须占一个线程,accept()/read()卡住线程;上万连接就要上万线程,内存、上下文切换开销巨大。 - 非阻塞IO(NIO)
单个线程循环轮询所有socket,挨个询问“你准备好了吗”,大量无效空轮询,CPU空转浪费。 - IO多路复用(select/poll/epoll/kqueue)
把所有socket交给操作系统内核监听,线程只阻塞等待内核通知;哪个socketIO就绪,内核主动告诉用户程序,只处理就绪的连接,无空轮询、线程极少。
二、工作流程(标准四步)
- 用户程序创建多个socket连接,把全部fd(文件描述符)注册到内核监听集合;
- 调用多路复用函数(
select/epoll_wait),线程阻塞,交出CPU给操作系统; - 内核持续监控所有注册fd:
- 若某个socket收到数据、可发送数据、连接断开 → 标记该fd为就绪状态; - 内核唤醒阻塞的用户线程,返回所有就绪fd列表;程序只遍历处理就绪的连接,未就绪的跳过。
三、三种主流实现(Linux)
1. select(早期,有缺陷)
- 限制:最大监听1024个fd,每次调用都要把全量fd从用户态拷贝内核态,内核需要线性遍历所有fd找就绪项;
- 缺点:连接越多性能越差。
2. poll(改良select,无1024上限)
- 去掉1024文件描述符上限,但仍存在「用户/内核频繁拷贝、线性遍历fd」性能问题,高并发场景依旧拉胯。
3. epoll(Linux高性能方案,Nginx/Redis/Netty底层)
分三步:
epoll_create:在内核创建一个epoll监听实例;epoll_ctl:增/删/改要监听的socket fd(只拷贝一次fd到内核,常驻);epoll_wait:阻塞等待就绪事件,内核用红黑树+就绪链表,只返回就绪fd,无遍历开销;
两大模式:
- 水平触发LT(默认):数据没读完会持续通知;
- 边缘触发ET:只有新数据到达时通知一次,必须一次性读完缓冲区,性能更高。
四、关键优势
- 线程极少:单线程即可管理成千上万TCP连接,不用一连接一线程;
- 低CPU消耗:无轮询空转,线程阻塞在内核,无IO事件时不占用CPU;
- 减少上下文切换:大量连接复用少量线程,避免海量线程频繁切换;
- 用户态/内核态拷贝优化(epoll):fd只注册一次,不用每次循环拷贝全部连接。
五、通俗类比理解
- BIO:每个客户配一个专属客服,客户不说话客服就原地等着,人多客服成本爆炸;
- 非阻塞NIO:一个客服不停挨个挨个问所有客户“你要办事吗”,大部分客户没需求,客服纯浪费体力;
- IO多路复用(epoll):所有客户全部交给前台(操作系统内核)统一看管,客户有需求前台主动喊客服,客服只处理有需求的客户,闲时休息。
六、和Java NIO的关系
Java NIO底层就是封装操作系统IO多路复用:
- Windows:select
- Linux:epoll
- MacOS:kqueue
Netty、Dubbo、Redis、Nginx高性能中间件/服务器全部基于IO多路复用实现高并发。
七、高频面试考点区分
- BIO:一连接一线程,低并发适用;
- NIO同步非阻塞:基于IO多路复用,单线程管理多连接,高并发网络IO首选;
- AIO异步IO:内核完成全部读写后回调程序,文件IO优势大,网络场景很少用;
- IO多路复用不是异步IO,本质仍是同步IO:用户线程收到通知后,仍需要自己调用read/write完成数据读写。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)