深入解析Linux内核网络-拥塞控制系列(二)

news2025/1/25 1:30:36

上篇文章:深入解析Linux内核网络-拥塞控制系列(一)对Linux内核网络中网络拥塞框架的框架进行了分析。本次针对具体的Cubic拥塞控制算法进行简单分析在进行代码的梳理前,同样还是先来看一下相关概念、原理:

在上一篇文章中也提到了拥塞控制算法的分类,Cubic属于基于丢包反馈的拥塞控制算法,即将丢包视为发生了网络拥塞的标志。在众多讲解Cubic拥塞控制算法的技术博文中,都会先谈及BIC拥塞控制算法。之所以涉及BIC拥塞控制算法,因为Cubic是在BIC拥塞控制算法上进一步衍生出来的。本文先将BIC进行介绍。

BIC拥塞控制算法的核心思想是通过二分搜索的思想来找到当前链路适合的拥塞窗口大小。如下图所示是BIC拥塞控制算法的拥塞窗口图像。

显然当链路在网络上因为排队而发生丢包时,链路的最佳拥塞窗口肯定是小于丢包时的拥塞窗口的,那么把丢包时的拥塞窗口大小记为 。发生丢包后,BIC使用乘法因子 缩小网络拥塞窗口的大小,并记录缩小前的拥塞窗口值为 ,随后进入上图中类似二分法的探索阶段,即每收到一个ACK的时候,便将窗口设置到 和 in的中点,拥塞窗口增长到这个中间值且没有出现丢包的话,就说明网络还可以容纳更多的数据包。那么将当前这个中值设为新的最小值,按照二分法重新计算下一个增长点,一直持续到接近Wmax。如上图所示每一个RTT轮次进行一次拥塞窗口的增加。由于二分法探索的性质,当远离 时上升较快,接近 时上升缓慢。越接近 附近,拥塞窗口的增长速度越慢,意味着在发生一次拥塞丢包缩小拥塞窗口后,拥塞窗口的增长可以更快地探索到 ,而且可以在 附近停留较多的时间。但上述描述的BIC拥塞控制算法存在不公平性问题,具体地,拥塞控制窗口的大小与RTT的大小强相关(每一个RTT轮次进行一次类似二分法的拥塞窗口探索)。例如当网络中存在两个TCP连接,其中一个TCP连接的RTT为20ms,另一个是30ms,那么RTT=20ms的TCP连接拥有更高的拥塞窗口增长率。

为缓解BIC的RTT不公平性问题,提出Cubic拥塞控制算法,Cubic的解决方法比较直接,将类似二分法的拥塞窗口函数设计成了如下1.1的三次函数 。

函数大致图像如下图所示,需要值得关注的是,函数的横坐标是时间T,而不是RTT。具体地,函数表达式如1.1所示。函数中的C是一个常数,作为调节因子,t是最近一次检测到丢包后经过的时间(如果假设丢包后进入一个新的拥塞窗口探索轮次,那么t就是当前轮次的持续时间,也是自上次窗口减少到当前的时间)。K的取值如1.2所示,其中 是乘法减少因子, 是最近一次发生网络拥塞时的拥塞窗口值,K代表1.1函数在没有丢包的条件下,从当前拥塞窗口增长到 所要花费的时间。从1.1公式里也可以看出来,拥塞窗口的变化不再是由RTT强相关。在拥塞避免阶段接收到ACK时,Cubic在下一个RTT使用公式1.1计算拥塞窗口(W(t+RTT))作为Target。

(1.1)

(1.2)

以上,将BIC到Cubic的基本原理进行了概览,下面将切入到Linux内核源码中看Cubic拥塞控制算法的相关实现。在分析代码流程前,这里先把Cubic拥塞控制算法涉及的核心变量贴出来:

