全网最详细的 I/O 多路复用解析

article/2025/10/19 12:46:15

前言

IO多路复用目前在大厂的面试中,一般在两个地方可能会被问到,一个是在问到网络这一块的时候,另一个是在问到 Redis 这一块的时候,因为 Redis 底层也是使用了IO多路复用,所以整体来说 IO多路复用,也算是一道比较高频的一个面试题,所以今天跟大家来分享一下。

本文内容有视频版本,喜欢看视频的同学可以直接通过下面的链接观看。如果你对文章的内容有疑惑,可以先看视频的对应内容,视频可能讲的会更细一点。

小白也看得懂的 I/O 多路复用解析(超详细案例)_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1r54y1f7bU?spm_id_from=333.999.0.0

基础概念

首先我们了解下2个基础概念,这2个概念在后续的文章中会反复用到。

Socket

套接字。百科:对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。

例子1:客户端将数据通过网线发送到服务端,客户端发送数据需要一个出口,服务端接收数据需要一个入口,这两个“口子”就是 Socket。

例子2:两个人通过电话进行通信,两个人都需要持有1个电话,socket 就类似于这个电话。

FD:file descriptor

文件描述符,非负整数。“一切皆文件”,linux 中的一切资源都可以通过文件的方式访问和管理。而 FD 就类似文件的索引(符号、指针),指向某个资源,内核(kernel)利用 FD 来访问和管理资源。

之前在视频中有同学问既然有 socket,为什么文章内容全是用的 FD 来举例,这是因为当我们调用内核函数创建 socket 后,内核返回给我们的是 socket 对应的文件描述符(fd),所以我们对 socket 的操作基本都是通过 fd 来进行。

Socket 通信

接着我们通过一张图来看下客户端和服务器使用 socket 进行通信的核心流程。

图中函数的含义如下:

socket:创建一个套接字

bind:将 socket 绑定到指定地址

listen:使套接字处于监听状态,等待客户端连接到来

accept:接受客户端连接

connect:客户端发起连接

read:从 fd 对应的 socket 中读取数据

write:将数据写入 fd 对应的 socket 中

close:关闭 socket 文件描述符

核心交互流程如下:

1)服务器端通过 socket、bind、listen 对 socket 进行初始化,最后阻塞在 accept 等待客户端请求到来。

2)客户端通过 socket 进行初始化,然后使用 connect 向服务端发起连接请求。此时客户端会和服务端进行 TCP 三次握手,三次握手完成后,客户端和服务端建立连接完毕,开始进入数据传输过程。

3)客户端发起 write 系统调用写入数据,数据从用户空间拷贝到内核空间 socket 缓冲区,最后内核将数据通过网络发送到服务器。

4)数据经过网络传输到达服务器网卡,接着内核将数据拷贝到对应的 socket 接收队列,最后将数据从内核空间拷贝到用户空间。

5)客户端和服务器完成交互后,调用 close 函数来断开连接。

IO模型小例子

接着我们通过一个例子来了解下各种IO模型。

例子:你是一个老师,让学生做作业,学生做完作业后收作业。

同步阻塞:逐个收作业,先收A,再收B,接着是C、D,如果有一个学生还未做完,则你会等到他写完,然后才继续收下一个。

解析:这就是同步阻塞的特点,只要中间有一个未就绪,则你会被阻塞住,从而影响到后面的其他学生。

同步非阻塞:逐个收作业,先收A,再收B,接着是C、D,如果有一个学生还未做完,则你会跳过该学生,继续去收下一个。

解析:可以看到同步非阻塞相较于同步阻塞已经是更好的方案了,你不会因为某个学生未就绪而阻塞住,这样就可以减少对后续学生的影响。但是这个方案也可能会出现其他问题,如果你下去收作业的时候,全部学生都还没做完,则你可能会白走一圈,然后一个作业也没收到。

select/poll:学生写完了作业会举手,但是你不知道是谁举手,需要一个个的去询问。

