9. TCP拥塞控制

基于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_MD5SIG
    tcp_sk(sk)->af_specific = &tcp_sock_ipv4_specific;
#endif

    return 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);

    //重传超时值,起始设置为1s
    icsk->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,不包括SACKS
    tp->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;
    //发送和接收buffer
    sk->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_ssthresh
      3.在拥塞窗口大于等于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 point
                   from 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-k
    if (t < ca->bic_K)      /* t - K */
        offs = ca->bic_K - t;
    else
        offs = 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计算出的值为15
    beta_scale = 8*(BICTCP_BETA_SCALE+beta) / 3
        / (BICTCP_BETA_SCALE - beta);

    //bic_scale默认为41
    cube_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时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

byd yes

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值