struct bictcp {
 u32 cnt;  /* increase cwnd by 1 after ACKs */
 u32 last_max_cwnd; /* last maximum snd_cwnd */
 u32 last_cwnd; /* the last snd_cwnd */
 u32 last_time; /* time when updated last_cwnd */
 u32 bic_origin_point;/* origin point of bic function */
 u32 bic_K;  /* time to origin point
       from the beginning of the current epoch */
 u32 delay_min; /* min delay (msec << 3) */
 u32 epoch_start; /* beginning of an epoch */
 u32 ack_cnt; /* number of acks */
 u32 tcp_cwnd; /* estimated tcp cwnd */
 u16 unused;
 u8 sample_cnt; /* number of samples to decide curr_rtt */
 u8 found;  /* the exit point is found? */
 u32 round_start; /* beginning of each round */
 u32 end_seq; /* end_seq of the round */
 u32 last_ack; /* last time when the ACK spacing is close */
 u32 curr_rtt; /* the minimum rtt of current round */
}

如上,结构体struct bictcp包含了上述讲到的Cubic拥塞控制算法的计算公式所涉及到的数值。struct bictcp作为拥塞控制中的核心结构体,嵌入在struct inet_connection_sock中。[提示:关于网络协议栈相关的核心结构体,以及之间的关系、在拥塞控制的相关源码中会看到如下转换:

struct sock *sk;
struct tcp_sock *tp = tcp_sk(sk);
struct bictcp *ca = inet_csk_ca(sk);

回到struct bictcp结构体上,内核源码的注释较清楚,这里就重点介绍四个重要的结构体成员。

第一个是epoch_start,当发生丢包后,进行拥塞窗口的乘法减小,即cwnd = *Wmax,同时也会重置epoch_start = 0。随即会进入一个探索拥塞窗口的新阶段,称该阶段为一个新的轮次,通过判断epoch_start是否被重置为0,若被重置,接下来会设置epoch_start = current_time,意味着正式进入一个新的拥塞窗口探索轮次,理解这个轮次是非常重要的。可以认为ecpoch_start的作用是为每个新轮次的打时间戳。

第二个是cnt,为Cubic算法中的极为重要的变量,cubic的核心函数的最终目的就是计算出cnt值,用来控制在拥塞避免状态阶段,何时才能增大拥塞窗口,具体实现是通过与struct tcp_sock中的snd_cwnd_cnt(snd_cwnd_cnt表示当前的拥塞窗口中已经发送,即经过对方ACK确认的数据段的个数)进行比较,决定是否增大拥塞窗口大小,可以认为cnt是增加一个单位cwnd需要的ACK数量。两者通过比较,共同完成拥塞窗口的增长控制。

第三个是bic_origin_point,代表新的链路饱和点,取MAX(last_max_cwnd,snd_cwnd),即取上一次丢包时的拥塞窗口大小与当前拥塞窗口的最大值。

第四个是bic_K,对应到公式中的K,源码中也有注释:time to origin point from the begining of the current epoch。表示在当前轮次内,假设没有丢包的情况下,从 *Wmax增长到链路饱和点bic_origin_point所要花费的时间,即bic_k决定了 *Wmax在没有进一步丢包的情况下到达bic_origin_point的时间。bic_K只会在进入每个新的轮次开始时进行计算。如下图所示,将bic_origin_point、epoch_start、bic_k描述在Cubic算法函数图像中。

在上一篇文章深入解析Linux内核网络-拥塞控制系列(一)中提到了拥塞控制算法的框架,Cubic算法实现的接口如下所示:

static struct tcp_congestion_ops cubictcp __read_mostly = {
 .init  = bictcp_init,
 .ssthresh = bictcp_recalc_ssthresh,
 .cong_avoid = bictcp_cong_avoid,
 .set_state = bictcp_state,
 .undo_cwnd = tcp_reno_undo_cwnd,
 .cwnd_event = bictcp_cwnd_event,
 .pkts_acked     = bictcp_acked,
 .owner  = THIS_MODULE,
 .name  = "cubic",
};

先着重看一下cong_avoid接口的实现,如下所示为该接口的实现,其中参数acked代表当前新的已确认的数据包。tcp_is_cwnd_limited函数是用于判断TCP连接是否受到拥塞窗口的限制,即检查发出去,但是还、有收到ACK的包是否达到了拥塞窗口的上限,可以暂且先不关注这个函数。重点看tcp_slow_start和bictcp_update、tcp_cong_avoid_ai函数。

static void bictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
 struct tcp_sock *tp = tcp_sk(sk);
 struct bictcp *ca = inet_csk_ca(sk);

 if (!tcp_is_cwnd_limited(sk))
  return;

 if (tcp_in_slow_start(tp)) {
  ......
  acked = tcp_slow_start(tp, acked);
  if (!acked)
   return;
 }
 bictcp_update(ca, tp->snd_cwnd, acked); /*计算一个ca->cnt出来 */
 tcp_cong_avoid_ai(tp, ca->cnt, acked);  /* 通过 计算的ca->cnt 进行拥塞控制 控制窗口cwnd的增长 */
}

