TCP拥塞控制算法内核实现剖析(八)

内核版本:3.2.12

主要源文件:linux-3.2.12/ net/ ipv4/ tcp_htcp.c

本文主要分析htcp的实现,作者zhangskd @ csdn

概要

HTCP, like Cubic, uses the elapsed time(t) since the last congestion event for calculating the current
congestion window size. The window growth function of HTCP is a quadratic function of t. HTCP is
unique in that it adjusts the decrease factor by a function of RTTs which is engineered to estimate the
queue size in the network path of the current flow. Thus, the decrease factor is adjusted to be
proportional to the queue size.

HTCP和Cubic相同处在于:窗口增长函数为多次函数,自变量为当前时间与上次丢包时间的差值。
以距离上次丢包的时间为自变量,就可以让窗口的增长与RTT无关,从而得到较好的RTT公平性。
所以说,在窗口增长函数方面,Cubic借鉴了HTCP的思想。

HTCP的窗口增长函数为二次函数,Cubic的窗口增长函数为三次函数。
单从自变量次幂来看,Cubic的窗口增长似乎猛了一些,其实不然。由于该立方函数的特性,当窗口
增长到接近上次发生丢包的窗口大小时,窗口增长就会变得很缓慢,这个阶段就称为稳定阶段。
HTCP的窗口增长函数其实是一个分段函数:当距离上次丢包的时间小于1s时,采用和Reno相同的窗口
增长策略,即每个RTT内窗口加一。而当距离上次丢包的时间大于1s后,则采用一个二次函数,这个
函数是一个凹函数,即增长会越来越快。

HTCP的独特之处在于:当发生丢包时,它的乘性减小因子并不是固定的,而是和网络中的包队列有关。
它试图达到这样一种效果:充分利用网络带宽,同时让瓶颈路由器缓存队列为空。这样就能在丢包后达
到最大的吞吐量。减小因子的计算涉及到吞吐量和RTT。

原理

 

窗口增长函数

On each ack set

        1      //  t <= delta
a = 
         1 + 10(t - delta) + ((t - delta)/2)^2    // t >= delta

and then set

        a = 2*a*(1-B)

t为距离上次丢包的时间。
当t <= delta时,认为处于较慢的网络,采用和Reno相同的窗口增长函数。
当t >= delta时,认为处于BDP网络,采用较为激进的二次窗口增长函数。
采用分段函数的目的是:保证对慢速网络的兼容性,以及在BDP网络中的友好性。
delta是一个阈值,在实现中设为1s。
为什么a又要乘2(1-B)呢?这是为了保证HTCP和Reno有着相同的高点,保证HTCP的友好性
和公平性。

自适应减小因子

注意:以下的B是(1 - B),丢包后cwnd=B*cwnd!

网络容量Capacity = BDP + Queue
当发生拥塞丢包时,乘性减小因子为多少才合适呢?HTCP认为乘性减小因子应该能达到这样一种效果:
完全利用BDP,同时使路由器队列Queue为空。这样就能够达到最大的吞吐量。
发生拥塞前,Queue满:
Throughput(Before)= cwnd(Before) / maxRTT
发生拥塞后,Queue空:
Throughput(After)= cwnd(After)/ minRTT
如果拥塞前后的吞吐量不变,则能最大程度的保证网络的利用率。(为什么吞吐量会不变呢?
因为拥塞前的窗口大,队列满,RTT大;拥塞后的窗口小,队列空,RTT小。窗口的减小和RTT的减小
刚好抵消了)
所以B=minRTT / maxRTT,此时有最大的吞吐量。
这种方法的好处是:当路由缓存较小时,B>0.5,不至于过多的减少吞吐量。

响应性

