Tail Loss Probe实现

TCPIP协议 专栏收录该内容
105 篇文章 6 订阅

Tail loss probe (TLP)利用RACK减少RTO的发生,TLP触发快速恢复以修复末端丢包,否则只能由之后的RTO来修复。在原始报文发送之后,TLP将在1到2倍的RTT时间内发送探测数据报文,探测报文可以是新的、之前非发送过的报文,或者重传SND.NXT序号之前已经发送过的报文。探测报文的发送旨在激起对端的反馈,例如ACK报文,这样RACK就可以发起快速重传,而不用等待RTO超时。

如下PROC文件tcp_early_retrans控制TLP功能的开关,值为0为关闭,值为3或者4位开启,默认情况下其值为3,即开启TLP。

$ cat /proc/sys/net/ipv4/tcp_early_retrans
3

TLP初始化

Linux内核中sysctl_tcp_early_retrans变量控制TLP功能的开启关闭,在初始化函数tcp_sk_init中将其赋值为3。

static int __net_init tcp_sk_init(struct net *net)
{
    net->ipv4.sysctl_tcp_early_retrans = 3;

TLP探测

在报文发送函数tcp_write_xmit的发送循环之后,如果报文发送数量sent_pkts不为零,并且push_one不等于2,即本次发送的不是TLP探测报文时,调用TLP调度函数tcp_schedule_loss_probe。

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp)
{
    while ((skb = tcp_send_head(sk))) {
        ...
        if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
            break;
        sent_pkts += tcp_skb_pcount(skb);

        if (push_one) break;
    }
    if (likely(sent_pkts)) {
        ...
        /* Send one loss probe per tail loss episode. */
        if (push_one != 2)
            tcp_schedule_loss_probe(sk, false);

如下TLP调度函数tcp_schedule_loss_probe,其执行TLP功能的前提条件有以下几个:

  • 控制变量sysctl_tcp_early_retrans的值为3或者4;
  • 网络中存在未确认的报文;
  • 当前连接支持SACK选项;
  • 当前连接的拥塞状态为TCP_CA_Open或者TCP_CA_CWR,即已经处于丢失恢复状态,不开启TLP。
bool tcp_schedule_loss_probe(struct sock *sk, bool advancing_rto)
{
    early_retrans = sock_net(sk)->ipv4.sysctl_tcp_early_retrans;
    /* Schedule a loss probe in 2*RTT for SACK capable connections
     * not in loss recovery, that are either limited by cwnd or application.
     */
    if ((early_retrans != 3 && early_retrans != 4) ||
        !tp->packets_out || !tcp_is_sack(tp) ||
        (icsk->icsk_ca_state != TCP_CA_Open && icsk->icsk_ca_state != TCP_CA_CWR))
        return false;

以上条件全部满足时,开始选择TLP定时器时长。如果有可用的SRTT值,使用其值(srtt_us中保存的为8倍的SRTT值),并且如果仅有一个未确认报文,接收端有可能执行延时ACK,为避免提前超时,将TLP的超时时间增加TCP_RTO_MIN(HZ/5 = 200ms)。否则,如果未确认报文大于一,将TLP超时时间增加TCP_TIMEOUT_MIN(2 jeffies)。

如果SRTT不可用,将TLP超时时间设置为TCP_TIMEOUT_INIT(1*HZ)。最后,如果当前启动了RTO,并且其剩余的超时时间比以上计算的TLP超时时间要更靠前,将TLP超时时长设置为RTO的剩余超时时间,开启定时器。

    /* Probe timeout is 2*rtt. Add minimum RTO to account
     * for delayed ack when there's one outstanding packet. If no RTT
     * sample is available then probe after TCP_TIMEOUT_INIT.
     */
    if (tp->srtt_us) {
        timeout = usecs_to_jiffies(tp->srtt_us >> 2);
        if (tp->packets_out == 1)
            timeout += TCP_RTO_MIN;
        else
            timeout += TCP_TIMEOUT_MIN;
    } else {
        timeout = TCP_TIMEOUT_INIT;
    }

    /* If the RTO formula yields an earlier time, then use that time. */
    rto_delta_us = advancing_rto ?
            jiffies_to_usecs(inet_csk(sk)->icsk_rto) :
            tcp_rto_delta_us(sk);  /* How far in future is RTO? */
    if (rto_delta_us > 0)
        timeout = min_t(u32, timeout, usecs_to_jiffies(rto_delta_us));

    tcp_reset_xmit_timer(sk, ICSK_TIME_LOSS_PROBE, timeout, TCP_RTO_MAX, NULL);

TLP超时处理

如下所示TLP定时器超时之后,由函数tcp_send_loss_probe处理,发送探测报文。

void tcp_write_timer_handler(struct sock *sk)
{   
    ...
    event = icsk->icsk_pending;
    
    switch (event) {
    case ICSK_TIME_LOSS_PROBE:
        tcp_send_loss_probe(sk);

以下tcp_send_loss_probe函数,如果发送队列(sk_write_queue)中存在未发送的数据,并且对端通告的接收窗口允许发送,调用tcp_write_xmit发送函数,注意其第四个参数值为2,参考以上的介绍,此次发送不会调用TLP定时器。根据发送函数最终是否发出报文,跳转到不同的处理。

void tcp_send_loss_probe(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    int mss = tcp_current_mss(sk);

    skb = tcp_send_head(sk);
    if (skb && tcp_snd_wnd_test(tp, skb, mss)) {
        pcount = tp->packets_out;
        tcp_write_xmit(sk, mss, TCP_NAGLE_OFF, 2, GFP_ATOMIC);
        if (tp->packets_out > pcount)
            goto probe_sent;
        goto rearm_timer;
    }

否则,套接口没有未发送数据,将再次发送重传队列最后端的报文,如果TLP重传过程正在进行(tlp_high_seq不为零),或者之前的重传报文还在本机的Qdisc/驱动程序队列中,跳转到启动RTO定时器,避免发送重复的背靠背TLP探测报文。

    skb = skb_rb_last(&sk->tcp_rtx_queue);

    /* At most one outstanding TLP retransmission. */
    if (tp->tlp_high_seq)
        goto rearm_timer;

    if (skb_still_in_host_queue(sk, skb))
        goto rearm_timer;

    pcount = tcp_skb_pcount(skb);
    if (WARN_ON(!pcount)) goto rearm_timer;

如果skb使用TSO发送,并且报文总长度不是MSS值的整数倍,将非整数倍长度的数据分离成一个新报文,重传此报文,并且记录下SND.NXT下一个发送序号到tlp_high_seq中。最后,如果成功发送TLP探测报文,清除TLP定时器,重新启动RTO定时器。

    if ((pcount > 1) && (skb->len > (pcount - 1) * mss)) {
        if (unlikely(tcp_fragment(sk, TCP_FRAG_IN_RTX_QUEUE, skb,
                      (pcount - 1) * mss, mss, GFP_ATOMIC)))
            goto rearm_timer;
        skb = skb_rb_next(skb);
    }

    if (WARN_ON(!skb || !tcp_skb_pcount(skb)))
        goto rearm_timer;

    if (__tcp_retransmit_skb(sk, skb, 1))
        goto rearm_timer;

    /* Record snd_nxt for loss detection. */
    tp->tlp_high_seq = tp->snd_nxt;

probe_sent:
    NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPLOSSPROBES);
    /* Reset s.t. tcp_rearm_rto will restart timer from now */
    inet_csk(sk)->icsk_pending = 0;
rearm_timer:
    tcp_rearm_rto(sk);

由以上函数可知,无论TLP探测报文是否发送,都将重新调度RTO定时器。函数tcp_rearm_rto如下,如果TLP探测报文未能发送,但是TLP定时器已经消耗了一定的时间,重新计算此时RTO超时的剩余时间。否则如果TLP探测报文发送完成,启动一个完整时长的RTO定时器。

void tcp_rearm_rto(struct sock *sk)
{
    if (!tp->packets_out) {
        inet_csk_clear_xmit_timer(sk, ICSK_TIME_RETRANS);
    } else {
        u32 rto = inet_csk(sk)->icsk_rto;
        /* Offset the time elapsed after installing regular RTO */
        if (icsk->icsk_pending == ICSK_TIME_REO_TIMEOUT ||
            icsk->icsk_pending == ICSK_TIME_LOSS_PROBE) {
            s64 delta_us = tcp_rto_delta_us(sk);

            /* delta_us may not be positive if the socket is locked
             * when the retrans timer fires and is rescheduled.
             */
            rto = usecs_to_jiffies(max_t(int, delta_us, 1));
        }
        tcp_reset_xmit_timer(sk, ICSK_TIME_RETRANS, rto, TCP_RTO_MAX, tcp_rtx_queue_head(sk));
    }

TLP报文ACK处理

如下ACK报文处理函数tcp_ack,如果tlp_high_seq有值,表明发送了TLP探测报文,由函数tcp_process_tlp_ack处理ACK。标志标量flag如果设置了FLAG_SET_XMIT_TIMER,表明此ACK报文确认了新的数据,需要重新调整TLP/RTO定时器。

static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
    u32 ack_seq = TCP_SKB_CB(skb)->seq;
    u32 ack = TCP_SKB_CB(skb)->ack_seq;

    if (tp->tlp_high_seq)
        tcp_process_tlp_ack(sk, ack, flag);
    /* If needed, reset TLP/RTO timer; RACK may later override this. */
    if (flag & FLAG_SET_XMIT_TIMER)
        tcp_set_xmit_timer(sk);

以下TLP周期内的ACK处理函数,如果当前报文的ACK序号在tlp_high_seq之前,表明不是对TLP探测报文的确认,不进行处理。如果此ACK报文中包含重复SACK(DSACK)块,说明对端收到了原始和TLP探测两份报文,没有丢失情况发生,清空tlp_high_seq。

static void tcp_process_tlp_ack(struct sock *sk, u32 ack, int flag)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (before(ack, tp->tlp_high_seq))
        return;

    if (flag & FLAG_DSACKING_ACK) {
        /* This DSACK means original and TLP probe arrived; no loss */
        tp->tlp_high_seq = 0;

否则,如果ACK确认序号在tlp_high_seq序号之后,表明未接收到对TLP探测报文的确认,报文已经丢失,将在函数tcp_init_cwnd_reduction中清零tlp_high_seq。

    } else if (after(ack, tp->tlp_high_seq)) {
        /* ACK advances: there was a loss, so reduce cwnd. Reset
         * tlp_high_seq in tcp_init_cwnd_reduction()
         */
        tcp_init_cwnd_reduction(sk);
        tcp_set_ca_state(sk, TCP_CA_CWR);
        tcp_end_cwnd_reduction(sk);
        tcp_try_keep_open(sk);
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPLOSSPROBERECOVERY);

最后,如果ACK确认了TLP探测报文,并且此ACK报文并没有移动SND.UNA值,也不携带数据,并非窗口更新,也没有包含SACK块,表明其为一个单纯的重复ACK报文,即对端已经接收到原始的报文和TLP探测报文,清零tlp_high_seq,结束TLP。

    } else if (!(flag & (FLAG_SND_UNA_ADVANCED | FLAG_NOT_DUP | FLAG_DATA_SACKED))) {
        /* Pure dupack: original and TLP probe arrived; no loss */
        tp->tlp_high_seq = 0;

在TLP/RTO定时器重设函数中,如果TLP调度失败,设置RTO定时器。

/* Try to schedule a loss probe; if that doesn't work, then schedule an RTO. */
static void tcp_set_xmit_timer(struct sock *sk)
{
    if (!tcp_schedule_loss_probe(sk, true))
        tcp_rearm_rto(sk);
}

如果超时定时器RTO到期,TLP的探测报文及其确认ACK报文有可能都已经丢失或丢失了一个,清零tlp_high_seq,结束TLP过程。

void tcp_retransmit_timer(struct sock *sk)
{
    tp->tlp_high_seq = 0;

内核版本 5.0

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值