tcp_in_slow_start函数通过判断是否处于慢启动区域,如下所示,通过比较当前的拥塞窗口大小与慢启动门限值,进而判断是否处于慢启动阶段。

static inline bool tcp_in_slow_start(const struct tcp_sock *tp)
{
 return tp->snd_cwnd < tp->snd_ssthresh;
}

 资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linuxc/c++高级开发【直播公开课】

零声白金VIP体验卡:零声白金VIP体验卡(含基础架构/高性能存储/golang/QT/音视频/Linux内核)

若处于慢启动阶段,则按照如下算法对拥塞窗口进行增加。那么该函数返回的acked如何理解?acked:刚刚被确认的数据包数量,且还未用来更新到拥塞窗口的剩余大小。如果在tcp_slow_start的慢启动流程中,acked值最终消耗变为0,那么说明刚刚确认的数据包均用在了慢启动环节的拥塞窗口增长上。当前还未退出慢启动阶段。

u32 tcp_slow_start(struct tcp_sock *tp, u32 acked)
{
   //在慢启动阶段,cwnd最多增加到慢启动门限值
 u32 cwnd = min(tp->snd_cwnd + acked, tp->snd_ssthresh);
   //acked用于更新cwnd,acked已使用cwnd-tp->snd_cwnd,更新acked
 acked -= cwnd - tp->snd_cwnd;
    //snd_cwnd_clamp; /* Do not allow snd_cwnd to grow above this */
 tp->snd_cwnd = min(cwnd, tp->snd_cwnd_clamp);

 return acked;
}

若在慢启动阶段返回的acked不为0,说明在慢启动阶段,刚刚确认的数据包已经足够使得拥塞窗口大小超过慢启动门限值,足以退出慢启动,剩余的acked用于拥塞避免阶段的增长。此时进入bictcp_update函数中,该函数也是Cubic的核心函数,函数的第一个参数是存放的Cubic拥塞控制算法相关变量信息的结构体,第二个参数是当前的拥塞窗口大小,第三个参数是在上面也介绍过:acked:刚刚被确认的数据包数量,且还未用来更新到拥塞窗口的剩余大小。这里要注意第三个参数acked可能是由慢启动环节中还未消耗掉的剩余量,也有可能是拥塞避免阶段的。

算法的实现过程中涉及到很多巧妙的设计,这里暂且不去关注这些细节,我们重点来看cubic三次函数的计算过程:

