推广 热搜: 采购方式  滤芯  带式称重给煤机  甲带  气动隔膜泵  减速机型号  无级变速机  链式给煤机  履带  减速机 

多路复用技术 的研究报告

   日期:2026-06-14 18:31:14     来源:网络整理    作者:本站编辑    评论:0    
多路复用技术 的研究报告

说实话, 我在很多地方都听过多路复用 ,但是始终没时间去研究他们有啥区别,以及底层原理, 今天咱们就来研究一下

  • I/O 多路复用(select/poll/epoll)

  • Redis 单线程 + 多路复用

  • Java NIO 的 Selector

  • 网络数据包的多路复用/解复用(端口、协议号)

这些“多路复用”技术原理不完全一样,但核心思想完全一致。

核心思想:用“一个东西”同时管理“多个通道/任务”,避免为每个任务都分配独立资源。

下面我帮你把这几个概念彻底分清。

我们直接从 Selector 底层实现原理 出发,以 epoll 为例,讲清楚“多路复用”的本质。

首先咱们了解一下 epoll

epoll 是 Linux 内核提供的一种 I/O 多路复用机制,它的核心工作是帮你同时监控大量的文件描述符(比如网络 socket)。

Epoll 解决什么问题?

你要写一个服务器,同时处理 10000 个客户端连接。

  • 老方法(select/poll):每次都要把 10000 个 fd 拷给内核,内核遍历一遍,再把结果拷回来 → 每次 O(N)

  • epoll:只在第一次告诉内核“帮我盯着这些 fd”,后面内核只告诉你“哪些 fd 有事件了” → 每次 O(1)

select/poll 做了什么

每次调用 select 或 poll,内核做的动作:

1. 把用户传进来的 fd 集合从用户态拷贝到内核态2. 遍历所有 fd(10000 个),逐个检查是否有事件3. 把有事件的 fd 标记出来4. 把结果拷回用户态

核心:每次调用都要完整遍历一遍全部 fd,开销是 O(N)。

epoll 把“遍历”提前到了事件发生时

epoll 在内核里维护了两个核心结构:

数据结构作用
红黑树存储所有被监控的 fd
就绪链表存储已经发生事件的 fd

事件发生时,内核主动做了这件事

当某个 socket 上有数据到达(硬件中断 → 网络协议栈 → 唤醒等待的进程):

内核在把数据交给协议栈处理的同时:  1. 找到这个 socket 对应的 fd  2. 如果这个 fd 在 epoll 的红黑树里  3. 就把这个 fd 添加到「就绪链表」中

我们从 OSI 七层模型 和 Linux 内核网络栈 两个角度,把“数据到达 → epoll 就绪”的完整路径画出来。

OSI 七层视角(简化版)

OSI 层动作
物理层网卡接收电信号/光信号,转为比特
数据链路层网卡驱动解析以太网帧,去掉 MAC 头
网络层IP 层处理,去掉 IP 头,确定是 TCP 还是 UDP
传输层TCP 层处理,找到对应的 socket
会话层以上数据 ready,等待用户进程读取

关键点:在第 4 层(TCP)处理完后,内核就知道这个数据属于哪个 socket fd。

Linux 内核网络栈的详细路径(从硬件到 epoll)

四步核心

  1. 硬件 + 驱动:DMA 把数据放到内存,发中断

  2. 软中断 + 网络栈:IP → TCP → socket → 接收队列

  3. 回调:sk_data_ready → ep_poll_callback → fd 入就绪链表 + 唤醒

  4. 用户进程:epoll_wait 返回 → recv 读数据

关键数据结构对应表

图上组件内核数据结构 / 函数作用
Ring Bufferrx_ring网卡驱动与内核共享的环形队列
软中断NET_RX_SOFTIRQ网络收包软中断
IP 层netif_receive_skbIP 层入口
TCP 层tcp_v4_rcvTCP 层入口
socketstruct sock代表一个网络连接
接收队列sk->sk_receive_queue存放已收到未读的 sk_buff
数据就绪回调sk->sk_data_ready数据可用时调用的函数指针
epoll 回调ep_poll_callback被 sk_data_ready 调用
epoll 就绪链表ep->rdllist存放有事件的 fd
epoll 等待队列ep->wq睡眠在 epoll_wait 上的进程
在补充一下 前边的流程

