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首部,這時,會調用my tcp_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,同時tcp_memory_pressure沒有被置位(即接收緩存中的數據量沒有太大)。tcp_grow_window中對新收到的skb的長度還有一些限制,並不總是增長rcv_ssthresh的值。具體見函數代碼。
以上是關於接收窗口,下面簡單看一下發送窗口。關於發送窗口,在struct tcp_sock中也有一些成員數據相關。
snd_wl1記錄發送窗口更新時,造成窗口更新的那個ACK數據報的第一個序號。它主要用於在下一次判斷是否需要更新發送窗口。
snd_wnd是發送窗口的大小,直接取值於來自對端的數據報的TCP首部。
max_window記錄來自對端通告的窗口的最大值。
snd_una表示當前正等待ACK的第一個序號,而發送窗口實際上是在每次收到來自對端的ACK后,都會更新,所以,實際上snd_una成了發送窗口的左邊沿。