static inline void bictcp_update(struct bictcp *ca, u32 cwnd, u32 acked)
{
 u32 delta, bic_target, max_cnt;
 u64 offs, t;

 ca->ack_cnt += acked; /* count the number of ACKed packets */
    ......
 ca->last_cwnd = cwnd;   /* 更新上一次的拥塞窗口值 */
 ca->last_time = tcp_jiffies32; /* 最后一次更新last_cwnd的时间 */

 if (ca->epoch_start == 0) {  /*开始一个新的epoch */
  ca->epoch_start = tcp_jiffies32; /* record beginning */
  ca->ack_cnt = acked;   /* start counting */
  ca->tcp_cwnd = cwnd;   /* syn with cubic */

  if (ca->last_max_cwnd <= cwnd) {
   ca->bic_K = 0;
   ca->bic_origin_point = cwnd;
  } else {
   /* Compute new K based on
    * (wmax-cwnd) * (srtt>>3 / HZ) / c * 2^(3*bictcp_HZ)
    */
   ca->bic_K = cubic_root(cube_factor
            * (ca->last_max_cwnd - cwnd));
   ca->bic_origin_point = ca->last_max_cwnd;
  }
 }

 t = (s32)(tcp_jiffies32 - ca->epoch_start); /*当前时间到epoch_start的时间*/
 t += msecs_to_jiffies(ca->delay_min >> 3);  /* + ca->delay_min  预测下一个rtt时间内的cwnd */
 /* change the unit from HZ to bictcp_HZ */
 t <<= BICTCP_HZ;
 do_div(t, HZ);

 if (t < ca->bic_K)  /* t - K */
  offs = ca->bic_K - t;
 else
  offs = t - ca->bic_K;

 /* c/rtt * (t-K)^3 */
 delta = (cube_rtt_scale * offs * offs * offs) >> (10+3*BICTCP_HZ);
 if (t < ca->bic_K)                            /* below origin  */
  bic_target = ca->bic_origin_point - delta;
 else                                          /* above origin* /
  bic_target = ca->bic_origin_point + delta;

 /* cubic function - calc bictcp_cnt*/
 if (bic_target > cwnd) {
  ca->cnt = cwnd / (bic_target - cwnd);
 } else {
  ca->cnt = 100 * cwnd;              /* very small increment*/
 }

 /*
  * The initial growth of cubic function may be too conservative
  * when the available bandwidth is still unknown.
  */
 if (ca->last_max_cwnd == 0 && ca->cnt > 20)
  ca->cnt = 20; /* increase cwnd 5% per RTT */
    ......
}

每次发生丢包时,epoch_start会重置为0,意味着将开始一个新的轮次、新的时段。丢包后调到bictcp_update函数时,设置epoch_start为当前时间,正式开始新的轮次,并开始更新 、K值。所以可以认为epoch_start是为丢包后的每个新轮次的开始打上时间戳。如下代码片段所示,若当前的拥塞控制窗口大于 ,那么更新当前链路的拥塞窗口的饱和点为 (即当前达到了新的饱和点),此时设置K为0(ca->bic_K),k=0代表当前拥塞窗口的大小就是饱和点,达到饱和点的时间是0。若当前拥塞窗口小于 ,更新当前链路的拥塞窗口饱和点为 ,并且计算出K,此时的K按照公式1.2进行计算当前拥塞窗口在没有丢包的情况下到达饱和点的时间。

if (ca->last_max_cwnd <= cwnd) {
   ca->bic_K = 0;
   ca->bic_origin_point = cwnd;
  } else {
   /* Compute new K based on
    * (wmax-cwnd) * (srtt>>3 / HZ) / c * 2^(3*bictcp_HZ)
    */
   ca->bic_K = cubic_root(cube_factor
            * (ca->last_max_cwnd - cwnd));
   ca->bic_origin_point = ca->last_max_cwnd;
  }
}

