谈谈对IO多路复用的select机制的理解

news2024/11/22 16:30:33

一、技术背景

如果要彻底明白select机制,还是要首先去了解IO,网络编程、Blocking IO、No Blocking IO的相关概念及底层实现。下面只是作为技术背景去介绍这几个概念。

1、理解IO本质

IO从英文本身去解释就是输入输出(Input/Output),这里不去过分深究计算机IO的这个概念,从通俗的来讲,可以理解为将数据(二进制)输入到计算机中,或者将数据从计算机输入到其他的硬件设备,如磁盘,网卡、其他外设之类的。

2、网络编程

所谓的网络编程,这里我也不细扣网上的概念,因为概念比较抽象,按照我的理解实际上就是把数据由内存写到网卡,或者网卡读取到远端传过来的数据,写入到内存,然后再由计算机程序去处理的一个交互的过程。对比磁盘IO,就是磁盘变成了所谓的网卡。

备注:下面讲的阻塞IO还是非阻塞IO均是针对网络编程这块。

3、阻塞IO

阻塞IO的全称即是BIO(Blocking IO),从它的英文来说都知道是阻塞IO,但是我们要弄懂阻塞IO阻塞的是哪部分。

从linux的库函数去理解,即就是recvfrom方法的系统调用,就会把当前进程(或线程)阻塞,等到操作系统数据将数据由内核态拷贝到用户态才会进行返回。如下图所示:

4、非阻塞IO

非阻塞IO的全称即是 No Blocking IO,也称为我们常说的NIO,它其实与BIO的本质区别就是数据未就绪的情况调用的时候立即返回,但是如果有数据已就绪的情况下。还是会阻塞等到数据拷贝完成,才进行返回。

至于什么是数据就绪,我个人理解分为两种情况:

(1)数据没有就绪

  • 即要么网卡没有收到远程客户端的数据。
  • 网卡收到了远程客户端的数据,但还没有拷贝到内核态。

(2)数据就绪

  • 网卡收到了远程客户端的数据,拷贝数据到内核态完成。

5、整体理解

下面是个人结合客户端服务端、七层网络协议、用户态内核态、socket编程后。对整个计算机的网络读取响应的理解所画的图。

 可以看出可以把socket理解为对底层操作系统的逻辑的屏蔽所封装出来,提供给我们进行网络IO交互的对象,通过对象我们可以读数据以及写数据。

二、select机制与在整个读取数据过程中的关系。

通过技术背景下的整个交互图来看。设想假如没有select(IO多路复用)机制,如果让我们想想现有的流程,我们怎么实现网络数据的读取。

1、BIO机制的引入

这里是不是可以用到我们上面说的BIO机制,通过recvform函数的调用,对当前进程(线程)进行阻塞,当远程客户端有数据发送到服务器这边,再进行返回。如下图所示。

通过上面流程我们是可以也可以实现远程网络数据的读取。但是你使用BIO会导致一个什么问题?答案就是阻塞进程(阻塞线程),想想如果我们以进程(线程)级别去调用,拿线程来说,我们的线程就会被挂起,这时候就有以前BIO的解决方案,那就是多线程+线程池的模式。如下图所示

 这个时候确实能支撑多个连接请求的问题,但是这个时候就会有个致命的问题。那就是线程池的数量有限,同时开启了多个线程对CPU负载很高,所以这个模型的缺陷导致无法支持更多的客户端请求。

2、NIO机制的引入

于是乎想到不是有NIO吗?如果使用NIO是不是就不用阻塞,用一个特有的线程去处理接受请求以及返回数据。没错,使用NIO确实解决了多线程的问题。但是NIO也会有一个问题,就是处理请求的线程会一直空转,因为要时刻监听发生了什么事件。如下图NIO代码所示。

public class NioServer {

    // 保存客户端连接
    static List<SocketChannel> channelList = new ArrayList<>();

