手機版
你好,游客 登錄 注冊
背景:
閱讀新聞

Linux TCP滑動窗口代碼簡述

[日期:2019-09-30] 來源:Linux社區  作者:yhp-smarthome [字體: ]

前言:TCP的可靠性大致通過3類方法來保障:1.確認和重傳。2.流量控制。3.擁塞避免。其中的流量控制中使用的滑動窗口,使得TCP的發送方和接收方速度得以匹配,從而為傳輸提供了可靠性支撐。本篇就介紹一下滑動窗口在Linux的大致代碼,對于滑動窗口的基本知識已經有無數優秀的文章,更有TCP/IP卷一可參考,本篇不再贅述。代碼基于 Linux 2.6.32。

1. 背景問題介紹

我們知道TCP是有確認機制的,就是對于發送方發送的每個字節,接收方都會顯式的進行確認(連續確認實際也是確認了每個字節)。那么仔細想象一下,該如何處理這個確認過程呢?比如,sender發送完一定數據后,停下來等待receiver的確認,然后再繼續發送,再繼續等待... 因此這里存在一個傳輸效率的問題。

接著再想另外一個問題,如果發送端的發送速度快,而接收端的接收速度慢,此時,如果不能協調收發速度,將會導致接收不及時丟包,進而加重重傳,丟更多的包,引發網絡雪崩。

上面兩個問題基本就是滑動窗口誕生要解決的問題,即提高傳輸效率和流量控制的功能。

2. 流量控制簡單過程

TCP的流量控制主要是協調收發的速度,在發送端維護著發送和接收窗口,同樣的,在接收端,也同樣如此。在說具體的操作之前,先說幾個相關的概念:發送隊列,接收隊列,重傳隊列,滑動窗口,發送窗口,擁塞窗口,通告窗口。

2.1 概念解釋
  • 發送隊列——我們知道協議棧在發送報文的時候,是要先放入到發送隊列的,每個打開的socket都維護著一個接收隊列和發送隊列。
  • 重傳隊列——當報文從發送隊列被發送后,會拷貝一份放到重傳隊列,重傳隊列用于超時定時器到期后,進行重傳。
  • 滑動窗口——滑動窗口是一個可以滑動的區間。在發送時,通常就是指發送窗口;在接收時,就是指接收窗口。滑動窗口是一種提高傳輸效率的機制,因此在發送和接收過程中都有。
  • 擁塞窗口——因為TCP有擁塞避免機制,因此引出了一個擁塞窗口,他也是一個限制發送速度的東西,當出現擁塞的時候,調節發送窗口大小。通常發送窗口是取擁塞窗口和通告窗口的較小值。
  • 通告窗口——通告窗口是接收端傳遞給發送端的一個窗口值,表示接收端還有多大空余空間接收數據。所以,發送端的發送窗口就是根據通告窗口進行調節,使發送和接收速度匹配。
2.2 發送和接收
  • 當有數據需要發送時,會把數據掛在這個socket的發送隊列中,在linux中這個隊列使用的是雙向鏈表實現的
struct sk_buff_head {
    /* These two members must be first. */
    struct sk_buff  *next;
    struct sk_buff  *prev;

    __u32       qlen;
    spinlock_t  lock;
};

TCP接收報文的函數為tcp_v4_do_rcv(),當建立連接后,接收處理的函數為tcp_rcv_established(),進來后,會進行fast path和slow path的區分處理,快通道和慢通道是由首部預測來確認的,首部預測用于提高TCP的處理速度。通常網絡上大多數報文都會走fast path,看處理細節

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 0;
}

那么就進行ack報文的處理tcp_ack(),在這個函數中,會更新發送窗口,使窗口右移,這樣就會有新的報文可以被發送,tcp_data_snd_check(sk);就把這些報文發送出去。
最后調用tcp_write_xmit(),其函數說明如下:

This routine writes packets to the network. It advances the
send_head. This happens as incoming acks open up the remote
window for us.

可以看出,這個確實是用于發送窗口擴大后的報文。從這里可以看出TCP報文的發送在窗口機制下是由接收的ack來

  • 當有數據需要接收時,依然看tcp_rcv_established()函數,先檢查是否滿足快速路徑,如果沒有亂序的話,就是這樣。
