基于linux5.0 tcp_cubic.c代码

Linux提供了丰富的拥塞控制算法,这些算法包括vegas、reno、HSCTP、BIC、CUBIC等等。

reno/BIC/CUBIC算法原理和对比参考:https://blog.csdn.net/dai_xiangjun/article/details/119181521

CUBIC拥塞控制基础

tcp_sock函数使用到的拥塞控制变量如下:

snd_cwnd: 拥塞控制窗口的大小

snd_ssthresh: 慢启动门限,如果snd_cwnd值小于此值这处于慢启动阶段。

snd_cwnd_cnt: 当超过慢启动门限时,该值用于降低窗口增加的速率

snd_cwnd_clamp: snd_cwnd能够增加到的最大尺寸

snd_cwnd_stamp: 拥塞控制窗口有效的最后一次时间戳

snd_cwnd_used: 用于标记在使用的拥塞窗口的高水位值,当tcp连接的数量被应用程序限制而不是被网络限制时,该变量用于下调snd_cwnd值。

linux也支持用户空间动态插入拥塞控制算法,通过tcp_cong.c注册,拥塞控制使用的函数通过向tcp_register_congestion_control传递tcp_congestion_ops实现,用户插入的拥塞控制算法需要支持ssthresh和con_avoid.

当前Linux系统使用的拥塞控制算法取决于sysctl接口的net.ipv4.tcp_congestion_control。缺省的拥塞控制算法是最后注册的算法(LIFO),如果全部编译成模块,则将使用reno算法,如果使用缺省的Kconfig配置,CUBIC算法将编译进内核(不是编译成module),并且内核将使用CUBIC作为默认的拥塞控制算法。

CUBIC使用的算法:

窗口增长函数:

C是cubic参数,t是自上一次窗口减少的时间,K是上述函数在没有丢包时从W增加到Wmax所花费的时间周期。其计算公式是:

在拥塞避免阶段接收到ACK时,CUBIC在下一个RTT使用公式1计算窗口增长率。其将W(t+RTT)设置成拥塞窗口大小。

根据当前拥塞窗口大小,CUBIC有三种状态

  1. TCP状态(t时刻窗口小于标准TCP窗长)

  2. 凸区域(拥塞窗口小于Wmax)

  3. 凹区域(拥塞窗口大于Wmax)

CUBIC慢启动门限阈值

问题:

1.什么时候初始化ssthresh?

在tcp_init_sock()函数中初始化ssthresh为0x7fffffff

2.什么时候更新ssthresh?

在收到ack时会调用bictcp_acked()函数更新ssthresh值,所以我们可以明确,hystart的作用是更新ssthresh, 但是需要满足三个条件:

  • 开启混合慢启动,即hystart值设置为1

  • 满足当前窗口snd_cwnd小于snd_ssthresh,即处于慢启动阶段

  • 并且当前窗口snd_cwnd应该不小于最低窗口16(hystart_low_window)

注意在tcp_init_sock()函数中的时候snd_cwnd值为10,明显小于16,不会进入混合慢启动,那么这时候snd_cwnd是谁在更新呢?

是在tcp_slow_start()函数中更新的snd_cwnd值,以下是第一次更新snd_cwnd的调用关系:

tcp_v4_do_rcv()->tcp_rcv_established()->tcp_ack()->tcp_cong_control()->tcp_cong_avoid()->bictcp_cong_avoid()->tcp_slow_start()

第一次更新后snd_cwnd的值变为了20

3.如何更新ssthresh?

更新ssthresh的函数是hystart_update()。此函数的调用关系

tcp_v4_do_rcv()->tcp_rcv_established()->tcp_ack()->tcp_clean_rtx_queue()->bictcp_acked()->hystart_update()

在hystart_update()函数里面是寻找ssthresh的最佳值。如何找到:

1.TRAIN方法:

  • 大前提:每一次寻找ssthresh的时间必须在2ms内,如果超过2毫秒,则会用DELAY方法

  • 第一次开始时记录TRAIN开始的时间为round_start(注意这里还记录一个和round_start相同的时间last_ack);

  • 当收到ACK后,进入hystart_update 记录时间为now,now减去last_ack必须小于等于2ms,如果不小于2ms说明rtt时间太长了,网络拥塞,TRAIN方法不适合,需要用DELAY方法。如果小于2ms,先更新lask_ack时间为now,然后判断now-round_start是否小于最小rtt/2,如果条件满足则更新snd_ssthresh

2.DELAY方法:

  • 收集8个样本,如果已经采集了HYSTART_MIN_SAMPLES个报文,并且,采样到的curr_rtt值大于最小的RTT值加上1/8倍的最小RTT值,即当curr_rtt的值大于9/8倍的最小RTT(delay_min)时,认为延迟增加过大,退出SlowStart,将当前拥塞窗口设置为sshresh

