TCP数据接收及快速路径和慢速路径

概述

tcp握手完成后,收到数据包后,调用路径为tcp_v4_rcv->tcp_v4_do_rcv->tcp_rcv_established
在tcp_rcv_established中处理TCP_ESTABLISHED状态的包。 并分为快速路径和慢速路径。
快速路径只进行非常少量的处理。

快速路径:用于处理预期的,理想情况下的数据段,在这种情况下,不会对一些边缘情形进行检测,进而达到快速处理的目的;

慢速路径:用于处理那些非预期的,非理想情况下的数据段,即不满足快速路径的情况下数据段的处理;

首部预测字段格式:首页预测字段,实际上是与TCP首部中的【头部长度+保留字段+标记字段+窗口值】这个32位值完全对应的;进行快速路径判断的时候,只需要将该预测值与TCP首部中的对应部分进行比对即可,具体见tcp_rcv_established;

快速路径(Fast Path)

内核使用tcp_sock中的pred_flags作为判断条件,0表示使用慢速路径,非0则表示快速的判断条件,值为tcp首部的第13-16字节,包含首部长度,标记位,窗口大小。
因此使用pred_flags来检查tcp头就能避免了头部的一些控制信息的处理。

static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd)
{//tcphdr首部的第13-16字节,包含首部长度,标记位,窗口大小
    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)
{//snd_wnd已经缩放过,要还原tcp头信息这里缩放回去
    __tcp_fast_path_on(tp, tp->snd_wnd >> tp->rx_opt.snd_wscale);
}

static inline void tcp_fast_path_check(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
/*
1 没有乱序数据包
2 接收窗口不为0
3 还有接收缓存空间
4 没有紧急数据

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

__tcp_fast_path_on调用时机

在tcp_finish_connect中没有开启wscale的时候,会调用__tcp_fast_path_on来设置快速路径条件。
因为没有开启wscale,所以不需要调用tcp_fast_path_on。
为什么开启wscale-窗口因子 后就要关闭快速路径呢?
这时候只是客户端进入TCP_ESTABLISHED状态,服务端还在等待客户端最后一次ack才能发送数据。
因此不会收到服务端的数据,也就不用考虑快速路径了。

void tcp_finish_connect(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
    tcp_set_state(sk, TCP_ESTABLISHED);
    ...
    if (!tp->rx_opt.snd_wscale)    //对方没有开启wscale,则开启快速路径
        __tcp_fast_path_on(tp, tp->snd_wnd);
    else
        tp->pred_flags = 0;    //目前不会收到服务端数据,不用开启快速路径
    if (!sock_flag(sk, SOCK_DEAD)) {
        sk->sk_state_change(sk); //唤醒connect
        sk_wake_async(sk, SOCK_WAKE_IO, POLL_OUT);
    }
}

tcp_fast_path_on调用时机

跟tcp_finish_connect一样,服务端进入TCP_ESTABLISHED状态的时候,也要尝试开启快速路径,因此调用tcp_fast_path_on设定快速路径判断条件

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    ...
    /* step 5: check the ACK field */
    acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH |
                      FLAG_UPDATE_TS_RECENT) > 0;
    switch (sk->sk_state) {
    case TCP_SYN_RECV:    //握手完成时的新建连接的初始状态
        if (!acceptable)
            return 1;
        ...
        tcp_set_state(sk, TCP_ESTABLISHED);
        sk->sk_state_change(sk);
        ...
        tp->snd_wnd = ntohs(th->window) << tp->rx_opt.snd_wscale; //snd_wnd已经缩放过
        tcp_init_wl(tp, TCP_SKB_CB(skb)->seq);
        if (tp->rx_opt.tstamp_ok)
            tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
        ...
        tcp_fast_path_on(tp);
        break;
    }
    ...
}

tcp_fast_path_check调用时机

相比起前两个进入TCP_ESTABLISHED就设置pred_flags,因为建立连接前没有其他数据包作为判定依据。
tcp_fast_path_check主要是在连接过程中,有其他数据包作为判定依据的条件下调用:

      • 没有乱序的数据包
      • 接收窗口不为0
      • 接收缓存未用完
      • 非紧急数据