Java NIO 的 Selector 底层在 Linux 上就是 epoll。
Selector.open() 对应 epoll_create,在内核中创建红黑树、就绪链表和等待队列。
channel.register() 对应 epoll_ctl,把 socket fd 加入红黑树。
selector.select() 对应 epoll_wait,检查就绪链表;为空时当前线程加入等待队列,进入睡眠。
当数据到达时,网卡通过中断 → 网络栈 → sk_data_ready 最终触发 ep_poll_callback,把 fd 加入就绪链表,并唤醒等待队列中的线程。
线程从 epoll_wait 返回,通过 selectedKeys 拿到就绪事件。
这就是 Java NIO 能够单线程处理海量连接的核心原理。”

有个问题, EventLoop里边是怎么操作 这个Selector的, 就WebCLient来说 里边的HttpClient里边的连接池 ConnectionProvider是把 TCP连接 创建好了 之后, EventLoop都用Selector完成注册, 之后假如说 Socket没收到数据 这个EventLoop 线程是 挂起吗? 还是去处理别的东西

场景:WebClient 发起请求,等待响应

步骤 1:连接池创建 TCP 连接

  • ConnectionProvider 创建 SocketChannel,此时还没有进入 EventLoop。

  • 连接建立成功。

步骤 2:把连接注册到 EventLoop 的 Selector

SocketChannel channel = ...;channel.configureBlocking(false);channel.register(selector, SelectionKey.OP_READ);
  • JVM 状态:RUNNABLE(执行 register 代码)

  • OS 状态:运行

步骤 3:请求已发出,等待响应(Socket 没有数据)

这是你关心的核心时刻。

EventLoop 线程执行到:

int ready = selector.select();  // 没有就绪事件时
层级状态说明
JVM 状态RUNNABLEJVM 不把 select() 阻塞归类为 WAITING 或 BLOCKED
OS 状态阻塞epoll_wait 系统调用让线程睡眠,不占用 CPU

关键:此时 EventLoop 在 OS 层是阻塞的,因此 不占用 CPU,但 JVM 看起来还是 RUNNABLE。

步骤 4:EventLoop 会去“处理别的东西”吗?

答案:不会。
因为没有事件(没有数据、没有新连接、没有超时),select() 不会返回。
EventLoop 线程在 OS 层彻底阻塞,CPU 会去执行其他可运行线程(比如另一个 EventLoop,或其他普通线程)。

所以:

  • 线程不会去执行其他 I/O 操作

  • 线程不会去处理其他 Channel

  • 线程不会去执行任务队列里的普通任务

它就是在 OS 层“睡着”,直到被唤醒。

三、那“去处理别的东西”指什么?

你之前可能听到过“EventLoop 会去处理别的东西”,那是有就绪事件的情况,而不是没事件的情况。

场景JVM 状态OS 状态会处理别的东西吗
没有就绪事件RUNNABLE阻塞❌ 不会,线程在睡觉
有就绪事件RUNNABLE运行✅ 会,只处理就绪的 Channel
任务队列有任务RUNNABLE运行✅ 会,执行普通任务

误区澄清:EventLoop 不会在没事件时“主动去处理别的 Channel”,因为 select() 还没返回。

四、数据到达时会发生什么?

  1. 网卡收到数据 → 内核回调 → ep_poll_callback → fd 加入就绪链表

  2. 内核唤醒阻塞在 epoll_wait 上的线程(OS 阻塞 → 就绪)

  3. select() 返回 → 线程从 OS 阻塞变为运行

  4. JVM 状态仍然是 RUNNABLE(没有变化)

  5. EventLoop 开始处理就绪事件(读数据、执行回调)

WebClient 的 EventLoop 在等待响应(Socket 没有数据)时:

  • JVM 状态:RUNNABLE(JVM 不区分网络 I/O 阻塞)

  • OS 状态:阻塞(由 epoll_wait 系统调用引起,线程不占用 CPU)

  • 会不会去处理别的东西?:不会。没有就绪事件时,select() 不会返回,线程在 OS 层睡眠。

  • 什么时候被唤醒?:数据到达、新连接、或超时时间到。

  • 唤醒后:只处理就绪的 Channel,处理完继续 select()。