该方法在快速和长距离网络上使用立方函数修改拥塞线性窗口。该方法使用窗口的增加独立于RTT(round trip times),这使得具有不同RTT的流具有相对均等的网络带宽。到达稳定阶段,CUBIC在带宽延迟积(BDP bandwith and delay product)较大时具有很好的扩展性、稳定性和公平性。立方根计算方法Newton-Raphson,误差约为0.195%

首先找到慢启动门限值snd_ssthresh,在tcp套接字初始化时tcp_prot的init成员会被调用,该函数直接指向tcp_v4_init_sock()

[net/ipv4/tcp_ipv4.c]
/* NOTE: A lot of things set to zero explicitly by call to*       sk_alloc() so need not be done here.*/
static int tcp_v4_init_sock(struct sock *sk)
{struct inet_connection_sock *icsk = inet_csk(sk);tcp_init_sock(sk);icsk->icsk_af_ops = &ipv4_specific;#ifdef CONFIG_TCP_MD5SIGtcp_sk(sk)->af_specific = &tcp_sock_ipv4_specific;
#endifreturn 0;
}struct proto tcp_prot = {.name           = "TCP",.owner          = THIS_MODULE,.close          = tcp_close,.pre_connect        = tcp_v4_pre_connect,.connect        = tcp_v4_connect,.disconnect     = tcp_disconnect,.accept         = inet_csk_accept,.ioctl          = tcp_ioctl,.init           = tcp_v4_init_sock,.destroy        = tcp_v4_destroy_sock,......
};

tcp_init_sock()用于初始化套接字,由于sk_alloc函数在为套接字分配内存时,已经将一些变量的初始值设置为了0,所以tcp_init_sock并没有初始化所有变量

[net/ipv4/tcp.c]

/* Address-family independent initialization for a tcp_sock.** NOTE: A lot of things set to zero explicitly by call to*       sk_alloc() so need not be done here.*/
void tcp_init_sock(struct sock *sk)
{struct inet_connection_sock *icsk = inet_csk(sk);/*tcp_sock的结构体中包含了拥塞控制所需的各种变量*/struct tcp_sock *tp = tcp_sk(sk);/*存放乱序TCP包的套接字链表初始化*/tp->out_of_order_queue = RB_ROOT;sk->tcp_rtx_queue = RB_ROOT;/*重传、延迟ack以及探测定时器初始化*/tcp_init_xmit_timers(sk);INIT_LIST_HEAD(&tp->tsq_node);INIT_LIST_HEAD(&tp->tsorted_sent_queue);//重传超时值,起始设置为1sicsk->icsk_rto = TCP_TIMEOUT_INIT;//mdev 用于RTT测试的均方差tp->mdev_us = jiffies_to_usecs(TCP_TIMEOUT_INIT);minmax_reset(&tp->rtt_min, tcp_jiffies32, ~0U);/*初始拥塞窗口大小,初始值10,这意味着窗口长度在大于10时才会进入拥塞算法,而一开始进入的是慢启动阶段*/tp->snd_cwnd = TCP_INIT_CWND;/* There's a bubble in the pipe until at least the first ACK. */tp->app_limited = ~0U;/* See draft-stevens-tcpca-spec-01 for discussion of the* initialization of these values.*//*慢启动门限值0x7fffffff*/tp->snd_ssthresh = TCP_INFINITE_SSTHRESH;//拥塞窗口最大长度tp->snd_cwnd_clamp = ~0;//MMS,初始值设置为536,不包括SACKStp->mss_cache = TCP_MSS_DEFAULT;tp->reordering = sock_net(sk)->ipv4.sysctl_tcp_reordering;tcp_assign_congestion_control(sk);tp->tsoffset = 0;tp->rack.reo_wnd_steps = 1;//套接字当前状态,sysctl_tcp_rmen[1]对应的是default,[0]是min,[1]最大值sk->sk_state = TCP_CLOSE;sk->sk_write_space = sk_stream_write_space;sock_set_flag(sk, SOCK_USE_WRITE_QUEUE);icsk->icsk_sync_mss = tcp_sync_mss;//发送和接收buffersk->sk_sndbuf = sock_net(sk)->ipv4.sysctl_tcp_wmem[1];sk->sk_rcvbuf = sock_net(sk)->ipv4.sysctl_tcp_rmem[1];sk_sockets_allocated_inc(sk);sk->sk_route_forced_caps = NETIF_F_GSO;
}

CUBIC算法文件:[net/ipv4/tcp_cubic.c]

CUBIC算法慢启动门限ssthresh在两种情况下会得到更新

  1. 在受到ack应答包,处理函数bictcp_acked()

  2. 发生拥塞时,慢启动门限回退,处理函数bictcp_recalc_ssthresh()