通过对这段代码的分析,再啰嗦一下对K的理解,K:time to origin point from the begining of the current epoch; orign point:Cubic函数的中心点,也是饱和点(bic_origin_point)对应的位置,故ca->bic_K代表的是从当前轮次开始对应的拥塞窗口,在没有进一步丢包的情况下,到达饱和点bic_origin_point所需要的时间(换句话就是:在当前轮次内,假设没有丢包的情况下,从当前拥塞窗口增长 *Wmax到链路饱和点bic_origin_point所要花费的时间)。下一步开始计算时间t,首先计算当前时间到当前轮次开始的时间,然后加下一个RTT的时间(由于下一个RTT时间是不确定的值,故在具体实现时是选择的rtt的平均值),t = 当前时间-进入当前轮次的时间+最小RTT。这里最终的t是一个预测时间。因为我们的目标就是要计算下个RTT时间的拥塞窗口大小,即保证在下一次ACK到达之前,有充足的cwnd配额可供持续发送数据。

 t = (s32)(tcp_jiffies32 - ca->epoch_start); /*当前时间到epoch_start的时间*/
 t += msecs_to_jiffies(ca->delay_min >> 3);  /* + ca->delay_min  

如下图所示,是时间t在cubic三次函数中的描述,蓝色括号代表当前轮次已经经过的时间,红色的是下一个rtt的时间。

上文提到的cubic计算公式,如下所示,t、K、Wmax、C均已知。

下面开始计算目标值,首先判断时间t,计算|t-K|的值,从而计算C*(t-K)^3;然后根据t与K的大小,即当前的t是否超过了到达饱和点的时间,对应的是Cubic函数图像的中心位置。计算最终的目标值,是预测的下个rtt拥塞窗口值,即保证下个时间段内有足够的cwnd配额可供持续发送数据。

if (t < ca->bic_K)  /* t - K */
  offs = ca->bic_K - t;
 else
  offs = t - ca->bic_K;

 /* c/rtt * (t-K)^3 */
 delta = (cube_rtt_scale * offs * offs * offs) >> (10+3*BICTCP_HZ);
 if (t < ca->bic_K)                            /* below origin  */
  bic_target = ca->bic_origin_point - delta;
 else                                          /* above origin* /
  bic_target = ca->bic_origin_point + delta;

最后根据当前拥塞窗口大小、目标值获取最终的cnt值,cnt值是cubic拥塞算法的核心,主要用来控制在拥塞避免状态时,什么时候才能增大拥塞窗口。具体取值时,按照cubic函数的形状:target与cwnd相差越多,增长越快,如果当前cwnd已经超出预期的target,应该做降速,所以此时取值cnt为100*cwnd。(关于cnt的定义在本文介绍cubic相关变量时有做特别说明)。

/* cubic function - calc bictcp_cnt*/
 if (bic_target > cwnd) {
  ca->cnt = cwnd / (bic_target - cwnd);
 } else {
  ca->cnt = 100 * cwnd;              /* very small increment*/
 }

bictcp_update函数最终要计算的就是上面的ca->cnt,当执行完bictcp_update后,顺序执行tcp_cong_avoid_ai函数,如下所示,函数第二个参数就是bictcp_update计算后的ca->cnt值。下面函数中tp->snd_cwnd_cnt是一个核心变量,文章前面也介绍过,代表当前的拥塞窗口中已经发生(经过对方ACK确认)的数据段的个数,该变量在下面函数中与ca->cnt进行对比,来决定是否增大拥塞窗口大小。

/* In theory this is tp->snd_cwnd += 1 / tp->snd_cwnd (or alternative w),
 * for every packet that was ACKed.
 */
void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w, u32 acked)
{
    
    /*
    tp->snd_cwnd_cnt 表示在当前的拥塞窗口中已经发送(经过对方ack包确认)的数据段个数.
    ca->cnt = w :它是cubic拥塞算法的核心,主要用来控制拥塞避免状态的时候,什么时候才能增大拥塞窗口
    具体实现是通过比较cnt和snd_cwnd_cnt,来决定是否增大拥塞窗口
    */
    
    /* If credits accumulated at a higher w, apply them gently now. */
    /*当被确认的包的数量大于w时,将snd_cwnd_cnt清0,继续加大拥塞窗口值,继续probe Wmax*/
    if (tp->snd_cwnd_cnt >= w) {
        tp->snd_cwnd_cnt = 0;
        tp->snd_cwnd++;
    }
 
    tp->snd_cwnd_cnt += acked; //累计被确认的包
    if (tp->snd_cwnd_cnt >= w) {
        /*按比例增加拥塞窗口,并减少snd_cwnd_cnt*/
        u32 delta = tp->snd_cwnd_cnt / w;
 
        tp->snd_cwnd_cnt -= delta * w;
        tp->snd_cwnd += delta;
    }
    tp->snd_cwnd = min(tp->snd_cwnd, tp->snd_cwnd_clamp);
}

至此,Cubic拥塞控制算法在Linux内核的核心计算过程分析结束,除了本文涉及到的分析要点外,Cubic拥塞控制算法还有很多其他的思想,例如混合慢启动、快速收敛、TCP友好型等,后面有机会也进行几次分析分享。本文若有描述不恰当、有误的地方,欢迎批评指正!

