转载Cubic拥塞控制算法进行简单分析

BIC是binary increase congestion contrl的缩写。不同拥塞控制算法的核心差异其实都体现在拥塞避免阶段。过去reno拥塞控制算法的主要缺点是增cwnd采用的方式是累加的线性增窗(AI,additive increase)。线性增窗主要缺点是:

  • 每经过一个RTO,cwnd才加1,如果RTO很长的话,需要很久才可以恢复到比较大的cwnd值,这样性能就不好了

BIC采binary increase的方式主要是在拥塞避免阶段采用二分查找的搜索来增加cwnd而不是像reno一样固定增加1

BIC原理

BIC相比reno的核心差别就是拥塞避免阶段增加cwnd的方式不同。BIC可以让cwnd尽快恢复到比较大的值。为了理解BIC,以下两个定义我们先需要理解:

  • Wmax: 发现丢包时(认为网络产生拥堵),此时我们的拥塞窗口定义成Wmax
  • Wmin: 乘法减小后的cwnd值

 

bic拥塞避免恢复cwnd的过程也是加法增加的,只不过每次不是增加1,而是采用二分查找的探测手段决定到底需要加多少。其cwnd的恢复过程如下。cwnd能以较快的方式接近Wmax并且尽量维持多的时间。BIC相比reno主要带来以下好处:Wmax收敛快,相比reno可以更快的收敛到Wmax

上图曲线右侧的过程是max cwnd的探测过程。BIC在cwnd超过Wmax以后,如果长时间未发生丢包,认为网络情况良好,则会慢慢增加cwnd的窗口取抢占更多带宽资源。

 

如何通过二分查找确定cwnd增加的值

就是不断递归 newWmin =(Wmax+Wmin)/2的过程即可:

BIC的缺点

正所谓,成也萧何败也萧何。快速收敛到Wmax在长RTT的情况下,是比较适合的,但是在短RTT的劣网环境下,激进的增窗会引发带宽的争抢,使得拥塞控制不具备公平性。公平性指的是新链路也可以像老链路一样公平的去获取带宽资源。显然BIC在短RTT劣网环境下会破坏这个公平性。

因为为了改进短RTT劣网环境下BIC公平性的问题,就引入了CUBIC

CUBIC的论文可以参考:CUBIC: A New TCPFriendly HighSpeed TCP Variant

 

 

原理:
在上一篇文章中也提到了拥塞控制算法的分类,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。

 

              CUBIC和BIC是类似的,同样也是乘法减小的过程,那么也就意味着t=0时刻,窗口是我们的Wmin,也就是β*Wmax,把t=0W(0)=β*Wmax带入,解出来                                               

以上,将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中。[提示:关于网络协议栈相关的核心结构体,以及之间的关系、转换请先阅读:Linux内核网络基础-TCP相关的几个关键结构体-小记],在拥塞控制的相关源码中会看到如下转换:
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;
}
若处于慢启动阶段,则按照如下算法对拥塞窗口进行增加。那么该函数返回的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
  如果 bic_target 小于等于当前拥塞窗口大小 cwnd,说明目标拥塞窗口大小已经小于或等于当前拥塞窗口大小,网络可能出现拥塞或者拥塞窗口已经很大了,
  此时不宜进一步增大拥塞窗口,因此需要减小增长速率。
ca->cnt = 100 * cwnd; 这一行计算了一个较小的增量值,用于控制拥塞窗口的增长速率,防止过度增长造成网络拥塞。
bictcp_cnt 的值,该值用于调整拥塞窗口的增长速率,值越大增长速率越小*/
 }

 /*
  * 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 */
    ......
    	/* The maximum rate of cwnd increase CUBIC allows is 1 packet per
	 * 2 packets ACKed, meaning cwnd grows at 1.5x per RTT.
	 */
	ca->cnt = max(ca->cnt, 2U);
}

/* 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)
{
	/* If credits accumulated at a higher w, apply them gently now. */
	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) {
		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);
}
每次发生丢包时,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;
  }
}

/*
 *	behave like Reno until low_window is reached,
 *	then increase congestion window slowly
 */
static u32 bictcp_recalc_ssthresh(struct sock *sk)
{
	const struct tcp_sock *tp = tcp_sk(sk);
	struct bictcp *ca = inet_csk_ca(sk);

	ca->epoch_start = 0;	/* end of epoch */

	/* Wmax and fast convergence */
	if (tp->snd_cwnd < ca->last_max_cwnd && fast_convergence)
		ca->last_max_cwnd = (tp->snd_cwnd * (BICTCP_BETA_SCALE + beta))
			/ (2 * BICTCP_BETA_SCALE);
	else
		ca->last_max_cwnd = tp->snd_cwnd;

	if (tp->snd_cwnd <= low_window)
		return max(tp->snd_cwnd >> 1U, 2U);
	else
		return max((tp->snd_cwnd * beta) / BICTCP_BETA_SCALE, 2U);
}
通过对这段代码的分析,再啰嗦一下对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友好型等,

 Algorithm 1: Linux CUBIC algorithm (v2.2) 

posted @   codestacklinuxer  阅读(588)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
历史上的今天:
2023-02-21 内核通用xdp
点击右上角即可分享
微信分享提示