if (tp->copied_seq == tp->rcv_nxt &&
                len - tcp_header_len <= tp->ucopy.len) {
#ifdef CONFIG_NET_DMA
                if (tcp_dma_try_early_copy(sk, skb, tcp_header_len)) {
                    copied_early = 1;
                    eaten = 1;
                }
#endif
                if (tp->ucopy.task == current &&
                    sock_owned_by_user(sk) && !copied_early) {
                    __set_current_state(TASK_RUNNING);

                    if (!tcp_copy_to_iovec(sk, skb, tcp_header_len))
                        eaten = 1;
                }

然后就把報文從內核態拷貝到用戶態,如果拷貝成功,標志成eaten = 1;接下來就是計算RTT以及更新接收的序列號。

if (eaten) 
{
    /* 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);

    __skb_pull(skb, tcp_header_len);
    tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITSTOUSER);
}

如果沒有拷貝成功,那么就把報文放在接收隊列中,同時更新RTT,失敗的原因比如用戶報文數據長度比用戶空間緩存的剩余量大等。

if (!eaten) 
{
    if (tcp_checksum_complete_user(sk, skb))
        goto csum_error;

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

    if ((int)skb->truesize > sk->sk_forward_alloc)
        goto step5;

    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITS);

    /* Bulk data transfer: receiver */
    __skb_pull(skb, tcp_header_len);
    __skb_queue_tail(&sk->sk_receive_queue, skb);
    skb_set_owner_r(skb, sk);
    tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
}

最后檢查一下是否需要發送ack報文或者是sack報文。

if (!copied_early || tp->rcv_nxt != tp->rcv_wup)
                __tcp_ack_snd_check(sk, 0);

另外一個是如果出現亂序等,快速路徑沒有滿足條件,則走慢速路徑,在慢速路徑中會有把報文放入亂序隊列等操作,具體不表了。

tcp_data_queue(sk, skb);

之后,用戶進程通過recv讀操作,把報文從接收隊列中讀取報文,在tcp_recvmsg可以看其過程:

skb_queue_walk(&sk->sk_receive_queue, skb) 
{
    /* Now that we have two receive queues this
     * shouldn't happen.
     */
    if (WARN(before(*seq, TCP_SKB_CB(skb)->seq),
         KERN_INFO "recvmsg bug: copied %X "
               "seq %X rcvnxt %X fl %X\n", *seq,
               TCP_SKB_CB(skb)->seq, tp->rcv_nxt,
               flags))
        break;

    offset = *seq - TCP_SKB_CB(skb)->seq;
    if (tcp_hdr(skb)->syn)
        offset--;
    if (offset < skb->len)
        goto found_ok_skb;
    if (tcp_hdr(skb)->fin)
        goto found_fin_ok;
    WARN(!(flags & MSG_PEEK), KERN_INFO "recvmsg bug 2: "
            "copied %X seq %X rcvnxt %X fl %X\n",
            *seq, TCP_SKB_CB(skb)->seq,
            tp->rcv_nxt, flags);
}

3. 總結

TCP的滑動窗口的流量控制是通過協調發送方和接收方的速度來實現的,具體來說,就是發送方窗口是由接收方回的ack驅動的,也就是說發送方要能持續發送包需要持續接收ack。另一個方面,接收方在讀取報文后,發送ack進行響應,循環進行接收。這個過程通過驅動窗口的可持續滑動,進而實現了流量控制和提高傳輸效率。

Linux公社的RSS地址:http://www.nmzech.live/rssFeed.aspx

本文永久更新鏈接地址http://www.nmzech.live/Linux/2019-09/160867.htm

linux
相關資訊       Linux TCP滑動窗口 
本文評論   查看全部評論 (0)
表情: 表情 姓名: 字數

       

評論聲明
  • 尊重網上道德,遵守中華人民共和國的各項有關法律法規
  • 承擔一切因您的行為而直接或間接導致的民事或刑事法律責任
  • 本站管理人員有權保留或刪除其管轄留言中的任意內容
  • 本站有權在網站內轉載或引用您的評論
  • 參與本評論即表明您已經閱讀并接受上述條款
北京快乐8走势图彩客网