当前位置:网站首页>TCP协议之《延迟ACCEPT》

TCP协议之《延迟ACCEPT》

2022-08-10 03:26:00 程序员扫地僧

通常情况下,在一个新的TCP连接完成三次握手之后,监听端的accept系统调用就可返回与此对应的子套接口。然而,TCP的延迟ACCEPT功能,允许TCP监听端仅在接收到客户端的数据报文后才去唤醒服务端应用的ACCEPT请求。

一、延迟ACCEPT开启
应用层通过setsockopt系统调用的选项TCP_DEFER_ACCEPT控制延迟ACCEPT功能。用户层下发以秒为单位的数值,内核转换为重传次数值使用,赋值于rskq_defer_accept变量。设置为0秒将关闭此功能。

static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen)
{
    switch (optname) {
    case TCP_DEFER_ACCEPT:
        /* Translate value in seconds to number of retransmits */
        icsk->icsk_accept_queue.rskq_defer_accept = secs_to_retrans(val, TCP_TIMEOUT_INIT / HZ, TCP_RTO_MAX / HZ);
        break;
}
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ)) /* RFC6298 2.1 initial RTO value    */
#define TCP_RTO_MAX ((unsigned)(120*HZ))
转换函数secs_to_retrans的第一个参数为用户层下发的秒值,后两个分别为以秒为单位的TCP初始重传超时时长和最大重传超时时长RTO,参见宏定义两者的值为别为1秒和120秒。

如果用户层下发值大于0的话,最终得到的重传次数值在1到255之间。以重传初始时长timeout开始,每次重传时长递增一倍,知道重传超时的最大值rto_max,之后的重传超时不变使用最大值,知道超时时长大于用户下发值为止,推算出对应的重传次数。

/* Convert seconds to retransmits based on initial and max timeout */
static u8 secs_to_retrans(int seconds, int timeout, int rto_max)
{
    if (seconds > 0) {
        int period = timeout;
 
        res = 1;
        while (seconds > period && res < 255) {
            res++;
            timeout <<= 1;
            if (timeout > rto_max)
                timeout = rto_max;
            period += timeout;
        }
    }
    return res;
}

在用户层使用系统调用getsockopt的选项TCP_DEFER_ACCEPT获取当前的延迟ACCEPT时长值时,内核使用retrans_to_secs函数将重传次数转换为以秒为单位的时间值,返回给用户层。其实现与以上函数secs_to_retrans的逻辑正相反。

static int do_tcp_getsockopt(struct sock *sk, int level, int optname, char __user *optval, int __user *optlen)
{
    switch (optname) {
    case TCP_DEFER_ACCEPT:
        val = retrans_to_secs(icsk->icsk_accept_queue.rskq_defer_accept, TCP_TIMEOUT_INIT / HZ, TCP_RTO_MAX / HZ);
        break;
}
/* Convert retransmits to seconds based on initial and max timeout */
static int retrans_to_secs(u8 retrans, int timeout, int rto_max)
{
    if (retrans > 0) {
        period = timeout;
        while (--retrans) {
            timeout <<= 1;
            if (timeout > rto_max)
                timeout = rto_max;
            period += timeout;
        }
    }
    return period;
}

二、服务端接收检查

当处于TCP_NEW_SYN_RECV的请求套接口,接收到对端的报文(通常为ACK)之后,调用函数tcp_check_req进行检查。如果此报文的结束序号end_seq等于请求套接口记录的初始接收序号(rcv_isn)加1,通常rcv_isn为SYN报文使用的序列号,意味着当前报文为无数据的单纯ACK确认报文。并且,超时重传的次数还未超出设定的rskq_defer_accept次数值。内核将请求套接口的acked置位,表明已接收到对端ACK报文,但是并不创建相应的子套接口。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen)
{
    /* While TCP_DEFER_ACCEPT is active, drop bare ACK. */
    if (req->num_timeout < inet_csk(sk)->icsk_accept_queue.rskq_defer_accept && TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {
        inet_rsk(req)->acked = 1;
        __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPDEFERACCEPTDROP);
        return NULL;
    }
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);
    
    return inet_csk_complete_hashdance(sk, child, req, own_req);
}
 

