TCP超时重传定时器梳理

注:本文分析基于3.10.107内核版本

简介

超时重传定时器是TCP连接可靠性的重要保证,其工作原理为TCP连接在发送某一个数据报文或者SYN报文后,该连接就会启动一个定时器,在规定时间内如果没有收到对端回复的ACK报文,那么定时器超时处理函数就重新发送数据,直到发送成功或者达到最大重传次数而上报错误为止。

定时器类型

一个TCP连接有多个定时器来保证TCP连接的可靠性及其传输效率。在include/net/inet_connection_sock.h中定义了以下几个定时器

#define ICSK_TIME_RETRANS   1   /* Retransmit timer */
#define ICSK_TIME_DACK      2   /* Delayed ack timer */
#define ICSK_TIME_PROBE0    3   /* Zero window probe timer */
#define ICSK_TIME_EARLY_RETRANS 4   /* Early retransmit timer */
#define ICSK_TIME_LOSS_PROBE    5   /* Tail loss probe timer */

上面五个定时器分别为:超时重传定时器、ACK延迟定时器、零窗口探测定时器、ER延迟定时器、尾部丢失探测定时器。
另外还有保活定时器、FIN_WAIT2定时器、TIME_WAIT定时器、SYNACK定时器。

但是在内核中并没有为每个定时器提供一个实例(timer_list),有些定时器共用一个实例,目前内核里有4个实例:

icsk->icsk_retransmit_timer:超时重传定时器、零窗口探测定时器、ER延迟定时器、尾部丢失探测定时器。
icsk->icsk_delack_timer:ACK延迟定时器。
sk->sk_timer:保活定时器,SYNACK定时器,FIN_WAIT2定时器。
death_row->tw_timer:TIME_WAIT定时器。

定时器的创建

这里我们主要关注超时重传定时器的创建流程,如下:
tcp_v4_init_sock
—->tcp_init_sock
——–>tcp_init_xmit_timers
————>inet_csk_init_xmit_timers
在连接初始化时,除了会创建超时重传定时器,ACK延迟定时器和保活定时器也会被一起创建。

void tcp_init_xmit_timers(struct sock *sk)
{
    inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer);
}

/*
 * Using different timers for retransmit, delayed acks and probes
 * We may wish use just one timer maintaining a list of expire jiffies
 * to optimize.
 */
void inet_csk_init_xmit_timers(struct sock *sk,
                   void (*retransmit_handler)(unsigned long),
                   void (*delack_handler)(unsigned long),
                   void (*keepalive_handler)(unsigned long))
{
    struct inet_connection_sock *icsk = inet_csk(sk);

    setup_timer(&icsk->icsk_retransmit_timer, retransmit_handler,
            (unsigned long)sk);
    setup_timer(&icsk->icsk_delack_timer, delack_handler,
            (unsigned long)sk);
    setup_timer(&sk->sk_timer, keepalive_handler, (unsigned long)sk);
    icsk->icsk_pending = icsk->icsk_ack.pending = 0;
}

定时器的激活

超时重传定时器的激活有很多种场景,目前个人有接触主要有以下两种:
1、TCP建链发送SYN报文时;
2、发送数据时,每收到对端的ACK报文,也会reset定时器。
我们从发送SYN报文流程里看:

/* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
    ...
    /* 这里是初始化传输控制块中与连接相关的成员
       其中就包括对重传超时时间RTO的初始化,对于
       SYN报文时1s,不同内核版本该值会不一样,在
       内核版本3.0.101(SUSE11SP3)上该值是3s。
    */
    tcp_connect_init(sk);
    ...

    /* Timer for repeating the SYN until an answer. 
       这里便是激活超时重传定时器了,其中TCP_RTO_MAX的值为120s。
    */
    inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                  inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
    return 0;
}

inet_csk_reset_xmit_timer函数里不仅负责对超时重传定时器进行重置,还负责其他4个定时器的激活。

/*
 *  Reset the retransmission timer
 */
static inline void inet_csk_reset_xmit_timer(struct sock *sk, const int what,
                         unsigned long when,
                         const unsigned long max_when)
{
    struct inet_connection_sock *icsk = inet_csk(sk);

    if (when > max_when) {
        when = max_when;
    }

    /* 重传定时器,零窗口探测定时器,ER定时器,PTO定时器以及迟定ACK定时器都通过该接口激活 */
    if (what == ICSK_TIME_RETRANS || what == ICSK_TIME_PROBE0 ||
        what == ICSK_TIME_EARLY_RETRANS || what ==  ICSK_TIME_LOSS_PROBE) {
        icsk->icsk_pending = what;
        icsk->icsk_timeout = jiffies + when;//报文超时时间
        sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);//重置定时器
    } else if (what == ICSK_TIME_DACK) {
        icsk->icsk_ack.pending |= ICSK_ACK_TIMER;
        icsk->icsk_ack.timeout = jiffies + when;
        sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
    }
}

