网络编程难点之select、poll与epoll详解

前言

为什么需要I/O多路复用技术?

首先,I/O多路复用技术主要被应用在需要高性能的网络服务器程序中。

高性能网络服务器程序需要做的事情就是供多个客户端同时进行连接并处理客户端传送过来的数据请求:

在这里插入图片描述

对于这种情况,很多人自然而然想到使用多线程的方式来处理,这当然是正确的想法,但这里我们并不讨论它,只探讨我们为什么需要I/O多路复用技术以及它到底能解决什么问题,因此多线程情况我们不作分析,下面都只探讨单线程情况下的程序情况。

在单线程情况下,多个客户端发起连接请求时,同一时间下服务器程序只能处理一个客户端的数据请求:

假设服务器程序接受了客户端1号的数据请求,那么单线程情况下该服务器程序就只能和客户端1号进行数据传输,此时若有客户端2号、3号发起连接请求,那么它们只能被阻塞(注意,后续客户端请求只是阻塞起来排起了队,并非数据就不要了,而这种技术是通过DMA控制器实现的,在计算机组成原理中有详细介绍这种技术)。

因此单线程服务器程序为了服务到位所有的客户端程序,我们理所当然的在写服务器端程序的数据处理代码时会有如下的逻辑:

//数据处理
while(1){//Fdx是建立通信连接的文件描述符//FdA~FdE表示已经建立通信连接的文件描述符数组for(Fdx in (FdA~FdE)){if(Fdx 有数据){读数据;处理数据;}}
}

即循环遍历各个已经建立通信连接的文件描述符,每遍历到一个就进行一次是否有数据的判断,然后进行数据处理。

这种方式简单粗暴,其实效率上也并不低,但是它确实有改进的地方,其缺陷如下:

1、时间复杂度高,很明显第二重循环是每次都要从头遍历的
2、每次都需要判断文件描述符中是否有数据,这个过程涉及到内核,用户态与内核态的频繁切换是其性能低下的原因之一

那么在单线程程序中,我们就可以使用I/O多路复用技术来提高单线程下在面对上述程序所面对的问题时的执行效率,也就是下面要说的select、poll和epoll技术(当然多线程下同样适用,但只要明白了单线程下的情况,多线程的情况也就是照猫画虎)。

select

首先来看一段代码,注意其中注释里的内容:

  sockfd = socket(AF_INET, SOCK_STREAM, 0);memset(&addr, 0, sizeof (addr));addr.sin_family = AF_INET;addr.sin_port = htons(2000);addr.sin_addr.s_addr = INADDR_ANY;bind(sockfd,(struct sockaddr*)&addr ,sizeof(addr));listen (sockfd, 5); for (i=0;i<5;i++) {memset(&client, 0, sizeof (client));addrlen = sizeof(client);fds[i] = accept(sockfd,(struct sockaddr*)&client, &addrlen);if(fds[i] > max)max = fds[i];}
// 上面是在准备下面代码要使用的已经建立通信连接的套接字文件描述符数组
//----------------------------------------------------------------
// 下面是处理连接请求的代码  while(1){FD_ZERO(&rset);for (i = 0; i< 5; i++ ) {FD_SET(fds[i],&rset);}//select的第一个参数表示监视对象文件描述符数量//第二个表示读操作的文件描述符监听集合//第三个表示写操作的文件描述符监听集合//第四个表示异常操作的文件描述符监听集合//第五个表示超时时间select(max+1, &rset, NULL, NULL, NULL);for(i=0;i<5;i++) {if (FD_ISSET(fds[i], &rset)){memset(buffer,0,MAXBUF);read(fds[i], buffer, MAXBUF);puts(buffer);}}	}

对于select函数最常用的也就是前面两个参数,现在来看下面两个问题:

1、为什么select第一个参数是 max+1?

select函数要求通过第一个参数传递监视对象文件描述符的数量。因此需要得到注册在rset变量中的文件描述符数。
又因为每次新建文件描述符时其值都会加1(文件描述符的值从小往大递增),所以只需要将最大的文件描述符加1再传递到select函数即可。加1是因为文件描述符的值从0开始。
如:需要监视的文件描述符数组为: 0,1,2,3,4,5,虽然最大的文件描述符值为5但总共的文件描述符的数量为6.

2、第二个参数监听文件描述符集合的作用是什么?

监听文件描述符集合本质上就是一个位图,它用来表征集合中的哪一个文件描述符触发了监听事件被置位,示例如下:

假设上述程序中,文件描述符数组中的文件描述符分别为:1 、2、 5、 7、 9;
对于监听集合rset,其作为位图这种数据结构,一开始其内存数据为:0、 0、 0、 0、 0 …
在经过上述程序中的FD_SET(fds[i],&rset)代码后,其内存数据变更为:0、1、1、0、0、1、0、1、0、1 …

另外在select函数中,rset的大小为 1024,这是系统内核决定的。

在select函数执行过程中,该函数会将rset集合拷贝一份放到内核态中,由内核态来帮我们完成之前简单粗暴的程序示例中的判断文件描述符是否有数据的过程,毫无疑问,内核程序进行判断会比我们自己写 if 要来的快,因为我们的if判断也是要询问内核的,询问内核的过程会涉及用户态与内核态的反复切换,效率低下。

在这里插入图片描述

如果所有的文件描述符都没有数据需要读取的话,那么 select会陷入阻塞。

当有数据需要读取的时候,监听集合rset中产生读取数据的文件描述符就会被置位(相当于被做了标记告诉select函数它要有数据需要读取),此时select就不再阻塞,程序得以进入到下一步。

下一步则就是我们所熟悉的了,因为select函数并未告知我们是哪些、有多少文件描述符有数据需要读取(被置位),因此我们只能继续使用之前的简单粗暴的方法进行循环遍历,通过宏函数FD_ISSET来判断是哪些文件描述符有数据需要读取,然后进行数据处理。

因此其提高效率最主要的一点在于select函数将rset这个文件描述符的集合拷贝到了内核态中让内核来帮我们判断文件描述符是否有数据需要读取,与此同时select函数也依然存在一些缺陷:

1、FD_SET这个位图数据结构最大为1024,上限太低
2、FD_SET在有数据读取时会发生置位,会改变对应文件描述符原来的位值,这意味着每一次select函数返回之后我们都需要将rset监听集合给恢复到一开始的情况,这同样会造成效率低下的问题
3、将rset集合从用户态拷贝到内核态一样是巨大的时空开销,即使它比我们一开始选择每次循环判断进入内核方式的开销要小,但还有提升的空间
4、在已经从select函数返回的情况下,因不知道是哪个或者哪几个文件描述符产生读取数据而导致的循环遍历所带来的时间开销

这基本就是select的全部内容了。

poll

poll的简单介绍

poll 是 UNIX 系统中的一个系统调用,用于实现 IO 多路复用。IO 多路复用允许单个进程或线程同时监视多个文件描述符(sockets、pipes、files等),并在这些文件描述符中的任何一个变得可读、可写或有异常条件时,获得通知。

poll 函数提供了一种机制,使得程序可以在不阻塞的情况下,同时等待多个文件描述符上的事件。

因为poll是方言,大家可能对其并非特别了解,因此介绍其函数原型如下:

#include <poll.h>  int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明如下:

fds:一个指向 pollfd 结构数组的指针,数组中每个元素代表一个要监视的文件描述符。
nfds:fds 数组中元素的数量,也即是程序想要监视的文件描述符的数量。
timeout:等待事件发生的超时时间,单位是毫秒。如果设置为 -1,则 poll 会无限期地等待;如果设置为 0,则 poll 会立即返回,不等待任何事件。

pollfd 结构体的定义如下:

struct pollfd {  int   fd;         /* 文件描述符 */  short events;     /* 等待的事件 */  short revents;    /* 实际发生的事件 */  
};

变量的解释说明:

fd:要监视的文件描述符。
events:程序关心的事件集合,可以是 POLLIN(数据可读)、POLLOUT(数据可写)、POLLERR(发生错误)、POLLHUP(连接挂起)等事件的一个或多个的组合。
revents:poll 返回时,这个字段会被设置为实际发生的事件集合。

返回值如下:

如果成功,poll 返回发生事件的文件描述符的数量。
如果发生错误,poll 返回 -1,并设置全局变量 errno 以指示错误类型。

poll的深入分析

同样的,先来看一段代码:

for (i=0;i<5;i++) {memset(&client, 0, sizeof (client));addrlen = sizeof(client);pollfds[i].fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);pollfds[i].events = POLLIN;}sleep(1);while(1){puts("round again");poll(pollfds, 5, 50000);for(i=0;i<5;i++) {//revents才是真正被内核置位的部分//因此判断该文件描述符是否被置位if (pollfds[i].revents & POLLIN){//当其被内核置为1表示有数据读取后,我们注意必须要将其置回0//就像select中重新更新位图一样pollfds[i].revents = 0;memset(buffer,0,MAXBUF);read(pollfds[i].fd, buffer, MAXBUF);puts(buffer);}}}

poll相比于select的改进都基于其第一个参数的pollfd结构体展开,它和select一样,会将要监听事件的文件描述符集合从用户态拷贝一份到内核态中,让内核态来帮忙判断是否存在待读取数据。

poll函数也是阻塞函数,如果监听集合中没有文件描述符存在待读取数据 ,那么就会陷入阻塞;而当有待读取数据时内核依然会进行一个置位操作,但这里就与select函数不同了,内核置位置的是pollfd结构体中的revents字段,该字段初始值为0。因此我们在通过其进行状态判断过后需要将其置位回原样,以便pollfd数组的下次循环监听。

接下来我们看poll函数相对于select函数作了哪些改进以及不足的地方有哪些:

优点:

1、poll 相对于早期的 select 来说,没有文件描述符数量的限制(虽然实际系统中可能仍然有限制,但通常更高)。
2、poll 提供了更灵活的事件集合。
3、pollfd相比于FD_SET来说重用性要高,因为不必每回更新整个监听事件的文件描述符集合

缺点:

因为poll的工作原理与select基本一样,所以剩下的缺点是一样的:
1、将pollfd数组从用户态拷贝到内核态一样是巨大的时空开销,即使它比我们一开始选择每次循环判断进入内核方式的开销要小,但还有提升的空间
2、在已经从poll函数返回的情况下,因不知道是哪个或者哪几个文件描述符产生读取数据而导致的循环遍历所带来的时间开销

这基本就是poll的全部内容了。

epoll

因为epoll是最常用的IO多路复用技术,因此我将花最大的篇幅来解释。

epoll的简单介绍

epoll 是 Linux 系统中的一个 I/O 多路复用机制,它是 select 和 poll 的后续改进版本。相比于 select 和 poll,epoll 提供了更高的性能,特别是在处理大量并发连接时。epoll 的主要优势在于其基于事件驱动的设计,它只关注那些真正活跃的文件描述符,而不是像 select 和 poll 那样轮询所有文件描述符。

epoll 相关的函数主要有三个:epoll_create、epoll_ctl 和 epoll_wait。

epoll_create: 创建一个新的 epoll 实例。

函数原型如下:

#include <sys/epoll.h>  int epoll_create(int size);

参数说明如下:

size 参数通常设置为需要监视的文件描述符的最大数量,但这个参数并不是严格的限制,只是内核初始化 epoll 实例时的一个提示(现在没啥用了,就随便填个数就行)。

epoll_ctl: 向 epoll 实例中添加、删除或修改文件描述符及其相关事件。

函数原型如下:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数说明如下:

epfd 是 epoll_create 返回的 epoll 实例的文件描述符。
op 表示操作类型,可以是 EPOLL_CTL_ADD(添加)、EPOLL_CTL_DEL(删除)或 EPOLL_CTL_MOD(修改)。
fd 是要添加、删除或修改的文件描述符。
event 是一个指向 epoll_event 结构体的指针,用于指定要关注的事件和回调函数。

epoll_event 结构体

其定义如下:

struct epoll_event {  __uint32_t events;    /* Epoll events */  epoll_data_t data;    /* User data variable */  
};

其成员部分说明如下:

events 字段表示感兴趣的事件类型,如 EPOLLIN(数据可读)、EPOLLOUT(数据可写)、EPOLLERR(发生错误)等。
data 字段是一个联合体,可以用于存储用户自定义的数据,通常用于在事件发生时关联文件描述符和用户数据。

epoll_data_t联合体

epoll_data_t 是一个联合体(union)类型,用于在 epoll 事件处理中存储与文件描述符相关的用户数据。这个联合体在 epoll_event 结构体中被用作 data 成员的类型,而 epoll_event 结构体用于描述 epoll 事件和相关的数据。

联合体(union)的特性是,在其所有成员中,任意时刻只允许一个成员拥有值,而其它成员的值是未定义的。这意味着,如果你给联合体的一个成员赋值,那么之前存储在其他成员中的值就会被覆盖。

其定义如下:

typedef union epoll_data {  void *ptr;  int fd;  uint32_t u32;  uint64_t u64;  
} epoll_data_t;

其成员部分说明如下:

ptr:一个指向 void 的指针,可以用来存储任意类型的指针。这通常用于指向与文件描述符相关的用户数据。
fd:一个 int 类型的成员,用来直接存储文件描述符本身。这是最常用的成员,因为很多时候我们需要知道哪个文件描述符触发了事件。
u32 和 u64:分别是 32 位和 64 位的无符号整数。这些成员可以用来存储用户自定义的整数值,但使用它们的情况相对较少。

由于 epoll_data_t 是一个联合体,我们不能同时使用 ptr 和 fd 成员。如果我们想要通过 ptr 成员来存储用户数据,并且同时还需要访问文件描述符,那么我们需要放弃使用 epoll_data_t 中的 fd 成员,并在 ptr 指向的用户数据中包含文件描述符。这通常是通过定义一个包含文件描述符和其他用户数据的结构体,并将该结构体的指针存储在 ptr 中来实现的。

在 epoll 的使用中,当注册一个文件描述符来监听事件时,我们可以通过 epoll_event 结构体的 data 成员来设置 epoll_data_t 的值。然后,当事件发生时,epoll_wait 函数会填充 epoll_event 结构体中的 data 成员,以便我们能够检索我们之前设置的数据,并知道是哪个文件描述符触发了事件。

epoll_wait: 等待 epoll 实例中的文件描述符上的事件发生。

函数原型如下:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数说明如下:

epfd 是 epoll 实例的文件描述符。
events 是一个指向 epoll_event 结构数组的指针,用于存储就绪的事件。
maxevents 指定 events 数组的大小。
timeout 指定等待事件的超时时间,单位为毫秒。

epoll的深入分析

同样的,先来看一段代码:

  struct epoll_event events[5];//epoll_create的参数没有意义,随便填int epfd = epoll_create(10);......//循环添加要监听事件的文件描述符进入监听集合events[5]for (i=0;i<5;i++) {static struct epoll_event ev;memset(&client, 0, sizeof (client));addrlen = sizeof(client);ev.data.fd = accept(sockfd,(struct sockaddr*)&client, &addrlen);ev.events = EPOLLIN;epoll_ctl(epfd, EPOLL_CTL_ADD, ev.data.fd, &ev); }//处理数据,可以看出比之前的所有select和poll函数都要简洁  while(1){puts("round again");nfds = epoll_wait(epfd, events, 5, 10000);for(i=0;i<nfds;i++) {memset(buffer,0,MAXBUF);read(events[i].data.fd, buffer, MAXBUF);puts(buffer);}}

使用epoll完成IO多路复用分三步走,首先使用epoll_create()创建一个epfd,即epoll实例,它是第二步epoll_ctl()函数以及第三步epoll_wait()函数的第一个参数,我们可以将这个epoll实例理解成一块白板,稍后还会再提这个。然后第二步使用epoll_ctl()函数来配置需要监听事件的文件描述符,从上述代码中我们可以知道我们是在使用epoll_ctl()往epfd这块白板上添加受监听的文件描述符与其对应的监听事件:
在这里插入图片描述
至此epfd相当于装填完毕,然后来到第三步调用epoll_wait()函数,在这里epoll函数将与select和poll函数不同,epoll中监听集合也就是刚刚提到的epfd不会再被整个从用户态拷贝到内核态,而是用户态和内核态一起操作这块空间,是共享的:

在这里插入图片描述

内核态依然是帮助我们判断哪一个文件描述符是否有数据到来,但我们不再会有从用户态到内核态进行数据拷贝产生的时空开销了,这相对于之前的select和poll函数而言是一个巨大的性能提升。

在没有数据的时候(在水平触发Level Trigger下),epoll与之前的select和poll一样会被阻塞。

而有数据的时候,epoll同样会进行置位的操作,但它不像之前一样需要位图这样的数据结构来进行标记,在epoll底层是使用红黑树进行了一个重排的操作:

假如以上图为例,从左往右第四个文件描述符有数据到来,那么它就会被置位然后进行重排,在重排后将会排到整个epfd队列的首位,如果有多个文件描述符同时有数据到来的话,那么就多个文件描述符一起重排,同样会被排到epfd队列的前面依序排列。

然后epoll_wait()函数便会返回,其返回值为被重排了位置的且有数据读取的文件描述符的数量,最后我们只要拿到这个数量然后依次循环遍历epfd队列前面相应数量的文件描述符(因为被重排到了epfd队列的首部嘛,所以只要遍历相应数量大小就可以处理完所有触发了数据读事件的文件描述符对象)即可完成数据的处理。

值得一提的是,被内核态置位了的文件描述符也不需要我们像之前那样进行重置了,系统会自动帮我们重置。

而且此时由于红黑树的高性能优势,循环的时间复杂度也从原来的O(n)降低成了O(1)。

epoll的优点总结如下:

高效的事件处理:epoll 采用基于事件驱动的设计,只关注活跃的文件描述符,而不是像 select 和 poll 那样轮询所有文件描述符。
支持大量文件描述符:epoll 没有 select 和 poll 中文件描述符数量的限制,可以处理数十万甚至上百万的文件描述符。
水平触发和边缘触发模式:epoll 支持水平触发(level-triggered)和边缘触发(edge-triggered)两种模式,可以根据不同的使用场景选择。
内存使用效率:epoll 在内核中使用了红黑树来存储文件描述符,这使得文件描述符的查找、插入和删除操作都非常高效。

缺点的话,等出了更牛逼的IO复用技术再说吧。

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

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

相关文章

【招生】江西师范大学—地质灾害研究团队—地理与环境学院/鄱阳湖流域实验室

【招生】江西师范大学—地质灾害研究团队—地理与环境学院/鄱阳湖流域实验室 研究方向&#xff1a;InSAR、极化SAR、GNSS、地球物理、GIS 招生专业&#xff1a;GIS、人文地理 学院地址&#xff1a;江西南昌江西师范大学

什么是数据同步利器DataX,如何使用?

什么是 Datax? DataX 是阿里云 DataWorks数据集成 的开源版本&#xff0c;使用Java 语言编写&#xff0c;在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、Ma…

python笔记_数据类型

定义&#xff1a;python的变量在使用前必须赋值&#xff0c;数据类型指的是变量指定的内存数据的类型 string字符串类型使用引号int整型整数float浮点型小数bool布尔值(逻辑)输出true/false A&#xff0c;整数类型 整型字节 1,python的整数有十六进制&#xff0c;十进制&#…

面试数据库篇(mysql)- 11主从同步

原理 MySQL主从复制的核心就是二进制日志 二进制日志&#xff08;BINLOG&#xff09;记录了所有的 DDL&#xff08;数据定义语言&#xff09;语句和 DML&#xff08;数据操纵语言&#xff09;语句&#xff0c;但不包括数据查询&#xff08;SELECT、SHOW&#xff09;语句。 复…

Java 小项目开发日记 03(文章分类接口的开发)

Java 小项目开发日记 03&#xff08;文章分类接口的开发&#xff09; 项目目录 配置文件&#xff08;pom.xml&#xff09; <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocat…

计算机网络:数据链路层知识点汇总

文章目录 一、数据链路层功能概述二、封装成帧和透明传输三、差错控制&#xff08;检错编码&#xff09;四、差错控制&#xff08;纠错编码&#xff09;五、流量控制与可靠传输机制六、停止-等待协议七、后退N帧协议&#xff08;GBN&#xff09;八、选择重传协议&#xff08;SR…

用户增长6步法

什么是用户增长&#xff1f; 通过痛点、产品、渠道、内容、技术、数据等要素实现用户的获取、激活、留存、变现、推荐&#xff0c;用户增长包含了产品出现前的用户增长、产品生产周期内的用户增长、产品生命周期外的用户增长三个阶段。 用户增长6步法&#xff1a;方法、模型和…

YOLOv8-TensorRT on Jetson

YOLOv8-TensorRT Jetson 项目地址&#xff1a;https://github.com/triple-Mu/YOLOv8-TensorRT/blob/main/docs/Jetson.md 文档地址&#xff1a;https://github.com/triple-Mu/YOLOv8-TensorRT/blob/main/docs/Jetson.md 注意 engine 文件不跨平台&#xff0c;只能在对应的平台…

docker mysql主从复制

新建主服务器容器实例3301 mysql 主 3301 docker run -p 3301:3306 --name mysql-master \ -v /mydata/mysql-master/log:/var/log/mysql \ -v /mydata/mysql-master/data:/var/lib/mysql \ -v /mydata/mysql-master/conf:/etc/mysql \ -v /home/mysql/mysql-files:/var/lib/…

MATLAB环境下使用相关图可视化相关矩阵

为了处理各行各业中出现的高维数据&#xff0c;迫切需要寻找适用的统计学方法。大维随机矩阵理论是处理高维数据的理论工具之一&#xff0c;在高维统计分析中&#xff0c;表现出良好的性能并有着广泛的应用。 二十世纪四十年代和五十年代初期&#xff0c;大维随机矩阵理论起源…

AI大模型 拍照搜题

最近&#xff0c;发现一款小程序【问智通】&#xff0c;实现了拍照搜题结合AI大模型&#xff0c;省去了打字和敲数学公式向AI提问&#xff0c;完美的补充了其它拍照搜题平台拍不到&#xff0c;没解析等不足&#xff01;&#xff01;&#xff01; 小程序码&#xff1a; APP下载…

【多模态】28、LLaVA 第一版 | Visual Instruction Tuning 多模态模型的指令微调

论文&#xff1a;Visual Instruction Tuning 代码&#xff1a;https://llava-vl.github.io/ 出处&#xff1a;NeurIPS 2023 Oral 系列工作&#xff1a;LLaVA-1.5、LLaVA-PLUS、LLaVA-Interactive、Video-LLaVA、LLaVA-Med 等&#xff0c;LLaVA 也是首次将指令学习引入多模态…

西门子WinCC冗余项目使用

1 如果需要使用WinCC冗余系统时&#xff0c;请仔细阅读下面的文档&#xff0c;它将解决以下几个问题&#xff1a; &#xff08;1&#xff09;WinCC冗余有什么样的功能&#xff1f; &#xff08;2&#xff09;需要购买什么样的授权&#xff1f; &#xff08;3&#xff09;应…

TypeScript 中命名空间与模块的区别

&#x1f469; 个人主页&#xff1a;不爱吃糖的程序媛 &#x1f64b;‍♂️ 作者简介&#xff1a;前端领域新星创作者、CSDN内容合伙人&#xff0c;专注于前端各领域技术&#xff0c;成长的路上共同学习共同进步&#xff0c;一起加油呀&#xff01; ✨系列专栏&#xff1a;前端…

H3C OSPF 外部路由引入实验

H3C OSPF 外部路由引入实验 实验拓扑 实验需求 按照图示配置 IP 地址R1&#xff0c;R2&#xff0c;R3 运行 OSPF 使内网互通&#xff0c;所有接口&#xff08;公网接口除外&#xff09;全部宣告进 Area 0&#xff1b;要求使用环回口作为 Router-id业务网段不允许出现协议报文…

请立刻停止编写 Dockerfiles 并使用 docker init

您是那种觉得编写 Dockerfile 和 docker-compose.yml 文件很痛苦的人之一吗&#xff1f; 我承认&#xff0c;我就是其中之一。 我总是想知道我是否遵循了 Dockerfile、 docker-compose 文件的最佳编写实践&#xff0c;我害怕在不知不觉中引入了安全漏洞。 但是现在&#xff0c…

【数据结构和算法初阶(C语言)】时间复杂度(衡量算法快慢的高端玩家,搭配例题详细剖析)

目录 1.算法效率 1.1如何衡量一个算法的好坏 1.2 算法的复杂度 2.主菜-时间复杂度 2.1 时间复杂度的概念 2.2 大O的渐进表示法 2.2.1算法的最好&#xff0c;最坏和平均的情况 3.经典时间复杂度计算举例 3.1计算冒泡排序的时间复杂度 3.2计算折半查找的时间复杂度 3.…

Vue3 学习笔记(Day5)

「写在前面」 本文为尚硅谷禹神 Vue3 教程的学习笔记。本着自己学习、分享他人的态度&#xff0c;分享学习笔记&#xff0c;希望能对大家有所帮助。推荐先按顺序阅读往期内容&#xff1a; 1. Vue3 学习笔记&#xff08;Day1&#xff09; 2. Vue3 学习笔记&#xff08;Day2&…

提升培训考试效率的系统设计策略

随着培训的重要性日益凸显&#xff0c;如何提升培训考试系统的效率成为了许多组织和机构关注的焦点。 一、设计自适应的考试界面 培训考试系统的界面应该能够自适应不同的屏幕尺寸和设备类型&#xff0c;如电脑、平板电脑和手机。采用响应式设计技术&#xff0c;确保考生在不同…

Leetcode115. 不同的子序列 -代码随想录

题目&#xff1a; 代码(首刷看解析 2024年2月29日&#xff09;&#xff1a; 不晓得这种超过int和long的测试案例是用来恶心谁的&#xff0c;用DP都没机会取模 class Solution { public:// 动态规划const int MOD 1000000007;int numDistinct(string s, string t) {long n s.…