static struct tcp_congestion_ops cubictcp __read_mostly = {/*CUBIC算法变量初始化,在tcp三次握手时,回调其初始化套接字的拥塞控制变量*/.init       = bictcp_init,/*拥塞时慢启动门限回退计算*/.ssthresh   = bictcp_recalc_ssthresh,.cong_avoid = bictcp_cong_avoid, //拥塞控制//如果拥塞状态是TCP_CA_Loss,重置拥塞算法CUBIC的各种变量.set_state  = bictcp_state,.undo_cwnd  = tcp_reno_undo_cwnd,.cwnd_event = bictcp_cwnd_event,.pkts_acked     = bictcp_acked,.owner      = THIS_MODULE,.name       = "cubic",
};

收到ack包的函数调用关系,最终调用到bictcp_acked()函数

int tcp_v4_rcv(struct sk_buff *skb)int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)static int tcp_clean_rtx_queue(struct sock *sk, u32 prior_fack,u32 prior_snd_una,struct tcp_sacktag_state *sack)static void bictcp_acked(struct sock *sk, const struct ack_sample *sample)

tcp_clean_rtx_queue()将已经收到应答包的帧从重传队列删除,在这个函数的末尾调用bictcp_acked()更新慢启动门限值。

/* Track delayed acknowledgment ratio using sliding window* ratio = (15*ratio + sample) / 16*/
static void bictcp_acked(struct sock *sk, const struct ack_sample *sample)
{const struct tcp_sock *tp = tcp_sk(sk);struct bictcp *ca = inet_csk_ca(sk);u32 delay;/* Some calls are for duplicates without timetamps */if (sample->rtt_us < 0)return;/* Discard delay samples right after fast recovery */if (ca->epoch_start && (s32)(tcp_jiffies32 - ca->epoch_start) < HZ)return;delay = (sample->rtt_us << 3) / USEC_PER_MSEC;if (delay == 0)delay = 1;/* first time call or link delay decreases *//*变量delay_min保存最小的RTT值*/if (ca->delay_min == 0 || ca->delay_min > delay)ca->delay_min = delay;/* hystart triggers when cwnd is larger than some threshold *//*1.混合慢启动标志hystart默认是开启的,2.当前窗口长度snd_cwnd应该满足小于snd_ssthresh3.在拥塞窗口大于等于hystart定义的最低窗口值16(hystart_low_window)时,hystart才开始执行*/if (hystart && tcp_in_slow_start(tp) &&tp->snd_cwnd >= hystart_low_window)hystart_update(sk, delay);
}

起始时慢启动门限值设置成了很大的0x7fffffff, snd_cwnd会一直增加直到大于等于hystart_low_windows(16)时,将调用hystart_update更新慢启动门限值。

static void hystart_update(struct sock *sk, u32 delay)
{struct tcp_sock *tp = tcp_sk(sk);struct bictcp *ca = inet_csk_ca(sk);/*如果found设置了HYSTART_ACK_TRAIN或HYSTART_DELAY标志位,这里依据启动的hystart检测方式,即hystart_detect的值,表明已经找到SlowStart的退出点,不在执行检测*/if (ca->found & hystart_detect)return;//如果启用了HYSTART_ACK_TRAIN检测方式if (hystart_detect & HYSTART_ACK_TRAIN) {/*now相当于当前ACK报文的时间戳*/u32 now = bictcp_clock();/* first detection parameter - ack-train detection *//*变量hystart_ack_delta的默认值为2毫秒。如果现在的时间戳now减去上一次的时间戳小于等于设定的ACK-train间隔(hystart_ack_delta),更新lask_ack的时间戳为当前ACK报文时间戳*/if ((s32)(now - ca->last_ack) <= hystart_ack_delta) {ca->last_ack = now;/*如果当前时间减去此轮ACK-train测量开始时间戳roud_start,大于最小RTT延迟的一半(delay_min保存了8倍的最小RTT延时,参考bictcp_acked函数),退出慢启动,将当前拥塞窗口设置为ssthresh*/if ((s32)(now - ca->round_start) > ca->delay_min >> 4) {ca->found |= HYSTART_ACK_TRAIN;NET_INC_STATS(sock_net(sk),LINUX_MIB_TCPHYSTARTTRAINDETECT);NET_ADD_STATS(sock_net(sk),LINUX_MIB_TCPHYSTARTTRAINCWND,tp->snd_cwnd);tp->snd_ssthresh = tp->snd_cwnd;}}}//DELAY方式防止延迟过大,因为上面的ACK-TAIN方式是保证RTT在2毫秒之内才会有作用/*如果启用了HYSTART_DELAY检测方式*/if (hystart_detect & HYSTART_DELAY) {/* obtain the minimum delay of more than sampling packets *//*当前采样的数量小于hystart(HYSTART_MIN_SAMPLES = 8)*/if (ca->sample_cnt < HYSTART_MIN_SAMPLES) {/*curr_rtt默认值就是0。如果当前测量轮次的curr_rtt大于当前报文RTT,更新curr_rtt,所以curr_rtt是本轮次中最小的RTT*/if (ca->curr_rtt == 0 || ca->curr_rtt > delay)ca->curr_rtt = delay;ca->sample_cnt++;} else {/*如果已经采集了HYSTART_MIN_SAMPLES个报文,并且,采样到的curr_rtt值大于最小的RTT值加上1/8倍的最小RTT值,即当curr_rtt的值大于9/8被的最小RTT(delay_min)时,认为延迟增加过大,退出SlowStart,将当前拥塞窗口设置为sshresh*/if (ca->curr_rtt > ca->delay_min +HYSTART_DELAY_THRESH(ca->delay_min >> 3)) {ca->found |= HYSTART_DELAY;NET_INC_STATS(sock_net(sk),LINUX_MIB_TCPHYSTARTDELAYDETECT);NET_ADD_STATS(sock_net(sk),LINUX_MIB_TCPHYSTARTDELAYCWND,tp->snd_cwnd);tp->snd_ssthresh = tp->snd_cwnd;}}}
}

