TCP的首部中有一個很重要的欄位就是16位長的視窗大小,它出現在每一個TCP資料報中,配合32位的確認序號,用於向對端通告本地socket的接收視窗大小。也就是說,如果本地socket發送一個TCP資料,其32位確認序號是5,視窗大小是5840,則用於告訴對端,對端已經發出的4個位元組的資料已經收到並確認,接下來,本地socket最多能夠接收從第5個位元組開始的5840個位元組長度的資料。這是由接收方進行的一種流量控制,接收方通過告訴發送方自己所能夠接收資料的大小,達到控制發送方發送速度的目的。
結構體struct tcp_sock中有很多成員資料跟滑動視窗協議相關,需要注意的是這裡講的滑動視窗都是指本地socket的接收視窗。
成員window_clamp表示滑動視窗的最大值,滑動視窗的大小在變化的過程中不能超出這個值。它在TCP串連建立的時候被初始化,被置為最大的16位整數左移視窗的擴大因子,因為滑動視窗在TCP首部中以16位表示,window_clamp太大會導致滑動視窗不能在TCP首部中表示。
成員rx_opt是一個struct tcp_options_received結構體,它有兩個成員snd_wscale和rcv_wscale,分別表示來自對端通告的滑動視窗擴大因子(本地發送資料報時需要遵守),和本地接收滑動視窗的擴大因子。snd_wscale從來自對端的第一個SYN中擷取。rcv_wscale在本地socket建立串連時初始化,它賦值的原則是使16位整數的最大值左移rcv_wscale後,至少可以達到整個接收緩衝的最大值。接收緩衝最大值在協議棧中由全域變數mysysctl_rmem_max表示,它是256*(256+sizeof(struct sk_buff))後的值,為107520,但sysctl_tcp_rmem[3]所表示的接收緩衝的上限更大,為174760,所以,取後者,這樣的話,rcv_wscale的值幾乎可以說是固定的,為2。所以window_clamp的值就是 65535 << 2 = 262140。可見,window_clamp的值超出了接收緩衝的最大值,但這沒有關係,因為在滑動視窗增長的時候,會考慮接收緩衝的大小這個因素的。
rcv_wnd表示當前的接收視窗的大小,這個值在接收到來自對端的資料後,會變動的。它的初始值取接收緩衝大小的3/4跟MAX_TCP_WINDOW之間的最小值,MAX_TCP_WINDOW在系統中的定義為32767U。然後,還要根據mss的值作一個調整,調整邏輯是:如果mss大於3*1460,則如果當前的rcv_wnd大於兩倍的mss,就取兩倍的mss作為rcv_wnd的值;如果mss大於1460,則如果當前的rcv_wnd大於3倍的mss,就取3倍的mss作為rcv_wnd的新值;否則,如果rcv_wnd大於4倍的mss,就取4倍的mss作為rcv_wnd的新值,我們的實驗環境的mss值為1448(因為tcp首部有12位元組的時間戳記選項),所以rcv_wnd最後被調整為1448*4=5792。
rcv_ssthresh是當前的接收視窗大小的一個閥值,其初始值就置為rcv_wnd。它跟rcv_wnd配合工作,當本地socket收到資料報,並滿足一定條件時,增長rcv_ssthresh的值,在下一次發送資料報組建TCP首部時,需要通告對端當前的接收視窗大小,這時需要更新rcv_wnd,此時rcv_wnd的取值不能超過rcv_ssthresh的值。兩者配合,達到一個滑動視窗大小緩慢增長的效果。
rcv_wup記錄滑動視窗的左邊沿,即落在滑動視窗中的最小的一個序號。這樣的話,rcv_wup+rcv_wnd即為滑動視窗的右邊沿,rcv_wup+rcv_wnd-rcv_nxt即為滑動視窗的空白部分。它的初始值為0,在移動滑動視窗時被更新。 以上是關於接收滑動視窗的幾個相關資料,下面我們看看它們是如何運用在TCP協議的通訊中的。
每次發送一個TCP資料報,都要構建TCP首部,這時,會調用mytcp_select_window選擇視窗大小,視窗大小選擇的基本思想是接收緩衝剩餘空間大小的3/4,但是不能超過rcv_ssthresh的大小。但是,如果這個新選擇的視窗大小比當前視窗的剩餘大小還小,則以當前視窗的剩餘大小作為新視窗的大小。同時右移左邊沿,令rcv_wup=rcv_nxt。這個新選擇的視窗是受rcv_ssthresh限制的,一般不會有什麼問題,但我們可以看到代碼中還是作了一些上限判斷,如果擴大因子為0,則視窗大小不能超過32767U,否則不能超過65535左移擴大因子後的值。
每次接收到來自對端的一個TCP資料報,且資料報長度大於128位元組時,我們需要調用mytcp_grow_window,增加rcv_ssthresh的值,一般每次為rcv_ssthresh增長兩倍的mss,增加的條件是rcv_ssthresh小於window_clamp,並且rcv_ssthresh小於接收緩衝剩餘空間的3/4,同時mytcp_memory_pressure沒有被置位(即接收緩衝中的資料量沒有太大)。mytcp_grow_window中對新收到的skb的長度還有一些限制,並不總是增長rcv_ssthresh的值。具體見函數代碼。
以上是關於接收視窗,下面簡單看一下發送視窗。關於發送視窗,在struct tcp_sock中也有一些成員資料相關。
snd_wl1記錄發送視窗更新時,造成視窗更新的那個ACK資料報的第一個序號。它主要用於在下一次判斷是否需要更新發送視窗。
snd_wnd是發送視窗的大小,直接取值於來自對端的資料報的TCP首部。
max_window記錄來自對端通告的視窗的最大值。
snd_una表示當前正等待ACK的第一個序號,而發送視窗實際上是在每次收到來自對端的ACK後,都會更新,所以,實際上snd_una成了發送視窗的左邊沿。