完成紧急数据的读取

紧急数据是由慢速路径处理,需要保持在慢速路径模式直到收完紧急数据,读完后就能检测是否能够开启fast path

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int nonblock, int flags, int *addr_len)
{
...
if (tp->urg_data && after(tp->copied_seq, tp->urg_seq)) {
tp->urg_data = 0;
tcp_fast_path_check(sk);
}
...
}

在慢速路径收到非乱序包的时候

tcp_data_queue是在慢速路径,对数据部分进行处理。
只有当前包是非乱序包,且接收窗口非0的时候,才能调用tcp_fast_path_check尝试开启快速路径

static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
    bool fragstolen = false;
    int eaten = -1;
    if (TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq) {    //没有数据部分,直接释放
        __kfree_skb(skb);
        return;
    }
    ...
    if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {    //非乱序包
        if (tcp_receive_window(tp) == 0)    //接受窗口满了,不能接受
            goto out_of_window;
        ...
        tcp_fast_path_check(sk);    //当前是slow path, 尝试开启快速路径
        ...
    }
    ...
}

当收到新的通告窗口值时

因为pred_flags中包含了窗口值,显然收到新的通告窗口时,需要更新

static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack,
                 u32 ack_seq)
{
    struct tcp_sock *tp = tcp_sk(sk);
    int flag = 0;
    u32 nwin = ntohs(tcp_hdr(skb)->window);
    if (likely(!tcp_hdr(skb)->syn))
        nwin <<= tp->rx_opt.snd_wscale;
    if (tcp_may_update_window(tp, ack, ack_seq, nwin)) {    //更新滑动窗口,或者收到新的窗口通知
        flag |= FLAG_WIN_UPDATE;
        tcp_update_wl(tp, ack_seq);    //snd_wl1=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);    //窗口更新了,要重新设置fast path检测条件
            ...
        }
    }
    ...
    return flag;
}

快速路径包处理

在tcp_rcv_established中,通过快速路径判断后,