在AIMD策略中,收敛性(convergence)和吞吐量(throughput)是对立的关系。
收敛性指的是当网络状况有变化时(如有新的连接加入或退出),网络中的连接能迅速的收敛到
一个相同的状态。
当B较大时,即丢包后窗口减小较少时,就会收敛的比较慢,所以B应该有一个上限。
当B=0.5时,收敛时间为4个congestion epochs;
当B=0.8时,收敛时间为13个congestion epochs;
规定B<0.8,能使收敛时间在13个拥塞周期之内。
当发现此次拥塞周期内的最大吞吐量与上个周期内的最大吞吐量的差值大于20%时,说明网络
状况有了较大的变化,需要快速收敛,所以此时应使B=0.5。

                 0.5      // | (max_througput(k+1) - max_througput(k) ) / max_througput(k) | > 0.2
B(k+1) =
                 minRTT / maxRTT  // otherwise

参数

#define ALPHA_BASE (1<<7) /* 1.0 with shift << 7 */
#define BETA_MIN (1<<6) /* 0.5 with shift << 7 */
#define BETA_MAX 102 /* 0.8 with shift << 7 */

ALPHA_BASE为加性增长因子AI的最小值<<7,在htcp初始化时设置。也就是说ALPHA 的最小值为1,
此时和Reno的相同。
这里的BETA因子是1 - 乘性减小因子。BETA的取值范围在[ BETA_MIN , BETA_MAX ]。
BETA设定最小值的原因是:BETA很小时,会严重降低吞吐量。
BETA设定最大值的原因是:BETA很大时,不能消除拥塞,且不能快速收敛。
BETA的设定时需要在吞吐量与收敛性之间权衡。 

/* turn on/off bandwidth switcher */
static int use_bandwidth_switch __read_mostly = 1;

use_bandwidth_switch表示beta是否使用分段函数,即是否需要检查不同epoch中吞吐量的变化。
如果不启用,那么beta在任何情况下都为minRTT/maxRTT。
use_bandwidth_switch可以看做快速收敛开关。 

/* turn on/off RTT scaling */
static int use_rtt_scaling __read_mostly = 1;

use_rtt_scaling表示是否根据rtt来对alpha进行调节。
启用use_rtt_scaling后:
(1) minRTT > 200ms,alpha放大倍数=2
(2) minRTT < 10ms,alpha放大倍数= 0.1
(3) 10ms <= minRTT <= 200ms,alpha放大倍数= minRTT/100
这样是为了进一步保证HTCP窗口增长的RTT公平性。 

struct htcp {
    u32 alpha; /* Fixed point arith, << 7 , 每收到ACK更新一次 */
    u32 beta; /* Fixed point arith, << 7 , 检测到丢包时,每个epoch更新一次 */
    u8 modeswitch; /* 当发现网络状况变化较大时设为0,使下个epoch的beta继续为0.5,
                                      从而保证能进行快速收敛。*/

    u16 pkts_acked; /* 每个ACK确认的数据包个数 */
    u32 packetcount; /* 每个RTT内确认的数据包个数 */
    u32 minRTT;  /* 每个epoch内的最小RTT,代表路由缓存为空的情况*/
    u32 maxRTT; /* 每个epoch内的最大RTT ,代表路由缓存满的情况*/
    u32 last_cong; /* The time since last congestion event end,
                      每个epoch的起点*/

    u32 undo_last_cong;
    u32 undo_maxRTT;
    u32 undo_old_maxB;
 
    /* Bandwidth estimation */
    u32 minB; /* 最小吞吐量,没用到 */
    u32 maxB; /* 当前epoch的最大吞吐量,每个RTT末计算一次 */
    u32 old_maxB; /* 上个epoch的最大吞吐量*/
    u32 Bi; /* 平滑后的当前RTT吞吐量*/
    u32 lasttime; /* RTT结束时间戳*/
};
static struct tcp_congestion_ops htcp __read_mostly = {
    .init = htcp_init,
    .ssthresh = htcp_recalc_ssthresh,
    .cong_avoid = htcp_cong_avoid,
    .set_state = htcp_state,
    .undo_cwnd = htcp_cwnd_undo,
    .pkts_acked = measure_achieved_througput,
    .owner = THIS_MODULE,
    .name = "htcp",
};

函数

算法初始化:

static void htcp_init (struct sock *sk)
{
    struct htcp *ca = inet_csk_ca(sk);

    memset(ca, 0, sizeof(struct htcp));

    ca->alpha = ALPHA_BASE;
    ca->beta = BETA_MIN;
    ca->pkts_acked = 1;
    ca->last_cong = jiffies;
}

BETA的动态调整
measure_rtt()是在measure_achieved_throughput()中被调用的,即每次收到ACK都会被调用。

目的是计算每个epoch内的minRTT和maxRTT,当丢包时就可以用此epoch的minRTT和maxRTT

来计算beta了。

static inline void measure_rtt(struct sock *sk, u32 srtt)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    struct htcp *ca = inet_csk_ca(sk);

    /* keep track of minimum RTT seen so far, minRTT is zero at first */
    if ( ca->minRTT > srtt || !ca->minRTT)
        ca->minRTT = srtt; 

    /* max RTT,只能在open状态下取样,处于其它状态时可能会得到
     * 不准确的maxRTT。
     */
    if ( icsk->icsk_ca_state == TCP_CA_Open) {
        if (ca->maxRTT < ca->minRTT)
            ca->maxRTT = ca->minRTT;

        /* srtt不能超过maxRTT的20ms*/
        if (ca->maxRTT < srtt &&
             srtt <= ca->maxRTT + msecs_to_jiffies(20))
             ca->maxRTT = srtt; 
    }
}

measure_achieved_throughput()在每收到一个ACK时调用,主要用于更新当前epoch的最大吞吐量

maxB。maxB则用于beta值的判断:当前maxB和上一个epoch的old_maxB如果相差超过20%,说

明网络情况有了较大的变化,所以需要快速收敛,设置beta=0.5。

static void measure_achieved_throughput(struct sock *sk, u32 pkts_acked, s32 rtt)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    const struct tcp_sock *tp = tcp_sk(sk);
    struct htcp *ca = inet_csk_ca(sk);
    u32 now = tcp_time_stamps;

    if (icsk->icsk_ca_state == TCP_CA_Open)
       ca->pkts_acked = pkts_acked;

    if (rtt > 0)
        measure_rtt(sk, usecs_to_jiffies(rtt));

    /* 如果不考虑收敛性,就不用考虑不同epoch间的吞吐量变化,所以也就不用
     * 计算当前的吞吐量了。
     */
    if (!use_bandwidth_switch)
         return;

   /* 只有在Open或Disorder状态下才计算当前吞吐量*/
   if ( !(1<< icsk->icsk_ca_state) & (TCPF_CA_Open | TCPF_CA_Disorder))) {
         ca->packetcount = 0;
         ca->lasttime = now;
         return;
    }

    ca->packetcount += pkts_acked;
    
    /* 在每个RTT末,即一个RTT快结束时,才计算吞吐量。
     * 所以吞吐量是每个RTT才计算一次的。
     */
    if (ca->packetcount >= tp->snd_cwnd - (ca->alpha >> 7 ? : 1) &&
        now - ca->lasttime >= ca->minRTT && ca->minRTT > 0) {

        /* 计算当前RTT内的吞吐量 */
        __u32 cur_Bi = ca->packetcount * HZ / (now - ca->lasttime);

        /* 如果现在距离上次丢包的时间不超过3个RTT, 则更新吞吐量相关数据*/
        if (htcp_ccount(ca) <= 3) {
            ca->minB = ca->maxB = ca->Bi = cur_Bi;
        } else {

           ca->Bi = (3 * ca->Bi + cur_Bi ) / 4; /* 平滑处理cur_Bi */
           if (ca->Bi > ca->maxB)
               ca->maxB = ca->Bi;
           if (ca->minB > ca->maxB)
               ca->minB = ca->maxB;
        }
        ca->packetcount = 0;
        ca->lasttime = now;
    }
}

/* 现在距离上次发生丢包的时间*/
static inline u32 htcp_cong_time(const struct htcp *ca)
{
    return jiffies - ca->last_cong;
}