超时处理函数

从上面定时器创建流程中我们知道,超时重传定时器的超时处理函数是tcp_write_timer()。

static void tcp_write_timer(unsigned long data)
{
    struct sock *sk = (struct sock *)data;

    bh_lock_sock(sk);
    if (!sock_owned_by_user(sk)) {// 什么情况下会被用户占用呢,不太理解,有知道的请赐教
        tcp_write_timer_handler(sk);
    } else {// 设置延迟标志,之后再处理
        /* deleguate our work to tcp_release_cb() */
        if (!test_and_set_bit(TCP_WRITE_TIMER_DEFERRED, &tcp_sk(sk)->tsq_flags))
            sock_hold(sk);
    }
    bh_unlock_sock(sk);
    sock_put(sk);
}

tcp_write_timer_handler()函数中根据触发的定时器类型,采取对应处理,如果是超时重传定时器会进入tcp_retransmit_timer()函数里处理。

/*
 *  The TCP retransmit timer.
 */

void tcp_retransmit_timer(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);

    if (tp->fastopen_rsk) {//fast open,让TCP三次握手过程也能交换数据,没太研究,略过
        WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV &&
                 sk->sk_state != TCP_FIN_WAIT1);
        tcp_fastopen_synack_timer(sk);
        /* Before we receive ACK to our SYN-ACK don't retransmit
         * anything else (e.g., data or FIN segments).
         */
        return;
    }
    //packets_out表示发送出去尚未收到对端确认的数据包,如果没有,超时就没意义,返回
    if (!tp->packets_out)
        goto out;

    WARN_ON(tcp_write_queue_empty(sk));

    tp->tlp_high_seq = 0;

    //收到对端窗口为0,sk状态不为SOCK_DEAD,同时也不处于TCP三次握手期间,也就是在established状态下出现了拥塞
    if (!tp->snd_wnd && !sock_flag(sk, SOCK_DEAD) &&
        !((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV))) {
        struct inet_sock *inet = inet_sk(sk);
        if (sk->sk_family == AF_INET) {
            LIMIT_NETDEBUG(KERN_DEBUG pr_fmt("Peer %pI4:%u/%u unexpectedly shrunk window %u:%u (repaired)\n"),
                       &inet->inet_daddr,
                       ntohs(inet->inet_dport), inet->inet_num,
                       tp->snd_una, tp->snd_nxt);
        }
        // 距离上次接收到ACK的时间超过TCP_RTO_MAX,120s,就认为有错误发生了
        if (tcp_time_stamp - tp->rcv_tstamp > TCP_RTO_MAX) {
            tcp_write_err(sk);//最终通过ICMP报文上报错误到应用层
            goto out;
        }
        //报文丢失,要进行拥塞处理和标识丢失数据段
        tcp_enter_loss(sk, 0);
        /* 重传发送队列里的第一个数据段 */
        tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
        __sk_dst_reset(sk);//刷路由
        goto out_reset_timer;
    }

    // TCP三次握手期间的定时器超时会走到下面的流程,判断连接是否超时
    if (tcp_write_timeout(sk))
        goto out;

    if (icsk->icsk_retransmits == 0) {//第一次进入超时重传流程
        int mib_idx;

        if (icsk->icsk_ca_state == TCP_CA_Recovery) {
            if (tcp_is_sack(tp))
                mib_idx = LINUX_MIB_TCPSACKRECOVERYFAIL;
            else
                mib_idx = LINUX_MIB_TCPRENORECOVERYFAIL;
        } else if (icsk->icsk_ca_state == TCP_CA_Loss) {
            mib_idx = LINUX_MIB_TCPLOSSFAILURES;
        } else if ((icsk->icsk_ca_state == TCP_CA_Disorder) ||
               tp->sacked_out) {
            if (tcp_is_sack(tp))
                mib_idx = LINUX_MIB_TCPSACKFAILURES;
            else
                mib_idx = LINUX_MIB_TCPRENOFAILURES;
        } else {
            mib_idx = LINUX_MIB_TCPTIMEOUTS;
        }
        NET_INC_STATS_BH(sock_net(sk), mib_idx);
    }

    tcp_enter_loss(sk, 0);

    /* 重传发送队列里的第一个数据段 */
    if (tcp_retransmit_skb(sk, tcp_write_queue_head(sk)) > 0) {
        /* Retransmission failed because of local congestion,
         * do not backoff. 尚未仔细研究什么情况会出现返回值大于零
         */
        if (!icsk->icsk_retransmits)
            icsk->icsk_retransmits = 1;
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                      min(icsk->icsk_rto, TCP_RESOURCE_PROBE_INTERVAL),
                      TCP_RTO_MAX);
        goto out;
    }

    icsk->icsk_backoff++;//增加指数退避次数
    icsk->icsk_retransmits++;//增加重传次数