/*
 *    TCP receive function for the ESTABLISHED state.
 *
 *    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.
 *
 *    When these conditions are not satisfied it drops into a standard
 *    receive procedure patterned after RFC793 to handle all cases.
 *    The first three cases are guaranteed by proper pred_flags setting,
 *    the rest is checked inline. Fast processing is turned on in
 *    tcp_data_queue when everything is OK.
 */
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
             const struct tcphdr *th, unsigned int len)
{
    struct tcp_sock *tp = tcp_sk(sk);

    skb_mstamp_get(&tp->tcp_mstamp);
    if (unlikely(!sk->sk_rx_dst)) /* 路由为空,则重新设置路由 */
        inet_csk(sk)->icsk_af_ops->sk_rx_dst_set(sk, skb);
    /*
     *    Header prediction.
     *    The code loosely follows the one in the famous
     *    "30 instruction TCP receive" Van Jacobson mail.
     *
     *    Van's trick is to deposit buffers into socket queue
     *    on a device interrupt, to call tcp_recv function
     *    on the receive process context and checksum and copy
     *    the buffer to user space. smart...
     *
     *    Our current scheme is not silly either but we take the
     *    extra cost of the net_bh soft interrupt processing...
     *    We do checksum and copy also but from device to kernel.
     */

    tp->rx_opt.saw_tstamp = 0;

    /*    pred_flags is 0xS?10 << 16 + snd_wnd
     *    if header_prediction is to be made
     *    'S' will always be tp->tcp_header_len >> 2
     *    '?' will be 0 for the fast path, otherwise pred_flags is 0 to
     *  turn it off    (when there are holes in the receive
     *     space for instance)
     *    PSH flag is ignored.
     */
        /* 快路检查&& 序号正确 && ack序号正确 */

    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 header prediction: tcp_header_len
         * is automatically equal to th->doff*4 due to pred_flags
         * match.
         */

        /* Check timestamp */ /* 有时间戳选项 */
        if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {
            /* No? Slow path! /* 解析时间戳选项失败,执行慢路 */
            if (!tcp_parse_aligned_timestamp(tp, th))
                goto slow_path;

            /* If PAWS failed, check it more carefully in slow path 
             */
             /* 序号回转,执行慢路 */
            if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)
                goto slow_path;

            /* DO NOT update ts_recent here, if checksum fails
             * and timestamp was corrupted part, it will result
             * in a hung connection since we will drop all
             * future packets due to the PAWS test.
             */
        }

        if (len <= tcp_header_len) {  /* 无数据 */
            /* Bulk data transfer: sender */
            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.
                 */ /* 输入/快速路径ack处理 */
                tcp_ack(sk, skb, 0);
                __kfree_skb(skb);
                /* 检查是否有数据要发送,并检查发送缓冲区大小
                收到ack了,给数据包一次发送机会,tcp_push_pending_frames*/
                tcp_data_snd_check(sk);
                return;
            } else { /* Header too small */
             /* 数据多小,比头部都小,错包 */
                TCP_INC_STATS(sock_net(sk), TCP_MIB_INERRS);
                goto discard;
            }
        } else { /* 有数据 */
            int eaten = 0;
            bool fragstolen = false;
             /* 进程上下文 */
            if (tp->ucopy.task == current &&
                /* 期待读取的和期待接收的序号一致 */
                tp->copied_seq == tp->rcv_nxt &&
                len - tcp_header_len <= tp->ucopy.len && /* 数据<= 待读取长度 */
                /* 控制块被用户空间锁定 */
                sock_owned_by_user(sk)) {
                __set_current_state(TASK_RUNNING); /* 设置状态为running??? */
                 /* 拷贝数据到msghdr */
                if (!tcp_copy_to_iovec(sk, skb, 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);

                    tcp_rcv_rtt_measure_ts(sk, skb); /* 接收端RTT估算 */

                    __skb_pull(skb, tcp_header_len);
                    /* 更新期望接收的序号 */
                    tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)->end_seq);
                    NET_INC_STATS(sock_net(sk),
                            LINUX_MIB_TCPHPHITSTOUSER);
                    eaten = 1;
                }
            }
            /* 未拷贝数据到用户空间,或者拷贝失败----没有把数据放到ucopy中 */
            if (!eaten) {
                if (tcp_checksum_complete(skb))
                    goto csum_error;
                      /* skb长度> 预分配长度 */
                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);

                tcp_rcv_rtt_measure_ts(sk, skb);

                NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPHPHITS);

                /* Bulk data transfer: receiver */ /* 数据加入接收队列  添加数据到sk_receive_queue中 */
                eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
                              &fragstolen);
            }

            tcp_event_data_recv(sk, skb);//inet_csk_schedule_ack, 更新rtt
            /* 确认序号确认了数据 */
            if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
                /* Well, only one small jumplet in fast path... */
                tcp_ack(sk, skb, FLAG_DATA);/* 处理ack */
                tcp_data_snd_check(sk);  /* 检查是否有数据要发送,需要则发送 */
                if (!inet_csk_ack_scheduled(sk))  /* 没有ack要发送 在tcp_event_data_recv标记过,但可能ack已经发出了,就不用检测是否要发送了*/
                    goto no_ack;
            }
          /* 检查是否有ack要发送,需要则发送 */
            __tcp_ack_snd_check(sk, 0);
no_ack:
            if (eaten)
                kfree_skb_partial(skb, fragstolen);
            sk->sk_data_ready(sk);
            return;
        }
------------------------------

慢速路径

pred_flags=0或者tcp包头匹配pred_flags失败的时候则为slow path处理

  • 本地接受缓存不足通告0窗口的时候, 因为0窗口探测包需要在慢速路径处理

static u16 tcp_select_window(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    u32 old_win = tp->rcv_wnd;
    u32 cur_win = tcp_receive_window(tp);    //根据过去接收窗口值,当前还能通告给对方的接收窗口配额
    u32 new_win = __tcp_select_window(sk);    //根据接收缓存计算出的新窗口值
    ...
    /* If we advertise zero window, disable fast path. */
    if (new_win == 0) {    //cur_win scale也为0
        tp->pred_flags = 0;    //开启0窗口, 关闭快速路径
        if (old_win)
            NET_INC_STATS(sock_net(sk),
                      LINUX_MIB_TCPTOZEROWINDOWADV);
    } else if (old_win == 0) {
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFROMZEROWINDOWADV);
    }
    return new_win;
}
  • 收到乱序包的时候