    public static void main(String[] args) throws IOException {

        // 创建NIO ServerSocketChannel,与BIO的serverSocket类似
        ServerSocketChannel serverSocket = ServerSocketChannel.open();
        // 绑定端口号 9000
        serverSocket.socket().bind(new InetSocketAddress(9000));
        // 设置ServerSocketChannel为非阻塞
        serverSocket.configureBlocking(false);
        System.out.println("服务启动成功");

        while (true) {
            // 这个 while 循环就一直在跑
            // 非阻塞模式 accept 方法不会阻塞,否则会阻塞
            // NIO的非阻塞是由操作系统内部实现的,底层调用了linux内核的accept函数
            SocketChannel socketChannel = serverSocket.accept();
            // 如果有客户端进行连接
            if (socketChannel != null) {
                System.out.println("连接成功");
                // 设置SocketChannel为非阻塞,读取数据前不阻塞
                socketChannel.configureBlocking(false);
                // 保存客户端连接在List中,将刚刚客户端与服务器建立的通道放到这个list中
                channelList.add(socketChannel);
            }
            // 遍历连接进行数据读取,不管这个通道有木有数据都会遍历
            Iterator<SocketChannel> iterator = channelList.iterator();
            while (iterator.hasNext()) {
                SocketChannel sc = iterator.next();
                ByteBuffer byteBuffer = ByteBuffer.allocate(128);
                // 非阻塞模式read方法不会阻塞,否则会阻塞
                int len = sc.read(byteBuffer);
                // 如果有数据,把数据打印出来
                if (len > 0) {
                    System.out.println("接收到消息:" + new String(byteBuffer.array()));
                    // 如果客户端断开,把socket从集合中去掉
                } else if (len == -1) {
                    iterator.remove();
                    System.out.println("客户端断开连接");
                }
            }
        }
    }
}

可以看到如上面NIO代码所示,需要不停的while循环进行空转来获取感兴趣的监听事件。非常消耗CPU的资源。到这里我们是不是应该思考,还有什么方式既能够实现NIO的方式,又能不空转,不浪费CPU资源呢?

3、select机制的引入

没错,其实你想实现的,linux的设计者也想到了,所以在linux早期就提供了select机制加上NIO的模式去实现处理客户端的请求的模式。select是linux最早期IO多路复用机制。其实本质上NIO的过程是不变的,变的是由操作系统告知我们有事件变更了,我们才去读取事件,而不是不断轮询去读取事件。如下图所示(以一个客户端为例)

 这个时候借助了IO多路复用机制,监听我们的感兴趣的事件发生后再进行数据的读取,就可以避免多余的CPU空转导致的CPU浪费的问题。变成了比较完善的一套机制。此时在这里可以下结论IO多路复用机制其实就是帮助我们高效的处理客户的请求

三、Select机制的原理

上面说到既然select机制能够这样帮我们去处理程序,那么它本身的实现原理是什么呢?同时它本身又有什么局限性呢?带着这个疑问我们继续挖掘这个select技术的本质。

1、底层库函数

我们做技术人讲究的是源代码,那好我们就来深入select的源码看看,其实select机制如果深挖到代码级别对应的就是linux系统下的一个底层库函数,我们来看看select函数

int select (int __nfds, fd_set *__restrict __readfds,
           fd_set *__restrict __writefds,
           fd_set *__restrict __exceptfds,
           struct timeval *__restrict __timeout);

既然看到这个函数,那我们来看看函数的参数到底代表什么?

参数说明:
__nfds:集合中所有文件描述符的范围,需设置为所有文件描述符中的最大值加1。
readfds:要进行监听的是否可以读文件的文件描述符集合。
writefds:要进行监听的是否可以写文件的文件描述符集合。
errorfds:要进行监听的是否发生异常的文件描述符集合。
timeval:select的超时时间,它可以使select处于三种状态:
1、若将NULL以形参传入,即不传入时间结构,就是将select至于阻塞状态,一定要等到监视的文件描述符集合中某个文件描述符发生变化为止。
2、若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否发生变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值。
3、timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回,否则在超时后不管怎样一定返回。
4、函数的返回值:
返回值 > 0:表示被监视的文件描述符有变化。数值表示变化的个数
返回值 = -1:表示select出错。
返回值 = 0:表示超时。