/* 从上次丢包到现在经历的RTT个数 */
static inline u32 htcp_ccount(const struct htcp *ca)
{
    return htcp_cong_time(ca) / ca->minRTT;
}

以上两个函数分别计算此epoch内的RTT和吞吐量相关数据,这都是为beta的更新做准备的。

htcp_beta_update()是在丢包后才被调用到,即每个epoch结束时调用一次。

static inline void htcp_beta_update(struct htcp *ca, u32 minRTT, u32 maxRTT)
{
    if (use_bandwidth_switch) { /* 开启快速收敛的功能 */
        u32 maxB = ca->maxB;
        u32 old_maxB = ca->old_maxB;
        ca->old_maxB = ca->maxB; /* 更新old_maxB,因为此epoch已经结束了*/

        /* 如果|maxB - old_maxB| / maxB > 20%,那么就快速收敛,设置beta=0.5 */
        if ( !between(5 * maxB, 4 * old_maxB, 6 * old_maxB)) {
              ca->beta = BETA_MIN;

              /* 这个变量的作用是使下个epoch继续使用beta=0.5,来保证快速收敛*/
              ca->modeswitch = 0; 
              return;
         }
    }

    /* minRTT>10ms才能使用,如果RTT太小的话,那么可能
     * beta=minRTT/maxRTT的值接近1。
     */
    if (ca->modeswitch && minRTT > msecs_to_jiffies(10) && maxRTT)
    {
        ca->beta = (minRTT << 7) / maxRTT; /* 计算beta*/

        /* 确保beta落在[BETA_MIN , BETA_MAX]区间内*/
        if ( ca->beta < BETA_MIN)
             ca->beta = BETA_MIN;
        else if (ca->beta > BETA_MAX)
             ca->beta = BETA_MAX:
    } else {

        ca->beta = BETA_MIN; /* 否则就使beta = 0.5 */
        ca->modeswitch = 1;
    }
}

ALPHA的动态调整
与htcp_beta_update()不同,htcp_alpha_update()在拥塞避免阶段收到ACK都会被调用。

static inline void htcp_alpha_update(struct htcp *ca)
{
    u32 minRTT = ca->minRTT;
    u32 factor = 1;
    u32 diff = htcp_cong_time(ca); /* 距离上次丢包的时间,为自变量*/

    /* 如果diff > HZ,则认为是BDP网络,采用激进的二次函数,
     * 而如果diff <= HZ,则认为是慢速网络,factor = 1。
     */
    if (diff > HZ) {
        diff -= HZ;
        factor = 1 + (10*diff + ((diff / 2) * (diff /2) / HZ)) / HZ;
    }

    if (use_rtt_scaling && minRTT) {
        u32 scale = (HZ << 3)  / (10 * minRTT) ;
       
        /* scale的值如下:
         * (1) 当minRTT > 200ms,scale = 4
         * (2) 当minRTT < 10ms,scale = 80
         * (3) 当10ms <= minRTT <= 200ms,scale = 80 / minRTT
         */
        scale = min(max(scale, 1U << 2), 10U << 3);

        /* fator的值如下:
         * (1) 当minRTT > 200ms,factor *= 2
         * (2) 当minRTT < 10ms,factor *= 0.1
         * (3) 当10ms <= minRTT <= 200ms,factor *= minRTT/100
         */
        factor = (factor << 3) / scale;
        if (!factor)
            factor = 1; /* factor不能为0*/
    }

    /* 把alpha乘2(1 - beta),为了TCP的友好性*/
    ca->alpha = 2 * factor * ((1 << 7) - ca->beta);
    if (!ca->alpha)
        ca->alpha = ALPHA_BASE;
}


封装调用

/* After we have the rtt data to calculate beta, we'd still prefer to wait one 
 * rtt before we adjust our beta to ensure we are working from a consistent data.
 * This function should be called when we hit a congestion event since only at 
 * that point do we really have a real sense of maxRTT ( the queues en route 
 * were getting just too full now).
 */
