TCP套接口快速接收路径

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

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标志字段设置为了零。

   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

快速路径的开启时机


在接收到保序的数据包后,函数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;
}

 

内核版本 4.15

 

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

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

抵扣说明:

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

余额充值