其实参数本身也是很好理解,linux一切皆文件的前提下,linux操作系统会为每一个Socke分配一个文件文件描述符去标识一个socket,通过文件描述符能对socket进行读写操作。所以fd_set就是文件描述符的集合。

那么fd_set又是怎样的一个结构呢?我们看看linux源码是怎么写的

typedef __kernel_fd_set     fd_set;
#undef __FD_SETSIZE
#define __FD_SETSIZE    1024

typedef struct {
    unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

可以看到,它本质上是一个结构体,这是C语言的语法。然后一个结构体里面会有一个long的数组。那么这个long型的数组是如何存储我们的文件描述符的呢?我们一步步来

(1)首先看这个数组的大小,__FD_SETSIZE / (8 * sizeof(long)) = 1024 / (8 * 8) = 16 

我们来看看这个计算规则,sizeof(long) 如果是64位计算机系统,则占8个字节,所以这个数组就是16个元素。

那么它是如何做到用16个元素的long数组存储1024个文件描述符的呢?

这里能用位存储的思路去理解,想想一个16个元素的long数组,而long本身占8个字节,那么总共可以表示16 * 8 * 8(一个字节等于8位) = 1024位,没错就是__FD_SETSIZE的值。还记得我们文件描述符是整形吗,那么就可以这样进行表示出文件描述符与Socket的关系,如下图所示。 

使用位存储的好处是什么?当然是节省空间,用更小的空间结构存储更多的映射关系。

所以在这里我们就可以知道为什么select是有上限的,最大只能监听1024个文件描述符,是因为linux代码写死了1024,如果想要监听更多的文件描述符,只有修改linux源码才可以实现。

2、Select机制是如何实现我们的事件监听的呢?

这里其实我本质上也是看网上的分析,推荐几篇文章

Linux select 一网打尽 - 知乎

深入select多路复用内核源码加驱动实现 - 黑客画家的个人空间 - OSCHINA - 中文开源技术交流社区

Linux select/poll机制原理分析 - 知乎

讲的还是挺不错的,这里我就在这个基础上进行一些自己对select机制的理解与总结,如下图所示。

源码调用链:select ---> sys_select ----> core_sys_select ---->  do_select

结合源码与网上的介绍文章,结合我自己的理解,我重新总结了一张图:

图中省略了复杂的数据结构,只留下整体的运转流程。

其实select机制说白了也是要借助底层驱动的支持,即当有事件发生时能够触发事件回调。由硬件层通知到我们的内核态,然后由内核态通知到我们的应用态的一个过程。

3、关于Select机制的总结

  • 由于底层数据结构的写死,所以select最多支持1024个文件描述符,也就是1024个连接
  • 以fd_set为例,每次都要从用户态拷贝至内核态,同时还要在内核态进行循环遍历,然后把有事件的响应的文件描述符fd_set返回,又要从内核态拷贝至用户态。用户态拿到这个有事件的文件描述符返回,还要针对返回的描述符进行遍历,才能知道哪个文件描述符对应的Socket可写可读,总共经历了两次遍历,两次拷贝,所以说为什么Select在文件描述符比较多的情况,效率为什么是低下的原因。
  • 因为Select返回后会污染入参的fd_set,所以每次调用都要重设fd_set,也是一个比较麻烦的点。

四、总结

1、关于Select就写到这里了,个人认为理解了Select机制再去理解Poll、Epoll会更容易理解,因为这两个就是在Select的基础上进行优化后的机制。所以才会深入研究下。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/91692.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

web前端期末大作业:基于HTML+CSS+JavaScript实现网上鲜花店网站设计(14页)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

手机进销存网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 功能模块包括&#xff1a;员工模块、手机类型模块、供应商模块、采购模块、客户模块、销售模块、统计模块、库存模块 (1…

Springboot内置的工具类之FileCopyUtils

前言 Spring内置的工具类里&#xff0c;最喜欢用的就是文件读写这一部分&#xff0c;虽然原生的写法也没几句&#xff0c;但是就是懒&#xff0c;不想循环、判断什么的&#xff0c;直接调用现成的静态方法&#xff0c;多高效&#xff0c;哈哈&#xff0c;这就是懒人必备。 Res…

UE5——动画混合(2)

一、引言 接上文《UE5——动画混合》&#xff0c;UE5还提供了 遮罩混合、惯性化两种混合&#xff0c;下面将讲述这两种动画混合方式 二、UE5上实现 1、遮罩混合 遮罩混合与《UE5——动画混合》中的 “骨骼的分层混合” 的运行逻辑其实是的一样的。相比起 “骨骼的分层混合”…

【SpringBoot+MyBatis】二级缓存以及使用Redis缓存数据

在MyBatis中&#xff0c;存在一级缓存以及二级缓存。一级缓存是默认自动开启&#xff0c;而二级缓存需要我们手动去开启。但看到这篇文章的人&#xff0c;大部分都是在做项目才发现的问题&#xff1a;每次访问数据都要查询一遍又一遍的数据库&#xff0c;这是在控制台上可以发现…

叶酸偶联N-季铵化壳聚糖(FA-HTCC)

叶酸偶联N-季铵化壳聚糖(FA-HTCC) 中文名称&#xff1a;叶酸偶联N-季铵化壳聚糖 英文名称&#xff1a;FA-HTCC 纯度&#xff1a;95% 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 外观:固体或粘性液体 包装&#xff1a;瓶装/袋装 溶解性&#xff1a;溶于大…

MYSQL 主从复制 --- binlog

一个MYSQL数据库存在的问题 在谈主从复制之前&#xff0c;应该都会有一个疑问&#xff0c;那么就是一个MYSQL数据库存在的问题呢&#xff1f; 1. 读和写所有压力都由一台数据库承担&#xff0c;压力大 2. 数据库服务器磁盘损坏则数据丢失&#xff0c;单点故障 为了解决我们可以…

Spark RDD介绍

RDD 引出问题 Spark是如何将多台机器上的数据通过一个类型来关联实现的&#xff1f; 答&#xff1a;通过RDD类型来实现关联 一、RDD简介 RDD&#xff08;Resilient Distributed Dataset&#xff09;&#xff1a;弹性分布式数据集 RDD的本质&#xff1a; 一个抽象的逻辑上的…

项目运行久了很卡,手动执行垃圾回收机制,秒丝滑

问题 当项目运行久了&#xff0c;内存会被大量占用。如何不重启项目&#xff0c;释放内存&#xff0c;继续丝滑开发呢&#xff1f; 回答 手动执行垃圾回收机制 打开任务管理器康康&#xff1a; 巨卡&#xff0c;无敌卡&#xff0c;解决它&#xff01; 打开命令行工具&…

Kaggle竞赛——Titanic泰坦尼克之灾(保姆级基础版)

Kaggle竞赛网址&#xff1a;https://www.kaggle.com/c/titanic 本次Kaggle泰坦尼克之灾分析过程大致分为&#xff1a; 第1步&#xff1a;了解数据 第2步&#xff1a;分析数据之间的关系 第3步&#xff1a;缺失项数据处理 第4步&#xff1a;特征因子化 第5步&#xff1a;处理特…

DBCO-SS-活性酯|DBCO-SS-NHS酯

DBCO-SS-活性酯|DBCO-SS-NHS酯 名称;DBCO-SS-活性酯|DBCO-SS-NHS酯 CAS NO&#xff1a;1435934-53-4 分子量&#xff1a;565.66 分子式&#xff1a;C28H27N3O6S2 含 量&#xff1a;>95% 外 观&#xff1a;固体粉末 保存&#xff1a;-20避光避湿 结构式&#xff1a; …

记录--手把手带你开发一个uni-app日历插件(并发布)

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 相信我们在开发各类小程序或者H5&#xff0c;甚至APP时&#xff0c;会把uni-app作为一个技术选型&#xff0c;其优点在于一键打包多端运行&#xff0c;较为强大的跨平台的性能。但是&#xff0c;只要开…

我为什么建议前端将Python 作为第二语言?

前言 “如何摆脱不停切图的困局&#xff1f;” 这不是一篇制造焦虑的文章&#xff0c;而是充满真诚建议的Python推广文。 当谈论到编程入门语言时&#xff0c;大多数都会推荐Python和JavaScript。 实际上&#xff0c;两种语言在方方面面都非常强大。 而如今我们熟知的ES6语…

用Java语言简单实现:炸金花,不知道你有没有兴趣跟着笔者来研究一下呀

说实话&#xff0c;到目前为止&#xff0c;笔者学习Java语言也有着进两个多月了吧&#xff01;&#xff01;但是&#xff0c;一直毫无进度&#xff01;博客更新的也很少&#xff0c;仅仅10篇刚出头&#xff0c;而且浏览量也很少&#xff0c;这样很不符合我的气质&#xff01;&a…

汉字风格迁移----FtransGAN

🚀针对的问题 以前的大多数作品都是通过将给定子集的风格转移到未见子集的内容来解决这个问题的。然而,他们只关注同一语言中的字体样式转换。在许多任务中,我们需要学习一种语言的字体信息,然后将其应用到其他语言。现有的方法很难完成这样的任务。 图1。几个应用实例。…

03_2排序算法:快速排序、归并排序

开始系统学习算法啦&#xff01;为后面力扣和蓝桥杯的刷题做准备&#xff01;这个专栏将记录自己学习算法是的笔记&#xff0c;包括概念&#xff0c;算法运行过程&#xff0c;以及代码实现&#xff0c;希望能给大家带来帮助&#xff0c;感兴趣的小伙伴欢迎评论区留言或者私信博…

IIS2MDCTR传感器规格、ISM303DACTR磁力计应用、STPSC20H12WL二极管特点

IIS2MDC 3轴数字式磁性传感器具有高达50高斯的磁场动态范围以及16位数据输出。IIS2MDC设有I2C串行总线接口&#xff0c;可支持标准模式 (100kHz)、快速模式 (400kHz)、快速模式 (1MHz) 以及高速模式 (3.4MHz)。 该传感器还设有SPI串行标准接口&#xff0c;另外对其进行配置后可…

IBPS低代码产品公司流辰信息:用心研发,不负众望!

作为一家低代码产品公司&#xff0c;流辰信息一直以高标准严格要求自己&#xff0c;努力研发&#xff0c;努力提升服务品质&#xff0c;从专业的角度为中大型企业数字化转型积极赋能&#xff0c;共创价值。 IBPS低代码开发产品是流辰信息努力研发的硕果&#xff0c;是满足企业级…

教材推荐网站

开发工具(eclipse/idea/vscode等)&#xff1a; 数据库(sqlite/mysql/sqlserver等)&#xff1a; 功能模块(请用文字描述&#xff0c;至少200字)&#xff1a; 高校教材推介交流平台的开发主要功能教师发布课程名称、用书信息&#xff0c;各出版社推荐样 书&#xff08;线上&#…

CRTO 考试总结

写在最前&#xff1a;欢迎大家来我的 Discord 频道 Northern Bay。邀请链接在这里&#xff1a;https://discord.gg/9XvvuFq9Wb 一起进步&#xff0c;一起 NB~ 背景 今天我结束了 Zero Point Security RTO I 的考试。证书到手。 Zero Point Security 是一家 Base 在英国的安全…