那实际上 这里Selector的多路复用机制, 最关键的就是 socket收到数据的时候  . sk.date_ready()触发回调, ep_poll. callbakk()将 fd 加入就绪链表 相当于说是 操作系统 从底层支持了这种多路复用的底层原理, 不然你上层怎么设计都没这么高效

 I/O 多路复用的本质。

多路复用之所以能高效工作,是因为操作系统内核从底层提供了事件通知机制,而不是靠上层轮询。

为什么说「不然你上层怎么设计都没用」?

假如内核不支持事件通知,最原始的做法是:

方案 A:完全靠用户态轮询

while (true) {    for (Socket socket : allSockets) {        if (socket.hasData()) {   // 每次都要系统调用            socket.read();        }    }}
  • 每个 socket 都要一次系统调用

  • 10000 个 socket 一次循环就要 10000 次系统调用

  • CPU 会被打满,但大部分时间在做无用功

方案 B:select/poll(内核遍历)

  • 你告诉内核「帮我看这些 fd」

  • 内核每次全量遍历,再把结果给你

  • 10000 个 fd 每次都要内核遍历一遍

问题:无论哪种,都避免不了「每次都要主动问内核一遍」。

epoll 的底层支持改变了什么?

epoll 不是让你「问内核」,而是让内核 主动通知你。

内核主动通知的前提:内核在网络栈里嵌入了回调点。

当数据到达时,协议栈在把数据交给 socket 的过程中,会调用 sk_data_ready 函数指针。
如果这个指针指向了 ep_poll_callback,epoll 就能得到通知。

// socket 结构里有一个函数指针struct sock {    void (*sk_data_ready)(struct sock *sk);};// 当数据到达时sk->sk_data_ready(sk);   // 调用这个函数

如果没有 epoll,这个函数指针可能指向默认处理(比如唤醒等待 recv 的进程)。
有了 epoll,就可以把指针指向 ep_poll_callback,让 epoll 先得到通知。

上层设计能否绕过这个底层机制?

方案是否依赖内核事件通知能否高效
纯用户态轮询❌ 不需要内核支持❌ 极差
select/poll❌ 只需要遍历 fd 集合❌ O(N) 差
epoll✅ 依赖回调 + 就绪链表✅ O(1) 好

结论:没有内核的事件通知机制,上层无论如何设计都无法做到 O(1) 高效。

这也是为什么 Java NIO 在不同操作系统实现不同

操作系统Java NIO Selector 底层原因
LinuxepollLinux 提供 epoll 内核支持
macOS / BSDkqueueBSD 提供 kqueue 内核支持
Windowsselect(旧) / IOCP(NIO.2)Windows 没有 epoll,用 IOCP 做异步 I/O

关键:Java NIO 只是对底层操作系统机制的一层封装。

  • Linux 上 Selector.open() → EPollSelectorProvider → epoll_create

  • macOS 上 Selector.open() → KQueueSelectorProvider → kqueue

如果你在 Linux 上运行 Java NIO,底层就是 epoll;在 macOS 上,底层就是 kqueue。
上层 Java 代码完全一样,但内核机制不同。

可以这样总结(面试高分)

「I/O 多路复用的核心是操作系统内核从底层支持事件通知机制,而不是靠上层轮询。
epoll 之所以高效,是因为它在网络栈中嵌入了回调,当数据到达时,内核主动把就绪的 fd 加入就绪链表,并唤醒等待的进程。
上层框架(Netty、Java NIO)只是对这些内核机制的封装。
没有内核的底层支持,无论上层怎么设计都不可能做到 O(1) 的高效事件通知。」

操作系统底层的事件通知能力(epoll/kqueue),才是多路复用高效的根本原因。上层一切框架都是站在这个基础之上的封装。

我说实话这个事件通知机制 跟观察者模式 好像啊

epoll 的事件通知机制,从设计思想上讲,就是典型的“观察者模式”在内核态的实现。

我们把这个对应关系彻底拆开,你就会发现,很多底层技术思想和你日常用的设计模式是相通的。


一、观察者模式 vs epoll 事件通知

观察者模式(OOP)epoll 机制(内核)说明
主题(Subject)被监控的 socket事件来源
观察者(Observer)epoll 实例(红黑树 + 就绪链表)负责收集和通知事件
注册(register)epoll_ctl(ADD, fd)把 socket 加入 epoll 的红黑树
事件触发(notify)数据到达 → sk_data_ready → ep_poll_callback内核主动通知,而不是轮询
获取事件(get event)epoll_wait 返回就绪链表观察者拉取通知

现在可以回答一个很难的问题

“epoll 和观察者模式有什么关系?”

答案:

「epoll 是观察者模式在内核态的实现。socket 是被观察者,epoll 实例是观察者。通过 epoll_ctl 注册 socket,当 socket 有事件时,内核主动调用回调函数 ep_poll_callback,把就绪的 fd 加入就绪链表,然后通知阻塞在 epoll_wait 上的线程。这和用户态观察者模式的思想完全一致,只是运行在内核态。」


四、为什么这个理解很重要?

维度意义
打通知识体系设计模式和操作系统底层不是割裂的
面试亮点能说出「epoll 是观察者模式的内核实现」会让面试官刮目相看
架构设计理解事件驱动系统的本质
  1. 设计模式层面:观察者模式

  2. 操作系统层面:epoll 的回调 + 就绪链表

  3. Java 框架层面:NIO Selector / Netty EventLoop

  4. 底层依赖:内核必须支持事件通知,否则上层无法高效

有一个问题, 为啥叫做 多路复用啊

“多路复用”这个词来自通信领域,后来被借用到计算机网络和操作系统里。

结论:

“多路” = 多个通道/连接(很多个 Socket)
“复用” = 用一个东西(一个线程 / 一个 Selector)来处理它们
合起来就是:用单个线程(或单个 Selector)来处理多个网络连接。

从“通信工程”的原意理解

在通信领域(比如电话系统):

  • 多路:很多人同时打电话

  • 复用:把这些通话合并到同一条物理线路上传输

目的:一条物理线路上同时传输多个通话,而不是每个通话拉一条线。

在 I/O 多路复用里是怎么对应的

通信工程I/O 多路复用(select / epoll)
多个通话多个 Socket 连接
一条物理线路一个 EventLoop / 一个线程
复用器(交换机)Selector / epoll

一句话:用一条“逻辑管道”(一个线程 + Selector)来处理多个 Socket。

几种“多路复用”的对照

1. I/O 多路复用(select / epoll)

  • 多个 Socket

  • 一个 epoll_wait

  • 哪个 Socket 有数据,就处理哪个

2. 网络协议中的时分复用 / 频分复用

  • 多个数据流

  • 一条物理线路

  • 按时间片或频率分给不同数据流

3. Java NIO 的 Selector

  • 多个 Channel

  • 一个 select() 调用

  • 哪些 Channel 就绪,就处理哪些

4. RDB 多路复用(比如 Redis)

  • 多个客户端连接

  • 一个主线程

  • 用 epoll 把请求分发到不同命令处理

有哪些 IO 操作?

IO 操作就是“输入输出”动作,在计算机里本质是 把数据从内核空间拷贝到用户空间(读)或反过来(写)。

常见的几种 IO 操作:

操作类型例子
网络 IOsocket read() / write()
文件 IOread() / write() 读写硬盘文件
标准输入输出scanf / printf(最终也是系统调用)
管道 / 消息队列pipe / mq_send

在网络编程里,主要说的是 socket 的读写。

I/O 多路复用复用的是什么?

你直觉上觉得应该是“复用 IO 操作”,这是对的,但不是复用“读写”这个动作,而是复用“等待哪个 IO 可以做了”这一层动作。

可以把整个 IO 过程拆成两个阶段:

  1. 等待数据就绪(内核准备好数据)

  2. 实际拷贝数据(从内核到用户空间)

I/O 多路复用复用的是第一阶段——“等待多个 IO 各自就绪”的等待过程。

用更直观的伪代码帮你理解

传统阻塞方式(不复用)

// 线程 1read(sock1);   // 阻塞,只等 sock1// 线程 2read(sock2);   // 阻塞,只等 sock2
  • 每个 socket 需要一个线程去等它 → 没有复用

I/O 多路复用方式(复用一个线程等所有)

// 一个线程select(socks, ...);   // 同时等待多个 socket:哪个就绪就处理哪个

一个等待调用,同时等 1000 个 socket → 复用了“等待”这个动作

为什么这个命名容易搞混?

因为你直觉把“I/O 操作”理解为 read() / write() 本身。

但在 I/O 多路复用的语境下,“I/O 操作”指的是“等待 IO 就绪”这个子动作。

英文 I/O multiplexing 中的 I/O 包含“等待就绪 + 实际传输”,但复用发生的层次在“等待”。

中文如果改成“IO 等待多路复用”也许更好懂,但官方名字已经固定为 I/O 多路复用。

“I/O 多路复用复用的不是 read() / write() 本身,而是等待多个 I/O 就绪的过程。
它用一个 select 或 epoll_wait 取代了 N 个阻塞的 read,从而把多个‘等待’合并成一个。
这就是‘复用’的真正含义。”

I/O 多路复用的底层核心思想,就是“事件通知机制”。

所有主流的多路复用实现(epoll、kqueue、IOCP),本质上都是内核在“某个 fd 就绪”时主动通知用户态,而不是让用户态反复询问。

---------------------------------------------------- 补充

fd = file descriptor(文件描述符)。

fd 就是操作系统给每个打开的文件、socket、管道等资源分配的「整数编号」。

你可以把它理解为:

  • 餐厅给你的一张排队小票(号码牌)

  • 你凭这个小票找到你对应的资源(文件、socket、管道)

常见的 fd 有哪些?

类型fd 值说明
标准输入0stdin
标准输出1stdout
标准错误2stderr
普通文件3, 4, 5…open() 返回的 fd
socket3, 4, 5…socket() 返回的 fd
epoll 实例7, 8…epoll_create() 返回的 fd

注意:fd 不仅可以是文件,也可以是 socket、管道、epoll 实例本身(因为 Linux 一切皆文件)。


咱们来复习一下线程的状态

Java(JVM)线程状态

状态说明进入条件退出条件
NEW线程创建,未启动new Thread()start()
RUNNABLE可运行 / 正在运行start() 之后等待 CPU 或执行完毕
BLOCKED等待锁进入 synchronized 没抢到锁获得锁
WAITING无限等待wait() / join() / park()notify() / unpark()
TIMED_WAITING限时等待wait(timeout) / sleep(timeout)超时 / 通知
TERMINATED结束run() 执行完—

Java JVM中线程的状态 有 线程在被创建出来的时候, 处于 new 初始状态,  执行了start 方法之后, 进入了 运行状态 runnable .  如果 线程调用了wait方法, 就会进入 waiting 等待状态, 而如果调用wait方法同时传入时间, 在等待的基础上增加了超时机制 线程会处于 timed_waiting状态 ,即超时等待状态 , 超时等待结束 ,或者其他线程通知notify 就会返回运行状态runnable .  如果进入了同步代码块 比方说synchronized修饰 的, 如果没有 获取到锁, 线程就会进入阻塞状态

操作系统线程状态

状态说明
就绪(Runnable)线程可运行,等待 CPU 分配时间片
运行(Running)线程正在 CPU 上执行
阻塞(Blocked / Sleeping)线程等待事件(I/O、锁、时间、唤醒信号等),不占用 CPU

JVM 状态与 OS 状态对应关系(面试必考点)

JVM 状态OS 状态说明
NEW不存在对应还没 start(),OS 不知道这个线程
RUNNABLE就绪 或 运行JVM 把“可运行”和“运行中”合并了
BLOCKED阻塞等待锁
WAITING阻塞等待 notify/unpark
TIMED_WAITING阻塞sleep/wait(timeout) 等
TERMINATED不存在对应线程已销毁

关键点:JVM 把 OS 的 就绪 + 运行 合并成了 RUNNABLE;把多种等待(锁、I/O、时间)都映射到 OS 的 阻塞。

 
打赏
 
更多>同类资讯
0 条相关评论

推荐图文
推荐资讯
点击排行
网站首页  |  关于我们  |  联系方式  |  使用协议  |  版权隐私  |  网站地图  |  排名推广  |  广告服务  |  积分换礼  |  网站留言  |  RSS订阅  |  违规举报  |  皖ICP备20008329号-18
Powered By DESTOON