注意,curr_rtt和delay_min保存的都是8倍的原值,而宏定义HYSTART_DELAY_THRESH,将delay_min>>3的值限定在了[4,16]之间。

慢启动 slow start

tcp_ack()在正确收到应答包后,有如下函数调用关系:[net/ipv4/tcp_input.c]

tcp_cong_control(sk, ack, delivered, flag, sack_state.rate);tcp_cong_avoid(sk, ack, acked_sacked);icsk->icsk_ca_ops->cong_avoid(sk, ack, acked);

调用的cong_avoid()函数如下:

[net/ipv4/tcp_cubic.c]

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);/*检查发送出去还没有收到ACK包的数量是否已经达到拥塞控制窗口上限,达到则返回*/if (!tcp_is_cwnd_limited(sk))return;/*当前窗口长度小于慢启动门限,则进入慢启动控制,否则进入拥塞避免*/if (tcp_in_slow_start(tp)) {//判断是否需要重置sk的CUBIC算法使用到的变量,ack > end_seq 表明本轮检测结束if (hystart && after(ack, ca->end_seq))bictcp_hystart_reset(sk);acked = tcp_slow_start(tp, acked);//慢启动处理函数//如果acked值为0,说明没有达到门限值(snd_ssthresh),不能进入拥塞避免阶段,当acked非0时,则会进入拥塞避免阶段if (!acked)return;}//更新ca(congestion avoid)的cnt成员,拥塞避免会使用该成员bictcp_update(ca, tp->snd_cwnd, acked);//拥塞避免处理算法tcp_cong_avoid_ai(tp, ca->cnt, acked);
}

慢启动函数tcp_slow_start():

/* Slow start is used when congestion window is no greater than the slow start* threshold. We base on RFC2581 and also handle stretch ACKs properly.* We do not implement RFC3465 Appropriate Byte Counting (ABC) per se but* something better;) a packet is only considered (s)acked in its entirety to* defend the ACK attacks described in the RFC. Slow start processes a stretch* ACK of degree N as if N acks of degree 1 are received back to back except* ABC caps N to 2. Slow start exits when cwnd grows over ssthresh and* returns the leftover acks to adjust cwnd in congestion avoidance mode.*/
u32 tcp_slow_start(struct tcp_sock *tp, u32 acked)
{/*snd_cwnd+acked = 当前窗口值 + acked包数量*/u32 cwnd = min(tp->snd_cwnd + acked, tp->snd_ssthresh); //取两者最小值//当snd_cwnd+acked < snd_ssthresh(门限值) 时,计算出的acked为0,说明还是在慢启动阶段,不能进入拥塞避免阶段acked -= cwnd - tp->snd_cwnd;tp->snd_cwnd = min(cwnd, tp->snd_cwnd_clamp);//这里更新拥塞窗口,(snd_cwnd_clamp为能增长到的最大窗口)return acked;//bictcp_cong_avoid()函数根据acked返回值来确实是否进入拥塞避免
}

1.最开始的窗口为1,则第一次acked为1,窗口变为2

2.发出去2个包,不管是delay ack 还是逐个ack,最后相当于acked 为2,窗口变为4

3.依次类推,最后就是类似于指数增长。

tcp_is_cwnd_limited()函数实现:检查发出去的包,但是还有收到ack的包是否已经达到了拥塞窗口的上限