out_reset_timer:
    if (sk->sk_state == TCP_ESTABLISHED &&
        (tp->thin_lto || sysctl_tcp_thin_linear_timeouts) &&
        tcp_stream_is_thin(tp) &&
        icsk->icsk_retransmits <= TCP_THIN_LINEAR_RETRIES) {
        icsk->icsk_backoff = 0;
        icsk->icsk_rto = min(__tcp_set_rto(tp), TCP_RTO_MAX);
    } else {
        /* Use normal (exponential) backoff */
        icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);//正常指数退避
    }
    inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);//重置定时器
    //当距离原始包发送6.2s后还未收到应答包时,就要刷新路由
    if (retransmits_timed_out(sk, sysctl_tcp_retries1 + 1, 0, 0))
        __sk_dst_reset(sk);//刷路由

out:;
}

重传到一定次数后就该返回超时了,在tcp_write_timeout里判断连接是否超时。

/* A write timeout has occurred. Process the after effects. */
static int tcp_write_timeout(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    int retry_until;
    bool do_reset, syn_set = false;

    if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {//TCP三次握手期间
        if (icsk->icsk_retransmits)
            dst_negative_advice(sk);//之前重传过,那就要更新路由缓存了
        // 获取重传次数,对于SYN报文系统默认5次,即内核参数sysctl_tcp_syn_retries,
        // 用户可以通过setsockopt的TCP_SYNCNT选项设置该参数,也可以通过修改/proc里面的内核参数
        retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;
        syn_set = true;//设置syn标识
    } else {
        // tcp_retries1默认为3,当数据报文重传次数超过此值时,也就是距离原始包发送3s后还未收到ACK报文
        // 表示可能遇到了黑洞,需要进行PMTU检测,并刷新路由
        if (retransmits_timed_out(sk, sysctl_tcp_retries1, 0, 0)) {
            /* Black hole detection */
            tcp_mtu_probing(icsk, sk);

            dst_negative_advice(sk);
        }

        retry_until = sysctl_tcp_retries2;//数据报文重传次数默认15次
        if (sock_flag(sk, SOCK_DEAD)) {
            const int alive = (icsk->icsk_rto < TCP_RTO_MAX);

            retry_until = tcp_orphan_retries(sk, alive);
            do_reset = alive ||
                !retransmits_timed_out(sk, retry_until, 0, 0);

            if (tcp_out_of_resources(sk, do_reset))
                return 1;
        }
    }

    //判断连接是否超时
    if (retransmits_timed_out(sk, retry_until,
                  syn_set ? 0 : icsk->icsk_user_timeout, syn_set)) {
        /* Has it gone just too far? */
        tcp_write_err(sk);//最终通过ICMP报文上报错误
        return 1;
    }
    return 0;
}


static bool retransmits_timed_out(struct sock *sk,
                  unsigned int boundary,
                  unsigned int timeout,
                  bool syn_set)
{
    unsigned int linear_backoff_thresh, start_ts;
    //如果是SYN包,则初始超时时间为1s,上面说过不同内核该值可能不一样
    //如果是数据包,则为200ms
    unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN;

    if (!inet_csk(sk)->icsk_retransmits)//之前没有重传,何来超时
        return false;

    //在tcp_connect()函数中会对retrans_stamp进行赋值,因此超时时间计算使用的是原始包发送的时间戳来计算
    if (unlikely(!tcp_sk(sk)->retrans_stamp))
        start_ts = TCP_SKB_CB(tcp_write_queue_head(sk))->when;
    else
        start_ts = tcp_sk(sk)->retrans_stamp;

    //有两种情况:对于SYN包,timeout始终设置为0,因为在三次握手期间TCP_USER_TIMEOUT是无效的
    //对于非SYN包,如果用户没有设置TCP_USER_TIMEOUT,那timeout也为0
    if (likely(timeout == 0)) {
        //对于SYN包,rto_base为1s,可求得最大指数退避次数为log2(120),向上取整为7次
        //SYN包的boundary为5次
        linear_backoff_thresh = ilog2(TCP_RTO_MAX/rto_base);

        if (boundary <= linear_backoff_thresh)
            //正常情况都是按这个计算超时时间,对于SYN报文超时时间是63s
            timeout = ((2 << boundary) - 1) * rto_base;
        else
            timeout = ((2 << linear_backoff_thresh) - 1) * rto_base +
                (boundary - linear_backoff_thresh) * TCP_RTO_MAX;
    }
    return (tcp_time_stamp - start_ts) >= timeout;
}

也就是说,对于SYN报文,需要重传5次,历时63s才能最终返回超时。

这个可以通过一个实验来证实。在A、B两台服务器上,A向B发起连接,B上把防火墙默认规则设置为DROP,然后用tcpdump抓包,就可以看到A一共向B发送了6次SYN报文,然后A发起的连接才返回timeout。


参考资料:
1、http://blog.csdn.net/zhangskd/article/details/35281345

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值