三、服务端发送超时
当服务端回复给客户端SYN+ACK报文之后,会启动一个(rsk_timer)超时定时器,以便在超时之后还未能接收到客户端相应的ACK报文,重新发送SYN+ACK报文。由以上的函数tcp_check_req的结束可知,如果延长ACCEPT发挥了作用,内核未能创建子套接口,也没调用到能够停止超时定时器的函数inet_csk_complete_hashdance。所以到时间后,超时处理函数reqsk_timer_handler得到运行。

static void reqsk_timer_handler(struct timer_list *t)
{
    struct request_sock *req = from_timer(req, t, rsk_timer);
    struct sock *sk_listener = req->rsk_listener;
    struct net *net = sock_net(sk_listener);
    struct inet_connection_sock *icsk = inet_csk(sk_listener);
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
 
    max_retries = icsk->icsk_syn_retries ? : net->ipv4.sysctl_tcp_synack_retries;
    thresh = max_retries;
 
    defer_accept = READ_ONCE(queue->rskq_defer_accept);
    if (defer_accept)
        max_retries = defer_accept;
    syn_ack_recalc(req, thresh, max_retries, defer_accept, &expire, &resend);
 
    if (!expire && (!resend || !inet_rtx_syn_ack(sk_listener, req) || inet_rsk(req)->acked)) {
 
        if (req->num_timeout++ == 0)
            atomic_dec(&queue->young);
        timeo = min(TCP_TIMEOUT_INIT << req->num_timeout, TCP_RTO_MAX);
        mod_timer(&req->rsk_timer, jiffies + timeo);
        return;
    }
drop:
    inet_csk_reqsk_queue_drop_and_put(sk_listener, req);
}

关键判断函数syn_ack_recalc,根据重传次数阈值、延迟ACCEPT次数等参数来确定是否过期以及是否重新发送SYN+ACK报文。如果当前的超时重传次数num_timeout已经大于等于阈值,并且还未接收到对端ACK确认报文,内核认为此连接已失败置位expire。或者即使接收到了ACK报文,但是重传次数已经大于等于最大的重传次数max_retries,内核也认为此连接已失败置位expire变量。

如果为接收到对端的ACK确认报文(acked为零),或者当前的超时重传次数与延迟ACCEPT次数仅相差1次时,置位resend变量重新发送SYN+ACK报文。后一判断条件可为对端建立TCP连接提供保障。

如果expire为真,超时处理函数reqsk_timer_handler将消耗此连接(实际上连接还未真正建立)。否则,如果置位了resend变量,内核使用inet_rtx_syn_ack函数执行SYN+ACK报文的重传。下一次的超时时间设置为初始超时时长TCP_TIMEOUT_INIT左移当前超时次数得到的值,但是不超过最大的超出时长TCP_RTO_MAX。

/* 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)
{
    *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;
}
 

四、客户端检查

TCP客户端接收到服务端回复的SYN+ACK报文之后,进入函数tcp_rcv_synsent_state_process的处理逻辑。如果当前套接口有数据等待发送,或者开启了延迟ACCEPT功能,或者ACK策略设置为pingpong模式,客户端不会立即回复ACK确认报文。对于第一种情况,客户端尝试将数据与ACK一同发送,可减少一次报文发送。但是ACK的延迟时长不能超过TCP_DELACK_MAX定义的时长(200毫秒),如果超时之后,由ACK超时处理函数tcp_delack_timer负责ACK的发送。

static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
 
    if (th->ack) {
        if (!th->syn)
            goto discard_and_undo;
 
        if (sk->sk_write_pending || icsk->icsk_accept_queue.rskq_defer_accept || icsk->icsk_ack.pingpong) {
            inet_csk_schedule_ack(sk);
            tcp_enter_quickack_mode(sk);
            inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK, TCP_DELACK_MAX, TCP_RTO_MAX);
discard:
            tcp_drop(sk, skb);
            return 0;
        } else {
            tcp_send_ack(sk);
        }
        return -1;
    }
}
#define TCP_DELACK_MAX  ((unsigned)(HZ/5))  /* maximal time to delay before sending an ACK */

感谢redwingz博主分享优等文章

原网站

版权声明
本文为[程序员扫地僧]所创,转载请带上原文链接,感谢
https://blog.csdn.net/wuyongmao/article/details/126237201