zoukankan      html  css  js  c++  java
  • 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;
    }
  • 相关阅读:
    python 三行代码实现快速排序
    kafka使用自带zookeeper 单机版安装、配置、启动、生产消费消息测试
    kafka manager添加集群出现错误KeeperErrorCode
    常用的kubectl命令
    20210531-20210606 助教一周小结(第十八周)
    20210524-20210530 助教一周小结(第十七周)
    20210517-20210523 助教一周小结(第十六周)
    20210510-20210516 助教一周小结(第十五周)
    20210503-20210509 助教一周小结(第十四周)
    20210426-20210502 助教一周小结(第十三周)
  • 原文地址:https://www.cnblogs.com/codestack/p/11918011.html
Copyright © 2011-2022 走看看