TCP SYNACK定时器梳理

来源:互联网 发布:优化mysql数据库的方法 编辑:程序博客网 时间:2024/05/24 02:40

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

问题:在TCP建链的三次握手中,如果服务端回复的SYN+ACK报文在传输过程中丢失,或者客户端接收到这个SYN+ACK报文,但是第三次握手回复的ACK报文在传输过程中丢失了,服务端以什么样的方式感知,这条链接又如何结束?

这便是SYNACK定时器的工作了。

激活定时器

在介绍listen()函数的backlog参数时,我们知道服务端接收到SYN报文后会进入tcp_v4_conn_request()函数进行进一步的处理。

int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb){    ....    //前面的流程省略,主要关注下面几个操作    //tcp fastopen特性,暂且认为我们未开启    do_fastopen = tcp_fastopen_check(sk, skb, req, &foc, &valid_foc);    // 准备构建SYN+ACK报文    skb_synack = tcp_make_synack(sk, dst, req,        fastopen_cookie_present(&valid_foc) ? &valid_foc : NULL);    if (skb_synack) {        //SYN+ACK报文的一些检查校验        __tcp_v4_send_check(skb_synack, ireq->loc_addr, ireq->rmt_addr);        skb_set_queue_mapping(skb_synack, skb_get_queue_mapping(skb));    } else        goto drop_and_free;    if (likely(!do_fastopen)) {        int err;        //这里就要构建SYN+ACK报文的IP头了,然后发送        err = ip_build_and_send_pkt(skb_synack, sk, ireq->loc_addr,             ireq->rmt_addr, ireq->opt);        err = net_xmit_eval(err);        if (err || want_cookie)            goto drop_and_free;        //记录SYN+ACK报文的发送时间        tcp_rsk(req)->snt_synack = tcp_time_stamp;        tcp_rsk(req)->listener = NULL;        //这里就是我们关注的将连接请求加入半连接队列的操作        inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);        if (fastopen_cookie_present(&foc) && foc.len != 0)            NET_INC_STATS_BH(sock_net(sk),                LINUX_MIB_TCPFASTOPENPASSIVEFAIL);    } else if (tcp_v4_conn_req_fastopen(sk, skb, skb_synack, req))        goto drop_and_free;    return 0;    ....}void inet_csk_reqsk_queue_hash_add(struct sock *sk, struct request_sock *req,                   unsigned long timeout){    struct inet_connection_sock *icsk = inet_csk(sk);    struct listen_sock *lopt = icsk->icsk_accept_queue.listen_opt;    const u32 h = inet_synq_hash(inet_rsk(req)->rmt_addr, inet_rsk(req)->rmt_port,                     lopt->hash_rnd, lopt->nr_table_entries);    //把请求加入半连接队列,放入syn_table的哈希桶中    reqsk_queue_hash_req(&icsk->icsk_accept_queue, h, req, timeout);    //更新半连接队列的统计信息    inet_csk_reqsk_queue_added(sk, timeout);}static inline void inet_csk_reqsk_queue_added(struct sock *sk,                          const unsigned long timeout){    //如果此前半连接队列不为0,说明SYNACK定时器已经激活    if (reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue) == 0)        //激活SYNACK定时器        inet_csk_reset_keepalive_timer(sk, timeout);}static inline int reqsk_queue_added(struct request_sock_queue *queue){    //别看入参数什么accept_queue,实际上操作的是结构体中的半连接队列    struct listen_sock *lopt = queue->listen_opt;    const int prev_qlen = lopt->qlen;    lopt->qlen_young++;//半连接队列中尚未重传过的连接数量    lopt->qlen++;//半连接队列长度    return prev_qlen;//返回该连接到达前的半连接队列长度}

定时器超时处理

由之前超时重传定时器的介绍中可知,保活定时器,SYNACK定时器,FIN_WAIT2定时器这三个定时器是同一个超时处理函数——tcp_keepalive_timer(),具体根据socket的状态来区分。

static void tcp_keepalive_timer (unsigned long data){    struct sock *sk = (struct sock *) data;    struct inet_connection_sock *icsk = inet_csk(sk);    struct tcp_sock *tp = tcp_sk(sk);    u32 elapsed;    /* Only process if socket is not in use. */    bh_lock_sock(sk);    if (sock_owned_by_user(sk)) {        //如果这个socket正在被用户使用,那么推迟50ms再处理        inet_csk_reset_keepalive_timer (sk, HZ/20);        goto out;    }    //socket状态为LISTEN,那肯定就是SYNACK定时器了    if (sk->sk_state == TCP_LISTEN) {        tcp_synack_timer(sk);        goto out;    }    //这里就是FIN_WAIT2定时器的处理    if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {        if (tp->linger2 >= 0) {            const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;            if (tmo > 0) {                tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);                goto out;            }        }        tcp_send_active_reset(sk, GFP_ATOMIC);        goto death;    }    ...    //往后便是保活定时器的处理,这里先省略,后续讲到时再说}#define TCP_SYNQ_INTERVAL   (HZ/5)  /* SYNACK定时器周期 */#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value */#define TCP_RTO_MAX ((unsigned)(120*HZ))static void tcp_synack_timer(struct sock *sk){    inet_csk_reqsk_queue_prune(sk, TCP_SYNQ_INTERVAL,                   TCP_TIMEOUT_INIT, TCP_RTO_MAX);}void inet_csk_reqsk_queue_prune(struct sock *parent,                const unsigned long interval,                const unsigned long timeout,                const unsigned long max_rto){    struct inet_connection_sock *icsk = inet_csk(parent);    struct request_sock_queue *queue = &icsk->icsk_accept_queue;    struct listen_sock *lopt = queue->listen_opt;    //SYN+ACK报文最大重传次数,如果用户没有设置,则使用sysctl_tcp_synack_retries的值    //即/proc/sys/net/ipv4/tcp_synack_retries,默认为5次,但是这个次数并不是固定的,后面会讲到    int max_retries = icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;    int thresh = max_retries;    unsigned long now = jiffies;    struct request_sock **reqp, *req;    int i, budget;    if (lopt == NULL || lopt->qlen == 0)        return;    //qlen是半连接队列的长度,max_qlen_log是半连接队列最大长度的对数    //也就是说当半连接队列的长度超过最大长度的一半,那就要对SYNACK报文的重传次数做调整    if (lopt->qlen>>(lopt->max_qlen_log-1)) {        int young = (lopt->qlen_young<<1);        //qlen_young变量代表的是半连接队列里尚未重传过的连接        //所以调整SYNACK重传次数的依据是半连接队列里尚未重传过连接的数量和队列长度的比值        //毕竟我们不能无限的重传下去,要考虑新连接,旧连接超时后就丢弃        //循环最后的结果为:        //尚未重传过连接的数量和队列长度的比值ratio 和 SYNACK报文重传次数thresh如下        // 1/2 <= ratio < 1/4   thresd = 4        // 1/4 <= ratio < 1/8   thresh = 3        // 1/8 <= ratio < 1/16  thresh = 2        while (thresh > 2) {            if (lopt->qlen < young)                break;            thresh--;            young <<= 1;        }    }    //如果用户设置了TCP_DEFER_ACCEPT选项,那么最大重传次数由用户设定    if (queue->rskq_defer_accept)        max_retries = queue->rskq_defer_accept;    //nr_table_entries的值为半连接队列的最大长度    //这个budget变量的意思就是保证一个定时器周期里最少遍历两次半连接队列,丢弃老连接    //为什么说最少呢,因为半连接队列长度不一定能达到nr_table_entries,    //也就有可能遍历3次,4次,或者不到3次。因此需要一个变量来记录遍历到的位置,    //以保证公平性,也为了快速获取上次遍历位置,这个变量就是clock_hand    budget = 2 * (lopt->nr_table_entries / (timeout / interval));    i = lopt->clock_hand;    do {        //syn_table里放的就是半连接请求        reqp=&lopt->syn_table[i];        while ((req = *reqp) != NULL) {            //判断该请求是否超时            if (time_after_eq(now, req->expires)) {                int expire = 0, resend = 0;                syn_ack_recalc(req, thresh, max_retries,                           queue->rskq_defer_accept,                           &expire, &resend);                req->rsk_ops->syn_ack_timeout(parent, req);                //如果没超时,那resend也为1,也就会执行inet_rtx_syn_ack()                //这个函数就是在重传SYNACK报文,报文发送成功后更新信息                if (!expire &&                    (!resend || !inet_rtx_syn_ack(parent, req) ||                     inet_rsk(req)->acked)) {                    unsigned long timeo;                    //增加超时次数计数                    //暂时不清楚为什么有两个计数,一个超时次数,一个重传次数                    if (req->num_timeout++ == 0)                        //重传后更新半连接队列的统计,减少未重传报文的数量                        lopt->qlen_young--;                    //重传时间间隔也是按照指数退避的方式增加,最大不超过max_rto,120s                    timeo = min(timeout << req->num_timeout,                            max_rto);                    //更新请求连接的的超时时间                    req->expires = now + timeo;                    reqp = &req->dl_next;                    continue;                }                //如果请求块重传次数到了,那就直接丢弃该连接                inet_csk_reqsk_queue_unlink(parent, req, reqp);                reqsk_queue_removed(queue, req);                reqsk_free(req);                continue;            }            reqp = &req->dl_next;        }        i = (i + 1) & (lopt->nr_table_entries - 1);    } while (--budget > 0);    //clock_hand变量用来记录遍历的位置,方便下次遍历    lopt->clock_hand = i;    if (lopt->qlen)        //如果半连接队列不为空,重置定时器        inet_csk_reset_keepalive_timer(parent, interval);}/* Decide when to expire the request and when to resend SYN-ACK */static inline void syn_ack_recalc(struct request_sock *req, const int thresh,                  const int max_retries,                  const u8 rskq_defer_accept,                  int *expire, int *resend){    if (!rskq_defer_accept) {        //如果用户没有设置TCP_DEFER_ACCEPT,判断重传次数是否达到阈值即可        *expire = req->num_timeout >= thresh;        *resend = 1;        return;    }    *expire = req->num_timeout >= thresh &&          (!inet_rsk(req)->acked || req->num_timeout >= max_retries);    /*     * Do not resend while waiting for data after ACK,     * start to resend on end of deferring period to give     * last chance for data or ACK to create established socket.     */    *resend = !inet_rsk(req)->acked ||          req->num_timeout >= rskq_defer_accept - 1;}int inet_rtx_syn_ack(struct sock *parent, struct request_sock *req){    //调用tcp_v4_rtx_synack()发送synack报文    int err = req->rsk_ops->rtx_syn_ack(parent, req);    if (!err)        //增加重传次数计数        req->num_retrans++;    return err;}

所以,总的来说,SYNACK定时器的处理和超时重传定时器类似,都以指数退避来作为报文重传间隔,只是SYNACK定时器会根据连接状况调整重传次数,这应该是为了服务端能够处理更多的连接而考虑。


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

原创粉丝点击