/* We follow the spirit of RFC2861 to validate cwnd but implement a more* flexible approach. The RFC suggests cwnd should not be raised unless* it was fully used previously. And that's exactly what we do in* congestion avoidance mode. But in slow start we allow cwnd to grow* as long as the application has used half the cwnd.* Example :*    cwnd is 10 (IW10), but application sends 9 frames.*    We allow cwnd to reach 18 when all frames are ACKed.* This check is safe because it's as aggressive as slow start which already* risks 100% overshoot. The advantage is that we discourage application to* either send more filler packets or data to artificially blow up the cwnd* usage, and allow application-limited process to probe bw more aggressively.*/
static inline bool tcp_is_cwnd_limited(const struct sock *sk)
{const struct tcp_sock *tp = tcp_sk(sk);/* If in slow start, ensure cwnd grows to twice what was ACKed. */if (tcp_in_slow_start(tp))return tp->snd_cwnd < 2 * tp->max_packets_out; //确保拥塞窗口小于发出去的包的两倍return tp->is_cwnd_limited;
}static inline bool tcp_in_slow_start(const struct tcp_sock *tp)
{return tp->snd_cwnd < tp->snd_ssthresh;
}

拥塞避免congestion avoid

拥塞避免:从慢启动可以看到,cwnd可以很快的增长上来,从而最大程序利用网络带宽资源,但是cwnd不能一直这样无限增长,一定需要某个限制。TCP使用了一个叫慢启动门限(ssthresh)值的变量,当cwnd超过该值后,慢启动过程结束,进入拥塞避免阶段。对于大多数TCP实现来说,ssthresh的值是65536(同样以字节计算)。拥塞避免的主要思想是加法增大,也就是cwnd的值不再指数级往上升,开始加法增加。此时当窗口中所有的报文段被确认时,cwnd的大小加1,cwnd的值就随着RTT开始线性增加,这样就可以避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。

在拥塞避免阶段接收到ack:用cubic函数固端下一个rtt的窗口cwnd,以及函数在(r+RTT)处的函数值,作为target

当cwnd <= RENO cwnd, CUBIC在TCP-friendly region

当cwnd >= RENO cwnd

当cwnd < Wmax, CUBIC在convex region(凸区域)

当cwnd >=Wmax, CUBIC在concave region(凹区域)

需要用的数据结构:

/* BIC TCP Parameters */
struct bictcp {u32 cnt;/*用来控制snd_cwnd的增长*/        /* increase cwnd by 1 after ACKs */
/*两个重要的count值:1.tcp_sock->snd_cwnd_cnt,表示在当前的拥塞窗口中已经发送(经过对方ack包确认)的数据段个数2.bictcp->cnt,它是cubic拥塞算法的核心,主要用来控制拥塞避免状态的时候,什么时候才能增大拥塞窗口
具体实现是通过比较cnt和snd_cwnd_cnt,来决定是否增大拥塞窗口
*/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;/*即新的Wmax饱和点,取Wlast_max_cwnd和snd_cwnd较大者*/ /* origin point of bic function */u32 bic_K;/*即新Wmax所对应的时间点t,W(bic_K) = Wmax*/      /* time to origin pointfrom the beginning of the current epoch */u32 delay_min; /*最小RTT*/ /* min delay (msec << 3) */u32 epoch_start;/*拥塞状态切换开始的时刻*/    /* beginning of an epoch */u32 ack_cnt;/*在一个epoch中的ack包的数量*/    /* number of acks */u32 tcp_cwnd; /*按照reno算法得到的cwnd,预估的tcp窗口*/  /* estimated tcp cwnd */u16 unused;u8  sample_cnt;/*第几个sample 在混合慢启动中用到*/ /* number of samples to decide curr_rtt */u8  found;      /* the exit point is found? */u32 round_start;/*针对每个RTT*/    /* beginning of each round */u32 end_seq;/*用来标识每个RTT*/    /* end_seq of the round */u32 last_ack;   /* last time when the ACK spacing is close */u32 curr_rtt;/*由sample中最小的RTT决定*/   /* the minimum rtt of current round */
};

cubic窗口值更新公式如下:

窗口增长函数:

参考论文: CUBIC: A New TCP-Friendly High-Speed TCP Variant

在看bictcp_update前,先看论文中对应算法伪代码:(origin_point是新的最大窗口,K是到达origin_point的耗时)

首先更新ack_cnt;

如果epoch_start == 0,即丢包了,开始新的拥塞避免时段,epoch_start设置为当前时间tcp_time_stamp。重置参数。

当cwnd < last_max_cwnd.  K = cubic_root((last_max_cwnd - cwnd)/C), origin_point = last_max_cwnd.CUBIC在convex region(凸区域) 

    当cwnd >= last_max_cwnd. K = 0, origin_point = cwnd. CUBIC在concave region(凹区域)

    ack_cnt = 1, Wtcp = cwnd.

下面是cubic函数计算

计算时间t => t = tcp_time_stamp + dMin - epoch_start。 (t = 当前时间+最小rtt - 拥塞避免开始的时间)

