一、核心一句话

单个线程/进程,同时监听多个IO文件描述符(socket/文件),任何一个IO就绪(可读/可写)就去处理,不用开多线程阻塞等待每一个连接,解决「多连接场景下线程资源爆炸」的问题。

先铺垫基础IO模型对比

  1. 阻塞IO(BIO)
    一个连接必须占一个线程,accept()/read() 卡住线程;上万连接就要上万线程,内存、上下文切换开销巨大。
  2. 非阻塞IO(NIO)
    单个线程循环轮询所有socket,挨个询问“你准备好了吗”,大量无效空轮询,CPU空转浪费。
  3. IO多路复用(select/poll/epoll/kqueue)
    把所有socket交给操作系统内核监听,线程只阻塞等待内核通知;哪个socketIO就绪,内核主动告诉用户程序,只处理就绪的连接,无空轮询、线程极少。

二、工作流程(标准四步)

  1. 用户程序创建多个socket连接,把全部fd(文件描述符)注册到内核监听集合
  2. 调用多路复用函数(select/epoll_wait),线程阻塞,交出CPU给操作系统;
  3. 内核持续监控所有注册fd:
    - 若某个socket收到数据、可发送数据、连接断开 → 标记该fd为就绪状态
  4. 内核唤醒阻塞的用户线程,返回所有就绪fd列表;程序只遍历处理就绪的连接,未就绪的跳过。

三、三种主流实现(Linux)

1. select(早期,有缺陷)

  • 限制:最大监听1024个fd,每次调用都要把全量fd从用户态拷贝内核态,内核需要线性遍历所有fd找就绪项;
  • 缺点:连接越多性能越差。

2. poll(改良select,无1024上限)

  • 去掉1024文件描述符上限,但仍存在「用户/内核频繁拷贝、线性遍历fd」性能问题,高并发场景依旧拉胯。

3. epoll(Linux高性能方案,Nginx/Redis/Netty底层)

分三步:

  1. epoll_create:在内核创建一个epoll监听实例;
  2. epoll_ctl:增/删/改要监听的socket fd(只拷贝一次fd到内核,常驻);
  3. epoll_wait:阻塞等待就绪事件,内核用红黑树+就绪链表,只返回就绪fd,无遍历开销;

两大模式:

  • 水平触发LT(默认):数据没读完会持续通知;
  • 边缘触发ET:只有新数据到达时通知一次,必须一次性读完缓冲区,性能更高。

四、关键优势

  1. 线程极少:单线程即可管理成千上万TCP连接,不用一连接一线程;
  2. 低CPU消耗:无轮询空转,线程阻塞在内核,无IO事件时不占用CPU;
  3. 减少上下文切换:大量连接复用少量线程,避免海量线程频繁切换;
  4. 用户态/内核态拷贝优化(epoll):fd只注册一次,不用每次循环拷贝全部连接。

五、通俗类比理解

  • BIO:每个客户配一个专属客服,客户不说话客服就原地等着,人多客服成本爆炸;
  • 非阻塞NIO:一个客服不停挨个挨个问所有客户“你要办事吗”,大部分客户没需求,客服纯浪费体力;
  • IO多路复用(epoll):所有客户全部交给前台(操作系统内核)统一看管,客户有需求前台主动喊客服,客服只处理有需求的客户,闲时休息。

六、和Java NIO的关系

Java NIO底层就是封装操作系统IO多路复用:

  • Windows:select
  • Linux:epoll
  • MacOS:kqueue

Netty、Dubbo、Redis、Nginx高性能中间件/服务器全部基于IO多路复用实现高并发。

七、高频面试考点区分

  1. BIO:一连接一线程,低并发适用;
  2. NIO同步非阻塞:基于IO多路复用,单线程管理多连接,高并发网络IO首选;
  3. AIO异步IO:内核完成全部读写后回调程序,文件IO优势大,网络场景很少用;
  4. IO多路复用不是异步IO,本质仍是同步IO:用户线程收到通知后,仍需要自己调用read/write完成数据读写。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