TCPIP-------超時與重傳

loveliu928發表於2020-11-17

        TCP是可靠的傳輸層協議,但這並不意味著傳送端傳送的資料一定可以到達接收端,因為傳輸過程中遇到的情況是不可控的,在TCP兩端互動過程中,資料和確認的報文都有可能丟失,因此在傳送端引入超時和重傳機制可以很好的解決報文丟失問題。其基本原理:TCP通過在傳送端為每個傳送出去的報文設定一個超時定時器,當定時器溢位時還沒有收到確認報文,它就重傳該資料。對任何TCP協議實現而言,怎樣決定超時間隔和如何確定重傳的方式是提高TCP效能的關鍵,如何設定這個定時器的時間(RTO),從而保證對網路資源最小的浪費。因為若RTO太小,可能有些報文只是遇到擁堵或網路不好延遲較大而已,這樣就會造成不必要的重傳。太大的話,使傳送端需要等待過長的時間才能發現資料丟失,影響網路傳輸效率。由於不同的網路情況不一樣,不可能設定一樣的RTO,實際中RTO是根據網路中的RTT(傳輸往返時間)來自適應調整的,而這些都與往返時間估計密切相關。往返時間(RTT)代表了某位元組資料傳送出去到對應確認返回期間的時間間隔。
        TCP超時與重傳中最重要的部分就是對一個給定連線的往返時間(RTT)的測量。由於路由器和網路流量均會變化,因此我們認為這個時間可能經常會發生變化,TCP應該跟蹤這些變化並相應地改變其超時時間。TCP必須測量在傳送一個帶有特別序號的位元組和接收到包含該位元組的確認之間的RTT,同時應注意,發出去的資料段與返回的確認之間並沒有一一對應的關係。在往返時間變化起伏很大時,基於均值和方差來計算RTO能提供更好的響應,下面這個演算法是Jacobson提出的,它說明如何平滑RTT,如何使用RTT來設定RTO,目前廣泛應用到了TCP協議中。

                                    Err = M-A                                        (1)
                                    A = A+ g* Err                                    (2)
                                    D =D + h *  (|Err|-D)                            (3)
                                    RTO = A+4*D                                      (4)

        其中M表示某次測量的RTT的值,A表示測得的RTT的平均值,A值的更新如第二式所示,D值為RTT的估計的方差,其更新如第三式所示。二式和三式中g和h都為常數,一般g取1/8,h取1/4。這樣取值是為了便於計算,從後面可以看出,通過簡單的移位操作就可以完成上述計算了。RTO的計算如第四式所示,初始時,RTO取值為6,即3s,A值為0,D值為6。

       通過圖來了解超時重傳機制:

                                                           
        從圖可以知道,傳送方連續傳送3個資料包,其中第二個丟失,沒有被接收到,因此不會返回對應的ACK,每傳送一個資料包,就啟動一個定時器,當第二個包的定時器溢位了還沒有收到ack,這時就進行重傳。

        下面分析一下LWIP協議棧中超時重傳機制的實現方法:

        在LWIP協議棧中TCP控制塊(tcp_pcb)內部與超時重傳相關欄位有rtime、rttest、rtseq、sa、sv、rto、nrtx,rtime表示重傳定時器,它的值每500ms被TCPIP核心定時器加1,當該值超過RTO時,報文段會進行重傳,rttest用於記錄報文段傳送時間,用於計算報文段在兩天主機之間的往返時間,tcp根據往返時間動態設定報文段的超時時間RTO,rtseq表示正在進行往返時間估算的報文段,sa、sv用於計算RTO、rto超時重傳時間間隔,nrtx表示報文段被重傳的次數,這個值與RTO密切相關。

         先看看超時重傳在LWIP協議棧中是怎麼實現的,在來看RTT估計。tcp_output從unsent佇列上取下第一個資料段,並呼叫函式tcp_output_segment將資料段傳送出去,傳送完畢後,tcp_output將該資料段掛接到unacked佇列上,至於掛在unacked佇列上的什麼位置,後面再講。tcp_output_segment負責將資料段傳送出去,傳送出去後它要做的工作如下面的程式碼所示:

static void tcp_output_segment(...)
{
    if(pcb->rtime == -1)
        pcb->rtime = 0;

    if (pcb->rttest == 0) 
    {
        pcb->rttest = tcp_ticks;
        pcb->rtseq = ntohl(seg->tcphdr->seqno);
    }
}

        rtime用於重傳定時器的計數,當其值為-1時表示計數器未被使能;當值為非0時表示計數器使能,在這種情況下,rtime的值每500ms被核心加1,當rtime超過rto的值時,在unacked佇列上的所有資料段將被重傳。lwip簡化了重傳設計,並沒有為每個報文段都設定一個獨立的定時器,而是未被確認的報文段共用一個。rttest用於記錄報文段傳送時間,,當rttest為0時表示RTT估計未啟動,報文段傳送之後,rttest的值為當前系統時間tcp_ticks,並用rtseq記錄當前RTT估計的報文段編號。當接收到對方確認的rtseq編號後,就可以過根據當前時間及傳送時間計出RTT。

        TCP慢定時器(tcp_slowtmr)每500ms被系統呼叫一次,重傳定時器啟動則加1,當rttime大於RTO時,則重傳未被確認的報文段,程式碼如下:

void tcp_slowtmr(void)
{
	......
    if(pcb->rtime >= 0)	
	{
		//重傳定時器啟動,計數值加1
        ++pcb->rtime;
    }
	......
	//有資料未被確認且超時發生
    if(pcb->unacked != NULL && pcb->rtime >= pcb->rto)
	{
		if (pcb->state != SYN_SENT)
		{
			//動態設定RTO,與重傳次數和RTT的值有關,進行避讓處理
			pcb->rto = ((pcb->sa >> 3) + pcb->sv) << tcp_backoff[pcb->nrtx];
        }

          //清空重傳定時器
          pcb->rtime = 0;
		  ......
		  //重傳報文段
          tcp_rexmit_rto(pcb);
        }
      }
    }
	......
}

        tcp_rexmit_rto函式實現重傳的機制很簡單,它將unacked連結串列上的所有資料段插入到unsent佇列的前端,並將控制塊重傳次數字段nrtx加1,最後呼叫tcp_output重發資料包。LWIP通過視窗的控制以及收到確認號後遍歷unsent佇列這兩種方式使得unacked連結串列上的整個報文段被重傳可能性降到了最小。

         在資料接收上,tcp_receive函式提取收到的資料段中的ackno,並用該ackno來處理unacked佇列,即當該ackno確認了某個資料段中的所有資料,則將該資料段從unacked佇列中移除,並釋放資料段佔用的空間。同時,函式要檢查unacked佇列,如果unacked佇列中沒有被需要確認的資料段了,此時需要停止重傳定時器,否則要復位重傳定時器。很簡單,用下面的程式碼:

static void tcp_receive(struct tcp_pcb *pcb)
{
    ......
    if(pcb->unacked == NULL)
        pcb->rtime = -1;
    else
    pcb->rtime = 0;
    ......
}

        根據上面講的RTT演算法,現在我們來進行RTT估計講解,我們將上面的四個表示式做簡單的變化,就得到了LWIP中計算RTT的表示式:

Err = M-A
A = A+ Err/8= A+(M-A)/8                  ——>  8A = 8A + M-A
D =D + (|Err |-D)/4= D + (|M-A |-D)/4    ——>  4D = 4D + (|M-A |-D)
RTO = A+4D

令sa = 8A,sv = 4D,這就是TCP控制塊中的兩個欄位。帶入上面變換後的表示式,得到:
sa = sa + M - sa>>3
sv = sv + (|M - sa>>3 |-sv>>2)
RTO = sa>>3 + sv

        我們就得到了最關心的RTO值,M表示某次測量的RTT的值,在LWIP中它就是系統當前tcp_ticks值減去資料包被髮送出去時的tcp_ticks值,tcp_ticks也是在核心的500ms週期性中斷處理中被加1。來看看原始碼是怎樣進行RTT估算的,這也是在函式tcp_receive中進行的。

static void tcp_receive(struct tcp_pcb *pcb)
{
    ......
    // 有RTT正在進行且該資料段被確認
    if (pcb->rttest && TCP_SEQ_LT(pcb->rtseq, ackno))
    {  
        m = (s16_t)(tcp_ticks - pcb->rttest);   //計算M值
        m = m - (pcb->sa >> 3);                 // M - sa>>3
        pcb->sa += m;                           //更新sa
        if (m < 0)
        {
           m = -m;                              // |M - sa>>3|
        }
        m = m - (pcb->sv >> 2);                 // (|M - sa>>3 |-sv>>2)
        pcb->sv += m;                           // 更新sv
        pcb->rto = (pcb->sa >> 3) + pcb->sv;    //計算rto
        pcb->rttest = 0;                        // 停止RTT估計

    }
    ......
}

       當以某個RTO為超時值傳送資料包後,在RTO時間後未收到對該資料段的確認,則該資料包被重發,若重發後仍收不到關於該資料包的確認,這種情況下,則接下來的重發包必須按照2的指數避讓,即將RTO值設定為前一次的2倍,當重發超過一定次數後,不再對資料包進行重發。這是在500ms定時處理函式tcp_slowtmr中完成的,看看原始碼(見上面慢定時器重傳程式碼),注意:一是對處於SYN_SENT狀態的控制塊不進行超時時間的避讓,可能是由於考慮到SYN_SENT狀態一般傳送出去的是SYN握手包,二是避讓使用一個陣列tcp_backoff通過移位的方式實現,tcp_backoff定義如下:

const u8_t tcp_backoff[13] = { 1, 2, 3, 4, 5, 6, 7, 7, 7, 7, 7, 7, 7};

       在這裡面,當重傳次數多於6次時,RTO值將不再進行避讓。最後一點是函式tcp_rexmit_rto,該函式真正完成資料包的重傳工作:

void tcp_rexmit_rto(struct tcp_pcb *pcb)
{
    ......
    // 將unacked佇列全部放到
    for (seg = pcb->unacked; seg->next != NULL; seg = seg->next);  

    // 將重傳報文段加入到unsent佇列前端
    seg->next = pcb->unsent;                                 
    pcb->unsent = pcb->unacked;
    pcb->unacked = NULL;
    // 下一個要傳送的資料編號指向佇列
    pcb->snd_nxt = ntohl(pcb->unsent->tcphdr->seqno);  
    // 重傳次數加1
    ++pcb->nrtx; 
     // 重發資料包期間不進行RTT估計    
    pcb->rttest = 0;   
    // 傳送一個資料包
    tcp_output(pcb);   
    ......
}

        從這個程式碼和上面的避讓演算法裡你可以很清楚的看到欄位nrtx的作用了,它是多次重傳時設定rto值的重要變數。另外注意,在重傳期間不應該進行RTT估計,因為這種情況下的估計值往往是不準確的。這就是傳說中的Karn演算法,Karn演算法認為由於某報文即將重傳,則對該報文的計時也就失去了意義。即使收到了ACK,也無法區分它是對第一次報文,還是對第二次報文的確認。

      文件內容參考了TCPIP詳解、老衲五木(朱升林)的微博。

 

相關文章