解析:这个方案相较于同步非阻塞来说有一点好处,就是你是确认有学生做完的,所以你下去肯定能收到作业,但是他有一个不好的点在于你需要一个个的去询问。

epoll:学生写完了作业会举手,你知道是谁举手,你直接去收作业。

解析:这个方案就很高效了,每次都能准确的收到作业。

同步阻塞IO

核心流程

当应用程序发起 read 系统调用时,在内核数据没有准备好之前,应用程序会一直处于阻塞等待状态,直到内核把数据准备好了返回给应用程序

交互流程

我们通过两段代码的一个动图来模拟同步阻塞IO下服务端和客户端的执行流程:

 大致流程如下:

1)服务端进行初始化:新建 socket、绑定地址、转为服务端 socket

2)服务端调用 accept,进入阻塞状态,等待客户端连接

3)客户端新建 socket,向服务端发起连接

4)服务端和客户端通过 TCP 三次握手建立连接

5)服务端继续执行 read 函数,进入阻塞状态,等待客户端发送数据

6)客户端向服务端发送数据

7)服务端读取数据,执行逻辑处理

同步阻塞IO模型

我们通过 read 函数来看下服务器内部用户空间和内核空间的调用流程,如下图所示:

大致流程如下:

1)应用进程发起 read 系统调用

2)应用进程阻塞等待数据就绪

3)数据通过网络传输到达网卡,然后再到内核socket缓冲区,当数据被拷贝到内核 socket 缓冲区时,此时处于就绪状态

4)将数据从内核拷贝到应用程序缓冲区,返回成功

多线程版本:文中使用的例子是单线程,如果是多线程则在每个 socket 建立连接后新建线程去负责处理该 socket 后续的流程,这样就不会由于单个 socket 阻塞住而影响到其他 socket。

总结

单线程:某个 socket 阻塞,会影响到其他 socket 处理。

多线程:当客户端较多时,会造成资源浪费,全部 socket 中可能每个时刻只有几个就绪。同时,线程的调度、上下文切换乃至它们占用的内存,可能都会成为瓶颈。

同步非阻塞IO

核心流程

当应用程序发起 read 系统调用时,在内核数据没有准备好之前,内核会直接返回错误,应用程序不断轮询内核,直到内核把数据准备好了返回给应用程序。

交互流程

我们通过两段代码的一个动图来模拟同步阻塞IO下服务端和客户端的执行流程:

 大致流程如下:

1)服务端调用 accept,数据未就绪,内核返回-1

2)服务端调用 accept,数据未就绪,内核返回-1

3)服务端调用 accept,数据未就绪,内核返回-1

4)客户端新建 socket,向服务端发起连接

4)服务端调用 accept,服务端和客户端通过 TCP 三次握手建立连接

5)服务端执行后续逻辑处理

我们通过 read 函数来看下服务器内部用户空间和内核空间的调用流程,如下图所示:

 大致流程如下:

1)服务端调用 read,数据未就绪,内核返回-1

2)服务端调用 read,数据未就绪,内核返回-1

3)服务端调用 read,数据就绪

4)将数据从内核拷贝到应用程序缓冲区,返回成功

同步非阻塞IO模型

总结

提供了非阻塞调用的方式,从操作系统层面解决了阻塞问题。

优点

单个 socket 阻塞,不会影响到其他 socket 

缺点

需要不断的遍历进行系统调用,有一定开销

SELECT

核心流程

1)应用程序首先发起 select 系统调用,传入要监听的文件描述符集合

2)内核遍历应用程序传入的 fd 集合,如果遍历完一遍后发现没有就绪的 fd 则用户进程会进入阻塞状态,如果有就绪的 fd 则会对就绪的 fd 打标,然后返回

3)应用程序遍历 fd 集合,找到就绪的 fd,进行相应的事件处理

select 接口

/*** 获取就绪事件** @param nfds      3个监听集合的文件描述符最大值+1* @param readfds   要监听的可读文件描述符集合* @param writefds  要监听的可写文件描述符集合* @param exceptfds 要监听的异常文件描述符集合* @param timeval   本次调用的超时时间* @return 大于0:已就绪的文件描述符数;等于0:超时;小于:出错*/
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