原文作者:技术简说

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

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

相关文章

TypeScript入门实战笔记 -- 开篇 为什么要选择 TypeScript ?

typescript 在线编辑器http://typescript.p2hp.com/play?#code/JYOwLgpgTgZghgYwgAgJIFUDO1Uhge2QG8AoZc5YAEwC5kQBXAWwCNoBuMikOJiOzGCigA5pwrI4ANzhg4UAPwChozgF8SmmAxAIwwfCGRYcefAAoADlHyXMdDNii4CASmJdyCQ5nwAbCAA6P3wRKxs7ABpkAHJrW0wY1xINEhNnM3MiSlpkAEZonj46GIBrROQ1…

PHP 二维码内容解析、二维码识别

目录 1.首先是一些错误的示例 2.正确示例 3.二维码解析 4.完整示例&#xff0c;含生成 5.代码执行结果 6.参考文档 1.首先是一些错误的示例 本示例使用的是php7.3 通过搜索各种结果逐个尝试以后&#xff0c;得出一个可使用版本 解析错误经历&#xff1a;vendor核心报错 …

m_map绘图添加遥感图片

在matlab下的m_map可以绘制地形图。那么&#xff0c;如何在m_map添加卫星遥感图片呢&#xff1f;这需要使用m_image函数。 基本语法为&#xff1a; m_image(lonlim,latlim,c)其中lonlim和latlim是这个图像地理坐标&#xff08;经纬度&#xff09;的边界&#xff0c;是一个长度…

机器学习实验四:贝叶斯分类器

系列文章目录 机器学习实验一&#xff1a;线性回归机器学习实验二&#xff1a;决策树模型机器学习实验三&#xff1a;支持向量机模型机器学习实验四&#xff1a;贝叶斯分类器机器学习实验五&#xff1a;集成学习机器学习实验六&#xff1a;聚类 文章目录 系列文章目录一、实验…

ubuntu22.04 安装cuda

CUDA&#xff08;Compute Unified Device Architecture&#xff09;是由 NVIDIA 开发的一种并行计算平台和编程模型。它允许开发者利用 NVIDIA 的 GPU&#xff08;图形处理单元&#xff09;进行高效的计算处理。CUDA 通过提供一系列的 C、C 和 Fortran 扩展&#xff0c;使得开发…

谈谈对OOA、OOD、OOP理解

1 前言 按照开发阶段排序&#xff0c;有如下排序&#xff1a; OOA阶段&#xff1a;面向对象分析&#xff0c;此阶段领域建模&#xff0c;需求分析。OOD阶段&#xff1a;面向对象设计&#xff0c;此阶段输出系统概要设计、系统详细设计。OOP阶段&#xff1a;面向对象编程&#…

Android引用SDK包实现高德地图展示

一、准备工作 注册高德地图开放平台 注册过程我就不多说了&#xff0c;挺简单的&#xff0c;需要登录&#xff0c;然后注册成为开发者&#xff0c;还需要支付宝认证、手机号码验证、邮箱验证挺多的&#xff0c;但是速度很快。基本上随时验证随时注册成功。新建应用新建…

C# | 对比不同种类的锁

文章目录 C# 对比不同种类的锁异同点对比表使用方法lock语句Monitor类Mutex类Semaphore类ReaderWriterLock类 结语 C# 对比不同种类的锁 Hi&#xff0c;在C#编程中&#xff0c;想要保护共享资源&#xff0c;通常会用到各种类型的锁。今天我们就来一起看看C#中不同种类的锁&…

UE4.27-UE5.1设置打包Android环境

打包Android配置文件 1. 配置打包Android的SDK需求文件位于下面文件中&#xff1a; 2. 指定了对应的SDK环境变量名字以及NDK需求等&#xff1a; UE4.27-UE5.1--脚本自动配置 安装前提 1. 务必关闭虚幻编辑器和Epic Games Launcher&#xff0c;以确保NDK组件的安装或引擎环境…

ubuntu 20.04.6 server 服务器 下载与安装