计算按照cubic函数的速度,经过时间t到达的目标窗口。target = origin_point + C(t - K)^3.

1.如果目标窗口target>现在窗口,我们就增大速度早点达到目标,cnt = cwnd/(target - cwnd)。(这个就保证了一个rtt能增到目标窗口)

2.如果target <=cwnd,我们就降低速率。cnt = 100*cwnd。

更新函数:

/** Compute congestion window to use.*/
/*cwnd为当前的拥塞窗口大小,acked为超出门限值的ack数量*/
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 *///判断是否要更新(如果窗口没变,而且与上次更新的时间小于HZ/32,即31.25ms就不用更新,直接跳出)//为什么要这么做?if (ca->last_cwnd == cwnd &&(s32)(tcp_jiffies32 - ca->last_time) <= HZ / 32)return;/* The CUBIC function can update ca->cnt at most once per jiffy.* On all cwnd reduction events, ca->epoch_start is set to 0,* which will force a recalculation of ca->cnt.*//*CUBIC 函数每个 jiffy 最多可以更新 ca->cnt 一次。
在所有 cwnd 减少事件中,ca->epoch_start 设置为 0,这将强制重新计算 ca->cnt。*/if (ca->epoch_start && tcp_jiffies32 == ca->last_time)goto tcp_friendliness;ca->last_cwnd = cwnd; //记录进入拥塞避免时的窗口值ca->last_time = tcp_jiffies32; //记录进入拥塞避免的时刻if (ca->epoch_start == 0) { //丢包后,开启一个新的时段ca->epoch_start = tcp_jiffies32; /*新时段的开始*/   /* record beginning */ca->ack_cnt = acked; /*ack包计算器初始化*/           /* start counting */ca->tcp_cwnd = cwnd;            /* 同步更新syn with cubic *///max(last_max_cwnd, cwnd)作为当前Wmax饱和点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)*//*公式(1-beta)*Wmax = C*k^3=> k^3 = (1-beta)*Wmax/C=> k = cubic_root((1-beta)*Wmax/C)实际用的是:k = cubic_root(cube_factor * (ca->last_max_cwnd - cwnd))cube_factor = 1/C*///计算新的K值,cubic_root对参数进行立方根计算/*cube_factor值在cubictcp_register函数中初始化cube_factor = 0x9fd809fd;cwnd为snd_cwnd当前的拥塞窗口值下面查看cubictcp_register()函数,其中说明了K值的相关计算*/ca->bic_K = cubic_root(cube_factor* (ca->last_max_cwnd - cwnd));ca->bic_origin_point = ca->last_max_cwnd;}}/* cubic function - calc*//* calculate c * time^3 / rtt,*  while considering overflow in calculation of time^3* (so time^3 is done by using 64 bit)* and without the support of division of 64bit numbers* (so all divisions are done by using 32 bit)*  also NOTE the unit of those veriables*    time  = (t - K) / 2^bictcp_HZ*    c = bic_scale >> 10* rtt  = (srtt >> 3) / HZ* !!! The following code does not have overflow problems,* if the cwnd < 1 million packets !!!*///计算时间t => t = tcp_time_stamp + dMin - epoch_start。 (t = 当前时间+最小rtt - 拥塞避免开始的时间)t = (s32)(tcp_jiffies32 - ca->epoch_start);t += msecs_to_jiffies(ca->delay_min >> 3); //注意这里delay_min在被赋值时就已经乘以8了/* change the unit from HZ to bictcp_HZ */t <<= BICTCP_HZ;do_div(t, HZ);//求|t - ca->bic_K|的绝对值 => t-kif (t < ca->bic_K)      /* t - K */offs = ca->bic_K - t;elseoffs = t - ca->bic_K;/* c/rtt * (t-K)^3 *//*根据公式(t-k)^3 + Wmax计算按照cubic函数的速度,经过时间t到达的目标窗口。target = origin_point + C(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*//*1.如果目标窗口target>现在窗口,我们就增大速度早点达到目标,cnt = cwnd/(target - cwnd)。(这个就保证了一个rtt能增到目标窗口)*/if (bic_target > cwnd) {ca->cnt = cwnd / (bic_target - cwnd);} else {/*2.如果target <=cwnd,我们就降低速率。cnt = 100*cwnd。*/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 */tcp_friendliness:/* TCP Friendly *//*估算采用reno的窗口大小:cwnd=cwnd+ack_cnt/delta*/if (tcp_friendliness) {u32 scale = beta_scale;delta = (cwnd * scale) >> 3;while (ca->ack_cnt > delta) {       /* update tcp cwnd */ca->ack_cnt -= delta;ca->tcp_cwnd++;}if (ca->tcp_cwnd > cwnd) {  /* if bic is slower than tcp */delta = ca->tcp_cwnd - cwnd;max_cnt = cwnd / delta;if (ca->cnt > max_cnt)ca->cnt = max_cnt;}}/* 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);
}