交互流程

我们通过一个动图来模拟服务器内部用户空间和内核空间的调用流程,如下图所示:

 大致流程如下:

1)用户空间发起 select 系统调用,将监听的 fd 集合从用户空间拷贝到内核空间

2)内核遍历 fd 集合,检查数据是否就绪

3)如果遍历一遍后发现没有 fd 就绪,则会将当前用户进程阻塞,让出 CPU 给其他进程

4)当客户端将数据发送到服务端,进入内核后,会通过数据库包找到对应的socket 

PS:客户端发送数据到数据进入服务端内核的流程类似下面 epoll 的流程

5)socket 检查是否有阻塞等待的进程,如果有则唤醒该进程

6)用户进程恢复运行后,会再遍历 fd 集合进行检查,此时它会检查到某些 fd 已经就绪了,它会给这些 fd 打上标记,然后结束阻塞,返回到用户空间

7)用户空间知道有事件就绪,遍历 fd 集合,找到就绪的 fd,进行相应的事件处理,例如将数据从内核缓冲区拷贝到应用程序缓冲区

8)最后执行逻辑处理。

IO多路复用模型

fd_set

fd_set 在 select 的整个调用过程中表达了两种不同的意思。

在入参时,fd_set 表示应用程序要监听哪些 fd;在回参时,fd_set表示哪些 fd 已经就绪了。

应用程序传入的 fd_set 其实是个位图,例如我们要监听 fd = 1、fd = 4,则传入 0000 0101,也就是 5。

这边使用的 long 类型数组来实现位图:1个 long 可以表示64位,则16个long可以表示1024位。

当内核处理完毕,将就绪的 fd 返回时,会将就绪的 fd 对应的位标记为1,然后覆盖掉入参的 fd_set,所以我们最终返回时的 fd_set 表示的是哪些 fd 是就绪的。

总结

  • 将 socket 是否就绪检查逻辑下沉到操作系统层面,避免大量系统调用。

  • 告诉你有事件就绪,但是没告诉你具体是哪个 FD。

优点

  • 不需要每个 FD 都进行一次系统调用,解决了频繁的用户态内核态切换问题

缺点

  • 单进程监听的 FD 存在限制,默认1024

  • 每次调用需要将 FD 从用户态拷贝到内核态

  • 不知道具体是哪个文件描述符就绪,需要遍历全部文件描述符

  • 入参的3个 fd_set 集合每次调用都需要重置

POLL

核心流程

基本同 select。

poll 接口

/*** 获取就绪事件** @param pollfd  要监听的文件描述符集合* @param nfds    文件描述符数量* @param timeout 本次调用的超时时间* @return 大于0:已就绪的文件描述符数;等于0:超时;小于:出错*/
int poll(struct pollfd *fds,unsigned int nfds,int timeout);struct pollfd {int fd;         // 监听的文件描述符short events;   // 监听的事件short revents;  // 就绪的事件
}

poll 函数基本同 select,只是对 select 进行了一些小优化,一个是优化了1024个文件描述符上限,另一个是新定义了 pollfd 数据结构,使用两个不同的变量来表示监听的事件和就绪的事件,这样就不需要像 select 那样每次重置 fd_set 了。

总结

跟 select 基本类似,主要优化了监听1024的限制。

优点

  • 不需要每个 FD 都进行一次系统调用,导致频繁的用户态内核态切换

缺点

  • 每次需要将 FD 从用户态拷贝到内核态

  • 不知道具体是哪个文件描述符就绪,需要遍历全部文件描述符

EPOLL

核心流程

1)应用程序调用 epoll_create,内核会分配一块内存空间,创建一个 epoll,最后将 epoll 的 fd 返回,我们后续可以通过这个 fd 来操作 epoll 对象

2)应用程序不断调用 epoll_ctl 将我们要监听的 fd 维护到 epoll,内核通过红黑树的结构来高效的维护我们传入的 fd 集合

