天天看點

linux關于tcp協定ack以及亂序封包暫存的實作--立即ack/延遲ack/捎帶ack

tcp需要ack,可是為了效率,并不是每發送一個資料都要等待ack,而是盡可能利用視窗機制,積累發送ack的,當然在某些特殊情況下還是需要馬上發送ack的,比如接收到亂序的資料,這種情況下,雖然接收端可以将亂序的資料包暫存,但是接收方必須發送一個ack号為按序的期望的序列号的ack給發送端,另外就是接收視窗需要調整,此時就要立刻發送ack,否則則可以延遲發送ack,看一下linux的這方面的代碼:

static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)

{

    struct tcp_sock *tp = tcp_sk(sk);

    //rcv_mss是估算的對端的mss,它對本端接收視窗的計算也有很大意義

    if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss //如果接收到了大于一個的封包,那麼就發送ack,一下子确認兩個封包

         && __tcp_select_window(sk) >= tp->rcv_wnd) || //需要調整視窗,最大化吞吐量

        tcp_in_quickack_mode(sk) ||

        (ofo_possible && //收到亂序的包

         skb_peek(&tp->out_of_order_queue))) {

        tcp_send_ack(sk);

    } else {

        tcp_send_delayed_ack(sk);

    }

}

大體上,上述的函數實作了RFC1122和RFC2581的關于ack的建議。

     不是說ack可以在send資料的時候捎帶發送嗎?确實是這樣,每當發送資料的時候,ack都會被發送,但是發送資料是應用層的事,如果應用層不發送呢?那豈不是永遠都無法ack了嗎?是以還必須有傳輸層本身的一套機制來支援ack的發送,捎帶ack僅僅是一個補充,傳輸層的ack發送就是立即模式和延遲模式,正如上面的__tcp_ack_snd_check所展示的那樣。

     發送ack其實很簡單,就是填寫一個tcp資料,ack字段設定為接收視窗最左邊的那個資料的序列号加1,延遲發送不怕和捎帶發送重複,RFC2581規定每個到來的封包隻能生成一個ack,除非需要發送端重傳才會發送備援ack,如果tcp進入了等待延遲發送ack的狀态,當接收端有資料要發送的時候就會将ack捎帶到發送端,同時清除延遲ack定時器的pendding,如此延遲ack定時器到期後就不會再發送ack了。

     穩定的理想情況下,接收端的視窗也是穩定的,不需要調整的,如果接收端不發送資料隻是接收資料,ack幾乎全部以延遲的方式發送給發送端,如果接收端同時也發送資料,那麼ack就會以捎帶的方式發給發送端,立即ack隻會在幾種特殊的異常情況才發送。

     一種是收到了一個以上的完整的tcp段,并且可能要放大視窗,為了使得吞吐量最大化,放大的視窗決不能浪費掉,于是需要立即發送ack給發送端,發送端接收到這個ack以後就會繼續發送其發送視窗中後面的資料了。發送端的mss和接收端的視窗大小相關聯,接收端的視窗設定為發送端mss的整數倍比較好,這樣記憶體的使用率最高,确定好接收端可以承受的視窗大小之後,如果其比目前視窗大,那麼立即發送ack使得發送端可以盡快發送資料。

     另外一種是在所謂的quick模式,quick模式并不是經常的,隻有在非互動的tcp連接配接才可能進入quick模式,因為互動的連接配接表明ack已經足夠快了,沒有必要立即發送ack了,一般都是捎帶ack或者延遲發送ack的,那麼如何判斷是否是互動連接配接呢?核心中tcp_opt結構體中有一個ack子結構體,内部有一個quick和pingpong兩個字段,其中pingpong就是判斷互動連接配接的,核心會在很多地方進行抉擇,根據很多參數,比如收發間隔或者使用者配置等判斷是否一個連接配接是互動的,如果不是互動的,那麼就存在一系列問題:1.由于使用者程序長期不取出收到的資料導緻一系列的問題,于是需要協定棧瞬間回複ack,2.積壓的ack沒有回複,影響了發送端的發送速率。此時就會給quick賦予一定的數值,每發送一個ack就會消耗掉一些quick值,直到用盡了quick而進入延遲模式,quick的值和視窗相關,因為接收端最多隻能确認接收視窗這麼大的資料。

     立即ack的最後一種可能就是收到亂序包,表明資料已經可能丢失了,那麼應該盡快地進入補救階段,就是說要盡快進入快速重傳,此時ack也要立即發送(核心發現越界包[和亂序包有差別]後會調用tcp_send_dupack發送一個ack後丢次封包而傳回),核心收到亂序封包後會在out_of_order_queue隊列緩存該亂序報本,最後會調用tcp_ack_snd_check再次發送一個ack,這個ack确認的是按序封包的最後一個,以前應該發送過該ack,這樣接收端收到亂序封包後就會發送一個備援的ack,如果下次接收的資料仍然是亂序的,那麼就再發送一個前兩個相同的ack,這樣發送端可能就會連續接收到三個一模一樣的ack,在接收端,第三次接收到的仍然是亂序封包時,再次發送備援ack,隻有這第4個ack被發送端收到後才會進行快速重傳。這裡的一個細節就是發送端收到了4個相同的ack(3個備援ack),進而作為進入快速重傳的标志,linux是這麼實作的,符合了rfc的建議,但是這種實作所依賴的是其背後的一個思想。

     一個封包談不上順序,最少兩個封包才有順序的概念,正如位元組序一樣,utf8以一個位元組為編碼機關,是以就沒有位元組序的問題,同樣的,僅僅來了一個封包也不能說它對于目前按序的封包來講是亂序的,隻有當第二個封包到來的時候,如果目前按序封包,第一個封包,第二個封包拼不成順序才能說明後來的這兩個封包是亂序的,當然這也是一種權衡的結果,正如三次握手為何是三次一樣,即使接收端收到了第三個亂序封包,仍有可能被第四個填充後成為按序封包,沒完沒了等下去是不行的,必須在發送端接收到确定的,不是很大的數目備援ack的時候進入快速重傳,同時也不能頻繁的快速重傳,是以就選擇了3個備援ack,當然這個數字是可以配置的。

     最後看一下亂序封包的重新調整。linux的協定棧實作中将亂序的封包按照序列号大小順序插入到一個隊列當中,此隊列是基于連接配接的,如果該亂序隊列有封包暫存的話,每接收到一個封包都會嘗試調用tcp_ofo_queue函數,它的意義在于努力将亂序的封包順序化,正如上述備援ack相關的背後的思想,每個新的封包都有可能填補按序封包和亂序封包之間的缺口,換句話說,每一個新到的封包都可能直接拼接到按序封包隊列最後一個的後面,同時也有可能完成這種拼接後,和後面的亂序隊列的最前面一個或者幾個或者全部的封包拼接,最終成為一系列按序的封包:

static void tcp_ofo_queue(struct sock *sk)

    struct tcp_opt *tp = tcp_sk(sk);

    __u32 dsack_high = tp->rcv_nxt;

    struct sk_buff *skb;

    while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {

        if (after(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) //最前面的skb都拼不上

            break;

        ...

        if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {

            __skb_unlink(skb, skb->list); //曾經接收的封包段,繼續

            __kfree_skb(skb);

            continue;

        }

        __skb_unlink(skb, skb->list); //可以拼接,更新tp的rcv_next字段

        __skb_queue_tail(&sk->sk_receive_queue, skb);

        tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;

 本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1271789

繼續閱讀