cubictcp_register(void)函数

static int __init cubictcp_register(void)
{BUILD_BUG_ON(sizeof(struct bictcp) > ICSK_CA_PRIV_SIZE);/* Precompute a bunch of the scaling factors that are used per-packet* based on SRTT of 100ms*//*基于 100ms 的 SRTT 预先计算一组每个数据包使用的缩放因子BICTCP_BETA_SCALE为1024用于比例因子计算,beta为717*///beta_scale计算出的值为15beta_scale = 8*(BICTCP_BETA_SCALE+beta) / 3/ (BICTCP_BETA_SCALE - beta);//bic_scale默认为41cube_rtt_scale = (bic_scale * 10);  /* 1024*c/ni z *//* calculate the "K" for (wmax-cwnd) = c/rtt * K^3*  so K = cubic_root( (wmax-cwnd)*rtt/c )* the unit of K is bictcp_HZ=2^10, not HZ K的单位是bictcp_HZ=2^10,而不是HZ,说明与平台无关**  c = bic_scale >> 10*  rtt = 100ms** the following code has been designed and tested for 以下代码已经被设计和测试* cwnd < 1 million packets 拥塞窗口 小于 一百万个包* RTT < 100 seconds RTT小于100秒* HZ < 1,000,00  (corresponding to 10 nano-second) HZ小于10纳秒*//* 1/c * 2^2*bictcp_HZ * srtt */cube_factor = 1ull << (10+3*BICTCP_HZ); /* 2^40  = 0x10000000000*//* divide by bic_scale and by constant Srtt (100ms) 除以 bic_scale 和常数 Srtt (100ms)*//*do_div 结果保留在cube_factor中,cube_factor = cube_factor/(bic_scale * 10) = 0x9fd809fd*/do_div(cube_factor, bic_scale * 10);return tcp_register_congestion_control(&cubictcp);
}

tcp_cong_avoid_ai()函数

[net/ipv4/tcp_cong.c]