3)应用程序调用 epoll_wait 来获取就绪事件,内核检查 epoll 的就绪列表,如果就绪列表为空则会进入阻塞,否则直接返回就绪的事件。

4)应用程序根据内核返回的就绪事件,进行相应的事件处理

epoll 接口

/*** 创建一个epoll** @param size epoll要监听的文件描述符数量* @return epoll的文件描述符*/
int epoll_create(int size);/*** 事件注册** @param epfd        epoll的文件描述符,epoll_create创建时返回* @param op          操作类型:新增(1)、删除(2)、更新(3)* @param fd          本次要操作的文件描述符* @param epoll_event 需要监听的事件:读事件、写事件等* @return 如果调用成功返回0, 不成功返回-1*/
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);/*** 获取就绪事件** @param epfd      epoll的文件描述符,epoll_create创建时返回* @param events    用于回传就绪的事件* @param maxevents 每次能处理的最大事件数* @param timeout   等待I/O事件发生的超时时间,-1相当于阻塞,0相当于非阻塞* @return 大于0:已就绪的文件描述符数;等于0:超时;小于:出错*/
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);

交互流程

我们通过一个动图来模拟服务器内部用户空间和内核空间的调用流程,如下图所示:

 大致流程如下:

1)用户空间调用 epoll_create ,内核新建 epoll 对象,返回 epoll 的 fd,用于后续操作

2)用户空间反复调用 epoll_ctl 将我们要监听的 fd 维护到 epoll,底层通过红黑树来高效的维护 fd 集合

3)用户空间调用 epoll_wait 获取就绪事件,内核检查 epoll 的就绪列表,如果就绪列表为空则会进入阻塞

4)客户端向服务端发送数据,数据通过网络传输到服务端的网卡

5)网卡通过 DMA 的方式将数据包写入到指定内存中(ring_buffer),处理完成后通过中断信号告诉 CPU 有新的数据包到达

6)CPU 收到中断信号后,进行响应中断,首先保存当前执行程序的上下文环境,然后调用中断处理程序(网卡驱动程序)进行处理:

  • 根据数据包的ip和port找到对应的socket,将数据放到socket的接收队列;

  • 执行 socket 对应的回调函数:将当前 socket 添加到 eventpoll 的就绪列表、唤醒 eventpool 等待队列里的用户进程(设置为RUNNING状态)

7)用户进程恢复运行后,检查 eventpoll 里的就绪列表不为空,则将就绪事件填充到入参中的 events 里,然后返回

8)用户进程收到返回的事件后,执行 events 里的事件处理,例如读事件则将数据从内核缓冲区拷贝到应用程序缓冲区

9)最后执行逻辑处理。

IO多路复用模型

同 select。

总结

epoll 直接将 fd 集合维护在内核中,通过红黑树来高效管理 fd 集合,同时维护一个就绪列表,当 fd 就绪后会添加到就绪列表中,当应用空间调用 epoll_wait 获取就绪事件时,内核直接判断就绪列表即可知道是否有事件就绪。

优点

解决了 select 和 poll 的缺点,高效处理高并发下的大量连接,同时有非常优异的性能。

缺点

  • 跨平台性不够好,只支持 linux,macOS 等操作系统不支持

  • 相较于 epoll,select 更轻量可移植性更强

  • 在监听连接数和事件较少的场景下,select 可能更优

LT VS ET

LT:Level-triggered,水平(条件)触发,默认。epoll_wait 检测到事件后,如果该事件没被处理完毕,后续每次 epoll_wait 调用都会返回该事件。

ET:Edge-triggered,边缘触发。epoll_wait 检测到事件后,只会在当次返回该事件,不管该事件是否被处理完毕。

小结

epoll 和 select、poll 默认都是 LT 模式,LT 模式会更安全一点,而 ET 则是 epoll 为了性能开发的一种新模式,LT 模式下内核在返回就绪事件之前都会进行一次额外的判断,如果 fd 量较大,会有一定的性能损耗。