收到乱序包后会调用tcp_data_queue_ofo添加skb到ofo队列中

static void tcp_data_queue_ofo(struct sock *sk, struct sk_buff *skb)
{
    ...
    /* Disable header prediction. */
    tp->pred_flags = 0;
    ...
}
  • 收到紧急数据
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;
}
  • 接受缓存不足
static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
                 unsigned int size)
{
    if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf || //接收缓存不够
        !sk_rmem_schedule(sk, skb, size)) {    //并且超过系统设置的最大分配空间了
        if (tcp_prune_queue(sk) < 0)    //尝试合并ofo/sk_receive_queue来腾出空间
            return -1;    //还是不够,超过sk_rcvbuf, 返回失败
        while (!sk_rmem_schedule(sk, skb, size)) {    //再次确认
            if (!tcp_prune_ofo_queue(sk)) //释放ofo队列中的数据
                return -1;
        }
    }
    return 0;
}
static int tcp_prune_queue(struct sock *sk)
{
    ...
    /* Massive buffer overcommit. */
    tp->pred_flags = 0;    //接收缓存还是不足,关闭快速路径
    return -1;
}

慢速路径包处理

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
             const struct tcphdr *th, unsigned int len)
{
    ...
    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)) {    // 确认的序号是已经发送的包
        //快速路径处理
        ...
    }

slow_path:
      /* 长度错误|| 校验和错误 */
    if (len < (th->doff << 2) || tcp_checksum_complete(skb))
        goto csum_error;
    /* 无ack,无rst,无syn */
    if (!th->ack && !th->rst && !th->syn)
        goto discard;

    /*
     *    Standard slow path.
      /* 种种校验 
     */

    if (!tcp_validate_incoming(sk, skb, th, 1))
        return;

step5:
          /* 处理ack */
    if (tcp_ack(sk, skb, FLAG_SLOWPATH | FLAG_UPDATE_TS_RECENT) < 0)
        goto discard;
     /* 计算rtt */
    tcp_rcv_rtt_measure_ts(sk, skb);

    /* Process urgent data. */
     /* 处理紧急数据 */
    tcp_urg(sk, skb, th);

    /* step 7: process the segment text数据段处理 */
    tcp_data_queue(sk, skb);

    tcp_data_snd_check(sk);/* 发送数据检查,有则发送 */
    tcp_ack_snd_check(sk);/* 发送ack检查,有则发送 */
    return;

tcp_data_queue

  • tcp_data_queue主要把非乱序包copy到ucopy或者sk_receive_queue中,并调用tcp_fast_path_check尝试开启快速路径
  • 对重传包设置dsack,并快速ack回去
  • 对于乱序包,如果包里有部分旧数据也设置dsack,并把乱序包添加到ofo队列中

tcp_data_queue_ofo

在新内核的实现中ofo队列实际上是一颗红黑树。
在tcp_data_queue_ofo中根据序号,查找到合适位置,合并或者添加到rbtree中。
同时设置dsack和sack,准备ack给发送方。

static int tcp_ack_update_window(struct sock *sk, const struct sk_buff *skb, u32 ack,
u32 ack_seq)
{
struct tcp_sock *tp = tcp_sk(sk);
int flag = 0;
u32 nwin = ntohs(tcp_hdr(skb)->window);
if (likely(!tcp_hdr(skb)->syn))
nwin <<= tp->rx_opt.snd_wscale;
if (tcp_may_update_window(tp, ack, ack_seq, nwin)) { //更新滑动窗口,或者收到新的窗口通知
flag |= FLAG_WIN_UPDATE;
tcp_update_wl(tp, ack_seq); //snd_wl1=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); //窗口更新了,要重新设置fast path检测条件
...
}
}
...
return flag;
}
原文地址:https://www.cnblogs.com/codestack/p/11918011.html