/* 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);
}

后来的快速恢复算法是在快速重传算法后添加的,当收到3个重复ACK时,TCP最后进入的不是拥塞避免阶段,而是快速恢复阶段。快速重传和快速恢复算法一般同时使用。快速恢复的思想是"数据包守恒"原则,即同一个时刻在网络中的数据包数量是恒定的,只有当"老"数据包离开了网络后,才能向网络中发送一个"新"的数据包,如果发送方收到一个重复的ACK,那么根据TCP的ACK机制就表明有一个数据包离开了网络,于是cwnd加1.如果能够严格按照该原则那么网络中很少会发生拥塞,事实上拥塞控制的目的也就在修正违反原则的地方。

快速重传和快速恢复

当收到乱序包时,tcp可能会立即应答,重复的应答不应该被延迟,重复ACK的目的是让对端知道一个收到数据包乱序了,并且通知对端其期望的序列号。

由于tcp并不知道一个重复的ACK源于一个丢失的数据包还是数据包的重组,其会继续等待是否有相同的ACK应答包,其基于如果数据奥是乱序的,则收到重复的ACK数量应该在一个或两个,然后是一个新的ACK到来,如果重复的ACK出现三次及以上,则预示着一个数据包丢失了。TCP然后会立即重传丢失的数据包,而不会等待重传定时器超时。

在快速重传丢失的数据包后启用拥塞避免算法,而不是慢启动算法被调用。这就是快速恢复的意义。这一方法使得在中度拥塞的情况下能有较高的吞吐率。

具体来说快速恢复的主要步骤是:

1.当收到3个重复ack时,把ssthresh设置为cwnd的一半,把cwnd设置为ssthresh的值加3,然后重传丢失的报文段,加3的原因是因为收到3个重复的ACK,表明有3个老的数据包离开了网络。

2.再收到重复的ACK时,拥塞窗口加1

3.当收到新的数据包的ack时,把cwnd设置为第一步中的ssthresh的值。原因是因为该ack确认了新的数据,说明从重复ack时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

9. TCP拥塞控制相关推荐

  1. tcp拥塞控制编程实验c语言代码,C语言 计算机网络TCP拥塞控制模拟程序

    帮助你更好地认识TCP拥塞控制的机制 #include "stdio.h" #include "stdlib.h" void show() { //system ...

  2. TCP流量控制-TCP拥塞控制 拥塞控制与流量控制的区别

    拥塞控制与流量控制的区别: 拥塞控制是让网络能够承受现有的网络负荷,是一个全局性的过程,涉及所有的主机.所有的路由器,以及与降低网络传输性能有关的所有因素. 相反,流量控制往往是指点对点的通信量的控制 ...

  3. 5.3.5 TCP拥塞控制

    5.3.5 TCP拥塞控制

  4. 【计算机网络】传输层 : 总结 ( TCP / UDP 协议 | 寻址与端口 | UDP 协议 | TCP 协议特点 | TCP 连接释放 | TCP 流量控制 | TCP 拥塞控制 ) ★★★

    文章目录 一.传输层 TCP / UDP 协议 ★ 二.寻址端口号 ★ 三.UDP 协议特点 四.UDP 协议首部格式 五.UDP 校验 六.TCP 协议 特点 ★ 七.TCP 报文段首部格式 八.T ...

  5. 【计算机网络】传输层 : TCP 拥塞控制 ( 慢开始 | 拥塞避免 | 快重传 | 快恢复 )

    文章目录 一.TCP 拥塞控制 二.TCP 拥塞控制算法 三.慢开始 和 拥塞避免 算法 四.快重传 和 快回复 算法 一.TCP 拥塞控制 TCP 拥塞控制 : ① 拥塞出现表现 : 资源需求总和 ...

  6. 计算机网络-基本概念(9)【传输层】TCP拥塞控制 【网络层】拥塞避免

    TCP拥塞控制 对网络中某一资源的需求超过了该资源(带宽.交换节点中的缓存.处理机)所能提供的可用部分.防止过多的数据注入网络中,防止路由器或链路过载,是属于全局性的过程,包括主机.路由器.链路等设备 ...

  7. s6-8 TCP 拥塞控制

    TCP 拥塞控制  虽然网络层也试图管理拥塞,但是,大多数繁重的任务是由TCP来完成的,因为针对拥塞的真正解决方案是减慢数据率  分组守恒:当有一个老的分组离开之后才允许新的分组注入网络  TC ...

  8. ​TCP 拥塞控制详解

    作者:engleliu,腾讯 PCG 开发工程师 本文主要介绍 TCP 拥塞控制算法,内容多来自网上各个大佬的博客及<TCP/IP 详解>一书,在此基础上进行梳理总结,与大家分享.因水平有 ...

  9. tcp拥塞控制_网络TCP的拥塞控制算法简介

    作为网络中使用最广泛的传输协议,TCP的拥塞控制机制是学术界和工业界关注的焦点问题之.然而,目前广泛使用的TCP传输协议的拥塞控制算法仍然使用相对固定的窗口调节策略,无法根据动态变化的场景自适应地调整 ...

  10. 计算机网络之传输层:7、TCP拥塞控制

    传输层:7.TCP拥塞控制 TCP拥塞控制: 拥塞控制算法: 慢开始和拥塞避免: 快重传和快恢复: TCP拥塞控制算法综合: TCP拥塞控制: 拥塞控制算法: 发送方维持一个叫做拥塞窗口(cwnd) ...

最新文章

  1. 微软 Surface Pro、Studio、Laptop 全线更新
  2. 瞬间教你学会使用java中list的retainAll方法
  3. Android动态方式破解apk进阶篇(IDA调试so源码)
  4. 回顾-离开帝都半年了
  5. 在未启动程序情况 点击视图设计器 弹出未将对象引用窗体的解决方案
  6. BZOJ 3697: 采药人的路径 [点分治] [我想上化学课]
  7. centos7切换root为mysql,CentOS 7中使用rpm方式安装MySQL 5.7后无法使用root登录解决
  8. Server Tomcat v7.0 Server at localhost was unable to start within 45 seconds
  9. 教大家抖音怎么开通直播功能
  10. 同济大学 线性代数 第六版 pdf_线性代数同济第六版第六章课后习题答案!
  11. android 手机 基站定位软件,安卓手机基站+GPS定位源码
  12. LeetCode常见报错解释
  13. H5打开APP技术总结
  14. 大数据时代保护个人信息安全该采取什么措施?
  15. HR面试都会问什么问题?(上)
  16. 白化滤波器matlab程序,04实验四:白化滤波器的设计实验报告
  17. 模拟电子技术基础-什么是放大?
  18. mysql配置secure_file_priv
  19. keep-alive相关用法及使用场景
  20. 2022新版PMP考试有哪些变化?

热门文章

  1. 生产任务分配问题 matlab+lingo
  2. oracle从入门到跑路
  3. 必不可少的10类MAC装机必备软件,个个万里挑一
  4. 批量将磁盘上所有文件的路径地址、文件名、扩展名和文件夹名整理到 Excel 表格中
  5. 拍立淘-以图搜图中的图像搜索算法
  6. SSH Tunnel隧道详解
  7. 2021年危险化学品经营单位安全管理人员考试报名及危险化学品经营单位安全管理人员最新解析
  8. 视频 | 苏炳添的“冠军卧室”曝光,来看看百米飞人的另一面
  9. ubuntu16.04 百度网盘加速下载文件
  10. linux去除pdf页头,删除PDF水印小妙招