总结

可以看到从最初的同步阻塞IO,到现在主流的 epoll,其实是一个不断演进的过程,就像我们的业务系统一样。

同步阻塞IO的方式实现比较简单,同时在当时可能已经能满足需求了,因此被最早提出来,然后随着不断的发展,在一些场景下,同步阻塞IO逐渐不能满足需求,于是操作系统底层开始优化,提出了非阻塞的模式。类似的,同步非阻塞IO也存在一定的问题,于是就有了后续的IO多路复用。

现在还有一种更牛逼的IO模型也在发展,叫做异步IO,这种模型下,你只需要一次非阻塞的系统调用,后续的事情全部由内核来帮你完成。不过异步IO当前在 linux 下还不够完善,所以当前 linux 的主流还是 epoll。

推荐阅读

全网最实用的 IDEA Debug 调试技巧(超详细案例)

面试官:如何进行 JVM 调优(附真实案例)

Java 基础高频面试题(2021年最新版)

Java 集合框架高频面试题(2021年最新版)

面试必问的 Spring,你懂了吗?

面试必问的 MySQL,你懂了吗?


http://chatgpt.dhexx.cn/article/bFx1PDmU.shtml

相关文章

计算机网络基础之多路复用技术

温故: 1、单工传输:单工传输只支持数据在一个方向上传输,数据传送只能在一个方向上进行,任何时候都不能改变方向,就像公路上的单行道,例如无线电广播。 2、半双工传输:半双工传输允许数据在两个方向上传输&…

《JAVA核心知识》学习笔记(JVM)-1

JVM (1) 基本概念: JVM 是可运行 Java 代码的假想计算机 ,包括一套字节码指令集、一组寄存器、一个栈、 一个垃圾回收,堆 和 一个存储方法域。 JVM 是运行在操作系统之上的,它与硬件没有直接 的交互 Hotspot JVM 后台运行的系统线…

多路复用技术概述

概述频分复用(Frequency Division Multiplexing)时分复用(Time Division Multiplexing)波分复用(Wave Division Multiplexing)码分复用(Code Division Multiplexing) 概述 数据是在物理链路的信道中传输的,通常一条链路上会有多条信道。在默认情况下,一…

计算机网络-多路复用

什么是多路复用技术呢? 多路复用(multiplexing),简称复用,是通信技术中的基本概念 。 事实上,多路复用技术的原理就是,把通信资源或者说是链路、信道资源进行的划分,分成一系列的资源片。把这些资源片分配…

一、多路复用

1.什么是多路复用 数据通信系统或计算机网络系统中,传输媒体的带宽或容量往往会大于传输单一信号的需求,为了有效地利用通信线路,希望一个信道同时传输多路信号,这就是所谓的多路复用技术(Multiplexing)。采用多路复用技术能把多个信号组合起…

分类变量回归: R语言中哑变量编码本质

