• TCP协议之《套接口快速接收路径》


    TCP套接口的快速路径开启的先决条件可由函数tcp_fast_path_check一窥究竟,分别如下:乱序out_of_order_queue队列为空、通告的接收窗口非空(rcv_wnd)、接收缓存sk_rmem_alloc小于套接口的限定值、没有urgent紧急数据在传输。

    static inline void tcp_fast_path_check(struct sock *sk)
    {
        struct tcp_sock *tp = tcp_sk(sk);
     
        if (RB_EMPTY_ROOT(&tp->out_of_order_queue) &&
            tp->rcv_wnd &&
            atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf &&
            !tp->urg_data)
            tcp_fast_path_on(tp);
    }


    快速路径使能的结果是对套接口结构成员pred_flags的配置,套接口连接的数据报文TCP头部长度保存在pred_flags的第31位到28位,共4个位数中;TCP_FLAG_ACK标志保存在第20比特位(宏TCP_FLAG_ACK定义为网络字节序);发送窗口大小保存在低16个比特位中。

    static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd)
    {
        tp->pred_flags = htonl((tp->tcp_header_len << 26) | ntohl(TCP_FLAG_ACK) | snd_wnd);
    }
    static inline void tcp_fast_path_on(struct tcp_sock *tp)
    {
        __tcp_fast_path_on(tp, tp->snd_wnd >> tp->rx_opt.snd_wscale);
    }


    最终的pred_flags变量相当于TCP报文头部的第三个32bit字段,只是将Reserved字段和除去ACK位的flags标志字段设置为了零。

     

    一、快速路径的开启时机

    在接收到保序的数据包后,函数tcp_data_queue将其加入到接收队列中,并且检查乱序队列中的数据包是否可合并到接收队列中,以上完成后,进行TCP接收的快速路径检查,如条件符合,为后续报文打开快速路径接收。

    static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
    {
        if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
            if (tcp_receive_window(tp) == 0)
                goto out_of_window;
     
            eaten = tcp_queue_rcv(sk, skb, 0, &fragstolen);
            if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
                tcp_ofo_queue(sk);
                if (RB_EMPTY_ROOT(&tp->out_of_order_queue))
                    inet_csk(sk)->icsk_ack.pingpong = 0;
            }
            tcp_fast_path_check(sk);
            return;
        }  
    }

    在TCP连接三次握手完成之后,内核默认为服务端(被动打开端)开启快速路径接收功能,此时连接刚刚建立,快速路径的开启条件都成立,没有必要使用tcp_fast_path_check函数。

    int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
    {
        switch (sk->sk_state) {
        case TCP_SYN_RECV:
            tcp_set_state(sk, TCP_ESTABLISHED);
            sk->sk_state_change(sk);
     
            tcp_fast_path_on(tp);
            break;
        }
    }
    但是对应主动开启连接的TCP客户端,在其接收到服务端的SYN+ACK报文后,在函数tcp_ack处理ACK过程中调用tcp_ack_update_window更新窗口时,根据条件判断是否需要开启快速接收路径。另外,随后的函数tcp_finish_connect函数除了将套接口状态设置为已建立TCP_ESTABLISHED外,还会根据接收到的服务端TCP窗口系数选项是否为0设置快速路径。如果服务端窗口系数选项有值禁用快速路径,否则开启之。

    static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
    {
        if (th->ack) {
            tcp_ack(sk, skb, FLAG_SLOWPATH);
            tcp_finish_connect(sk, skb);
        }
    }
    void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
    {
        tcp_set_state(sk, TCP_ESTABLISHED);
     
        if (!tp->rx_opt.snd_wscale)
            __tcp_fast_path_on(tp, tp->snd_wnd);
        else
            tp->pred_flags = 0;
    }

    如果当前TCP套接口处在慢速路径接收状态,内核更新套接口窗口值时,会检查是否可开启快速路径。TCP的对端通过新的窗口通告表明此刻本端为发送端,如果能够开启快速路径,将为之后可能要接收的数据进行快速处理。

    static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack, u32 ack_seq)
    {
        if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {
            flag |= FLAG_WIN_UPDATE;
            tcp_update_wl(tp, ack_seq);
     
            if (tp->snd_wnd != nwin) {
                tp->snd_wnd = nwin;
     
                /* Note, it is the only place, where
                 * fast path is recovered for sending TCP.
                 */
                tp->pred_flags = 0;
                tcp_fast_path_check(sk);
            }
        }
    }

    在应用层接收函数的处理中,以下函数tcp_recvmsg,如果判断套接口中的紧急数据urg_data已经被拷贝给上层应用,消除了一个阻碍快速路径开启的条件,调用tcp_fast_path_check进行开启检查。

    int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len)
    {
        do {
            last = skb_peek_tail(&sk->sk_receive_queue);
            skb_queue_walk(&sk->sk_receive_queue, skb) {
            }
     
    skip_copy:
            if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
                tp->urg_data = 0;
                tcp_fast_path_check(sk);
            }
        } while (len > 0);
    }


    二、快速路径接收
    除了以上介绍的快速路径开启条件,在接收函数tcp_rcv_established中内核还要进行一些其它的条件检查。

    /*
     *  It is split into a fast path and a slow path. The fast path is
     *  disabled when:
     *  - A zero window was announced from us - zero window probing is only handled properly in the slow path.
     *  - Out of order segments arrived.
     *  - Urgent data is expected.
     *  - There is no buffer space left
     *  - Unexpected TCP flags/window values/header lengths are received
     *    (detected by checking the TCP header against pred_flags)
     *  - Data is sent in both directions. Fast path only supports pure senders or pure receivers (this means either the sequence number or the ack
     *    value must stay constant)
     *  - Unexpected TCP option.
     */
    void tcp_rcv_established(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
    {
        unsigned int len = skb->len;

    首先判断TCP报文头部的第三个32bit字段是否与开启快速路径时保存的pred_flags值相同,在比较之前忽略掉TCP头部的TCP_HP_BITS定义的位,即Reserved和PSH位,二者相同意味着TCP的其它位段没有发生改变。TCP数据包的开始序号seq要等于套接口正在等待的序号数据;并且数据包的确认序号ack_seq不能大于(小于或者等于)套接口下一个要发送的序号报文snd_nxt,表明对端没有在接收数据,快速路径不允许两端同时发送接收数据。

        if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags && TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
            !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
            int tcp_header_len = tp->tcp_header_len;
    其次,尝试解析TCP的timestamp选项,如果TCP头的长度不等于标准长度与timestamp选项长度之和,略去选项解析。否则,判读是否为timestamp选项,如果不是转到慢速路径处理,或者timestamp选项中携带的时间戳小于最近一次接收到的时间戳,PAWS检查失败,同样跳转到慢速路径。注意在此处内核不会更新最近一次接收的时间戳ts_recent的值,因为还没有对数据包进行checksum检验,假如更新成一个混乱的时间戳值,比如非常大的值,将导致后续的报文不能够通过PAWS检测,而全部被丢弃。    

            if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
                if (!tcp_parse_aligned_timestamp(tp, th))
                    goto slow_path;
                if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
                    goto slow_path;
            }
    如果数据包的长度与TCP报文头部长度相等,没有数据部分,仅为ACK确认报文。此类型报文已在TCP接收入口处进行了checksum校验,可进行时间戳ts_recent的更新。随后处理此ACK报文,检查是否还有数据包可进行发送。对于长度小于TCP头部长度的数据包,直接丢弃。在快速路径中,如果本端为数据发送端,将接收到对端的大量ACK报文,在此处进行处理。

            if (len <= tcp_header_len) {
                if (len == tcp_header_len) {
                    /* Predicted packet is in window by definition. seq == rcv_nxt and rcv_wup <= rcv_nxt. Hence, check seq<=rcv_wup reduces to: */
                    if (tcp_header_len == (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) && tp->rcv_nxt == tp->rcv_wup)
                        tcp_store_ts_recent(tp);
     
                    /* We know that such packets are checksummed on entry. */
                    tcp_ack(sk, skb, 0);
                    __kfree_skb(skb);
                    tcp_data_snd_check(sk);
                    return;
                } else {            /* Header too small */
                    goto discard;
                }
    对于长度大于TCP报文头部长度的数据包,表明存在数据部分。首先完成checksum校验,丢弃校验失败的报文。如果此时skb的占用空间大于套接口的预分配空间额度值,跳转到慢速路径执行。随后更新时间戳ts_recent,调用tcp_queue_rcv处理接收到的数据报文。此段处理意味着本端在快速路径中为数据接收端。最后,如果发送端数据包的ACK确认序号不等于本端套接口的待确认序号,由于快速路径的单向特性,本端并不发送数据,一旦两者不相等的,表明本端发送了数据,需要处理ACK并且检查是否还有后续数据发送。否则,内核直接进行ACK发送策略检查。

            } else {
                if (tcp_checksum_complete(skb))
                    goto csum_error;
     
                if ((int)skb->truesize > sk->sk_forward_alloc)
                    goto step5;
     
                /* Predicted packet is in window by definition. seq == rcv_nxt and rcv_wup <= rcv_nxt. Hence, check seq<=rcv_wup reduces to: */
                if (tcp_header_len == (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) && tp->rcv_nxt == tp->rcv_wup)
                    tcp_store_ts_recent(tp);
     
                eaten = tcp_queue_rcv(sk, skb, tcp_header_len, &fragstolen);
                tcp_event_data_recv(sk, skb);
     
                if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
                    /* Well, only one small jumplet in fast path... */
                    tcp_ack(sk, skb, FLAG_DATA);
                    tcp_data_snd_check(sk);
                    if (!inet_csk_ack_scheduled(sk))
                        goto no_ack;
                }
     
                __tcp_ack_snd_check(sk, 0);
    no_ack:
                if (eaten)
                    kfree_skb_partial(skb, fragstolen);
                sk->sk_data_ready(sk);
                return;
            }
        }
    }

    三、快速路径接收的关闭
    与以上介绍的快速路径开启的条件判断类型,一旦这些条件不满足,就需要关闭快速路径。例如以下,接收到乱序报文tcp_data_queue_ofo函数,套接口空间不足即使缩减队列空间也没有足够空间tcp_prune_queue函数,以及接收到TCP紧急数据tcp_check_urg时,都要关闭快速路径。

    static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
    {
        /* Disable header prediction. */
        tp->pred_flags = 0;
    }
    static int tcp_prune_queue(struct sock *sk)
    {
        /* Massive buffer overcommit. */
        tp->pred_flags = 0;
        return -1;
    }
    static void tcp_check_urg(struct sock *sk, const struct tcphdr *th)
    {
        tp->urg_data = TCP_URG_NOTYET;
        tp->urg_seq = ptr;
        /* Disable header prediction. */
        tp->pred_flags = 0;
    }

    最后,在TCP报文发送函数tcp_transmit_skb中,调用函数tcp_select_window选择窗口大小时,如果将要通过的窗口为0,关闭快速路径接收功能。TCP的窗口探测probe只能在慢速路径中处理。

    static u16 tcp_select_window(struct sock *sk)
    {
        /* If we advertise zero window, disable fast path. */
        if (new_win == 0) {
            tp->pred_flags = 0;
        } else if (old_win == 0) {
            NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFROMZEROWINDOWADV);
        }
        return new_win;
    }

  • 相关阅读:
    持续集成与持续交付CI/CD
    JAVA虚拟机--JVM
    代码随想录-034-459.重复的子字符串
    【完美世界】天仙书院偷食也就算了,竟然还偷院长的孙女,美滋滋
    生活常用类API推荐
    微服务架构的外部 API 集成模式
    C++——虚函数、虚析构函数、纯虚函数、抽象类
    iOS开发-CoreNFC实现NFC标签Tag读取功能
    java面试题
    Linux初始化mysql后外网无限制访问
  • 原文地址:https://blog.csdn.net/wuyongmao/article/details/126265908