static void htcp_param_update(struct sock *sk)
{
    struct htcp *ca = inet_csk_ca(sk);
    u32 minRTT = ca->minRTT;
    u32 maxRTT = ca->maxRTT;

    htcp_beta_update(ca, minRTT, maxRTT);
    htcp_alpha_update(ca);

    /* add slowly fading memory for maxRTT to accomodate routing changes 
     * 稍微减小一下maxRTT,因为minRTT和maxRTT在丢包后都没有重置。
     */
    if (minRTT >0 && maxRTT > minRTT)
        ca->maxRTT = minRTT + ((maxRTT - minRTT) * 95) / 100;
}

丢包时调用:

static u32 htcp_recalc_ssthresh(struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    const struct htcp *ca = inet_csk_ca(sk);
    htcp_param_update(sk);
    return max((tp->snd_cwnd * ca->beta) >> 7, 2U);
}

拥塞避免函数:

static void htcp_cong_avoid(struct sock *sk, u32 ack, u32 in_flight)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct htcp *ca = inet_csk_ca(sk);

    if (!tcp_is_cwnd_limited(sk, in_flight))
         return;

    if (tp->snd_cwnd <= tp->snd_ssthresh)
        tcp_slow_start(tp);
    else {
        /* In dangerous area, increase slowly.
         * In theory this is tp->snd_cwnd += alpha / tp->snd_cwnd
         */
         if ((tp->snd_cwnd_cnt * ca->alpha) >>7 >= tp->snd_cwnd) {
              if (tp->snd_cwnd < tp->snd_cwnd_clamp)
                  tp->snd_cwnd++;
              tp->snd_cwnd_cnt = 0;
              htcp_alpha_update(ca); /* snd_cwnd有变时才调用*/
         } else 
              tp->snd_cwnd_cnt += ca->pkts_acked;

        ca->pkts_acked = 1;
    }
}

拥塞窗口调整撤销

当切换到CWR、Recovery、Loss状态前,会先保存相关变量。如果以后发现检测

到的拥塞时虚假的,就能根据这些保存下来的变量,撤销拥塞窗口的调整。

static inline void htcp_reset(struct htcp *ca)
{
    ca->undo_last_cong = ca->last_cong;
    ca->undo_maxRTT = ca->maxRTT;
    ca->undo_old_maxB = ca->old_maxB;

    ca->last_cong = jiffies;
}

htcp_reset()是在htcp_state()中被调用的。

static void htcp_state(struct sock *sk, u8 new_state)
{
    switch(new_state) {
    case TCP_CA_Open:
            {
                struct htcp *ca = inet_csk_ca(sk);
                if (ca->undo_last_cong) { /* 清除保存数据*/
                    ca->last_cong = jiffies;
                    ca->undo_last_cong = 0;
                }
            }
            break;

    case TCP_CA_CWR:
    case TCP_CA_Recovery:
    case TCP_CA_Loss:

           htcp_reset(inet_csk_ca(sk));
           break;
    }
}

撤销拥塞窗口的调整,恢复到调整前的状态:

static u32 htcp_cwnd_undo(struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    struct htcp *ca = inet_csk_ca(sk);

    if (ca->undo_last_cong)
    {
        ca->last_cong = ca->undo_last_cong;
        ca->maxRTT = ca->undo_maxRTT;
        ca->old_maxB = ca->undo_old_maxB;
        ca->undo_last_cong = 0;
    }
    return max(tp->snd_cwnd, (tp->snd_ssthresh << 7) / ca->beta);
}

性能

大多数TCP拥塞控制算法都是由个人提出的,而htcp则是由Hamilton Institute提出的。

比较遗憾,还没找到关于htcp的全方面的性能测试报告。

在实际的使用中,htcp并没有突出的表现。一个可能的原因是:在拥塞避免阶段,每次切换到二次增

长函数需要1s的时间。在网络拥塞时期,拥塞周期可能在1s前就结束了,所以在大多数时间上都是

采用Reno的窗口增长策略。

原文地址:https://www.cnblogs.com/aiwz/p/6333387.html