本篇描述分类变量如何进行回归(翻译自http://www.sthda.com/english/articles/40-regression-analysis/163-regression-with-categorical-variables-dummy-coding-essentials-in-r/) 分类变量(也称为因子或定性变量)是可以将观测数据分组的变量。它们有…

python哑变量转换为类别变量

就是get_dummies()功能的逆变化,把哑变量重新变为类别变量 原先的数据 转变后的数据 代码如下: df.columns[1,2,3,4,5,6]df df[df1].stack().reset_index() df.columns[A,B,C] print(df) del df[A] del df[C]

回归模型中的哑变量

在构建回归模型时,如果自变量X为连续性变量,回归系数β可以解释为:在其他自变量不变的条件下,X每改变一个单位,所引起的因变量Y的平均变化量;如果自变量X为二分类变量,例如是否饮酒(…

Python超实用小技巧:分类变量转化为哑变量(附哑变量详解)

代码示例 features ["Pclass", "Sex", "SibSp", "Parch"]# 筛选出分类变量用来建模X pd.get_dummies(train[features])# 把分类变量转化为哑变量 哑变量详解 定义:哑变量(DummyVariable)&#xf…

多元线性回归哑变量设置方法

多元线性回归是研究一个连续型变量和其他多个变量间线性关系的统计学分析方法,如果在自变量中存在分类变量,如果直接将分类变量和连续性变量统一纳入模型进行分析是有问题的,尤其是无序分类资料,即使进入了模型,也难以…

matlab虚拟变量,不要再稀里糊涂的做回归了:如何设置哑变量

原标题:不要再稀里糊涂的做回归了:如何设置哑变量 虚拟变量 ( Dummy Variables) 又称虚设变量、名义变量或哑变量,用以反映质的属性的一个人工变量,是量化了的自变量,通常取值为0或1。引入哑变量可使线形回归模型变得更…

哑变量与逻辑回归

哑变量与逻辑回归 数据 部分数据: admit,gre,gpa,rank 0,380,3.61,3 1,660,3.67,3 1,800,4,1 1,640,3.19,4 0,520,2.93,4 1,760,3,2 1,560,2.98,1 0,400,3.08,2 1,540,3.39,3 0,700,3.92,2 0,800,4,4 0,440,3.22,1 1,760,4,1 0,700,3.08,2 1,700,4,1导入库 numpy…

哑变量处理

dummyVars(formula, data, sep “.”, levelsOnly FALSE, fullRank FALSE, …) sep:因子变量名及其级别之间的可选分隔符。使用sep NULL表示没有分隔符(即模型的正常行为)。 data4 <- read.csv("玩家玩牌数据.csv") head(data4) library(VIM) aggr(data4,pro…

哑变量的基本介绍及R语言设置

哑变量的基本介绍及R语言设置 1. 哑变量的基本介绍【摘自医咖会】1.1 什么是哑变量&#xff1f;1.2 什么情况下需要设置哑变量?1.3 如何设置哑变量的参照组&#xff1f;1.4 设置哑变量时的注意事项 2. R语言中哑变量的设置2.1 示例数据2.2 哑变量设置的4种方式2.3 线性回归小实…

matlab如何转换哑变量,SPSS教程:手把手教你设置哑变量及解读结果!

将哑变量引入回归模型,虽然使模型变得较为复杂,但可以更直观地反映出该自变量的不同属性对于因变量的影响,提高了模型的精度和准确度。 举一个例子,如职业因素,假设分为学生、农民、工人、公务员、其他共5个分类,其中以“其他职业”作为参照,此时需要设定4个哑变量X1-X4…

clear both

<style type"text/css"> p.f1{float:left;width :100px;} p.f2{float:left;width :100px;} </style> <title>无标题文档</title> </head> <body> <p class"f1">这个是第1项 </p> <p class"f2&quo…

CSS中正确理解clear:both

原文地址&#xff1a;http://blog.sina.com.cn/s/blog_709475a10100wkdj.html 要注意以下几点&#xff1a; 1、 浮动元素会被自动设置成块级元素&#xff0c;相当于给元素设置了display:block&#xff08;块级元素能设置宽和高&#xff0c;而行内元素则不可以&#xff09;。 …

clear:both 的作用

如&#xff1a; <div style"border:2px solid red;"> <div style"float:left;width:80px;height:80px;border:1px solid blue;">TEST DIV</div> <div style"clear:both;"></div> </div> 你可以将此部分代码…

CSS中clear:both的作用

clear:both意思就是清除浮动&#xff0c;例如我们设置了三个div如下&#xff1a; <div style"line-height: 30px;background-color: #EEEEEE;height: 100px;width: 50px;float: left;padding: 5px;">侧边栏<br>侧边栏 </div> <div style"…

css中clear:both属性的理解及用法

css中clear:both属性的作用是清除浮动&#xff0c;设置了浮动就会破坏文档流结构&#xff0c;影响后边的布局&#xff0c;此时在设置清除浮动便可解决这一问题&#xff0c;可以认为&#xff0c;设置了clear:both的当前元素会把前边元素中设有浮动属性的元素&#xff0c;当做没设…