下载地址&#xff1a;https://releases.ubuntu.com/20.04.6/ubuntu-20.04.6-live-server-amd64.iso 第一步&#xff1a; 准备U盘&#xff0c;使用软碟通将下载好的镜像写入到U盘中 软碟通网址&#xff1a;https://www.cn.ultraiso.net/xiazai.html 点击&#xff1a;文件 ->…

unity 2d 入门 飞翔小鸟 Cinemachine 记录分数(十二)

1、创建文本 右键->create->ui->leagcy->text 2、设置字体 3、设置默认值和数字 4、当切换分辨率&#xff0c;分数不见问题 拖拽这里调整 调整到如下图 5、编写得分脚本 using System.Collections; using System.Collections.Generic; using UnityEngine; …

MVC、MVP、MVVM模式的区别

前言&#xff1a;这三个表现层框架设计模式是依次进化而形成MVC—>MVP—>MVVM。在以前传统的开发模式当中即MVC模式&#xff0c;前端人员只负责Model&#xff08;数据库&#xff09;、 View&#xff08;视图&#xff09;和 Controller /Presenter/ViewModel&#xff08;控…

Java程序设计实验6 | 集合类

*本文是博主对Java各种实验的再整理与详解&#xff0c;除了代码部分和解析部分&#xff0c;一些题目还增加了拓展部分&#xff08;⭐&#xff09;。拓展部分不是实验报告中原有的内容&#xff0c;而是博主本人自己的补充&#xff0c;以方便大家额外学习、参考。 &#xff08;解…

LeetCode算法题解(单调栈)|LeetCode503. 下一个更大元素 II、LeetCode42. 接雨水

一、LeetCode503. 下一个更大元素 II 题目链接&#xff1a;503. 下一个更大元素 II 题目描述&#xff1a; 给定一个循环数组 nums &#xff08; nums[nums.length - 1] 的下一个元素是 nums[0] &#xff09;&#xff0c;返回 nums 中每个元素的 下一个更大元素 。 数字 x 的…

【Web渗透】渗透测试简介

基本介绍 渗透测试(Penetration Testing)是一种通过模拟恶意攻击者的技术与手法对目标系统在可控制的范围内进行安全测试和安全评估的过程&#xff0c;其目的是在挖掘当前系统潜在的安全风险点后对系统进行安全升级来提升系统的安全性&#xff0c;并以此来规避被恶意攻击者入侵…

【C语言】操作符详解(二)

目录 移位操作符 左移操作符 右移操作符 位操作符:&、|、^、~ 一道面试题 移位操作符 <<左移操作符 >>右移操作符注:移位…

2023年【R2移动式压力容器充装】考试试卷及R2移动式压力容器充装实操考试视频

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 R2移动式压力容器充装考试试卷是安全生产模拟考试一点通生成的&#xff0c;R2移动式压力容器充装证模拟考试题库是根据R2移动式压力容器充装最新版教材汇编出R2移动式压力容器充装仿真模拟考试。2023年【R2移动式压力…

WGCLOUD v3.5.0 新增支持监测交换机的接口状态UP DOWN

WGCLOUD v3.5.0开始 可以监测交换机或SNMP设备的接口状态了&#xff0c;直接上图

浅析以太网接口及串口转以太网技术

浅析以太网接口 以太网相关接口主要包括&#xff1a;MII/RMII/SMII以及GMII/RGMII/SGMII接口。 一、MII接口 MII&#xff08;Media Independent Interface&#xff09;介质无关接口或称为媒体独立接口&#xff0c;它是IEEE-802.3定义的以太网行业标准。它包括一个数据接口和…

《安富莱嵌入式周报》第328期:自主微型机器人,火星探测器发射前失误故障分析,微软推出12周24期免费AI课程,炫酷3D LED点阵设计,MDK5.39发布

周报汇总地址&#xff1a;嵌入式周报 - uCOS & uCGUI & emWin & embOS & TouchGFX & ThreadX - 硬汉嵌入式论坛 - Powered by Discuz! 更新一期视频教程&#xff1a; 【实战技能】 单步运行源码分析&#xff0c;一期视频整明白FreeRTOS内核源码框架和运行…