1. 程式人生 > >第九章 tcp擁塞控制--基於Linux3.10

第九章 tcp擁塞控制--基於Linux3.10

tcp_sock函式使用到的控制擁塞變數如下:

snd_cwnd:擁塞控制視窗的大小

snd_ssthresh:慢啟動門限,如果snd_cwnd值小於此值這處於慢啟動階段。

snd_cwnd_cnt:當超過慢啟動門限時,該值用於降低視窗增加的速率。

snd_cwnd_clamp:snd_cwnd能夠增加到的最大尺寸。

snd_cwnd_stamp:擁塞控制視窗有效的最後一次時間戳

snd_cwnd_used:用於標記在使用的擁塞視窗的高水位值,當tcp連線的數量被應用程式限制而不是被網路所限制時,該變數用於下調snd_cwnd值。

Linux也支援使用者空間動態插入擁塞控制演算法,通過tcp_cong.c註冊,擁塞控制使用的函式通過向tcp_register_congestion_control傳遞tcp_congestion_ops實現,使用者插入的擁塞控制演算法需要支援ssthresh和con_avoid。

tp->ca_priv用於存放擁塞控制私有資料。tcp_ca(tp)返回值是指向該地址空間的。

當前有三種擁塞控制演算法:

         最簡單的源於TCP reno(高速、高伸縮性)。

         其次是更復雜點的BIC演算法、Vegas和Westwood+演算法,這類演算法對擁塞的控制會依賴於其它事件。

優秀的TCP擁塞控制演算法是複雜的,這需要再公平和效能之間權衡。

當前Linux系統使用的擁塞控制演算法取決於sysctl介面的net.ipv4.tcp_congestion_control。預設的擁塞控制演算法是最後註冊的演算法(LIFO),如果全部編譯成模組,則將使用reno演算法,如果使用預設的Kconfig配置,CUBIC演算法將會編譯進核心(不是編譯成module),並且核心將使用CUBIC演算法作為預設的擁塞控制演算法。

cubic使用的演算法

窗長增長函式:

 

C是cubic引數,t是自上一次視窗減少的時間,K是上述函式在沒有丟包時從W增加到 所花費的時間週期。其計算公式是

在擁塞避免階段收到ACK時。CUBIC在下一個RTT使用公式1計算視窗增長率。其將 設定成擁塞視窗大小。

根據當前擁塞視窗大小,CUBIC有三種狀態,TCP狀態(t時刻窗長小於標準TCP窗長)、凹區域(擁塞視窗小於 )、凸區域(擁塞視窗大於 )。

cubic慢啟動門限閾值

該方法在快速和長距離網路上使用立方函式修改擁塞線性視窗。該方法使視窗的增加獨立於RTT(round trip times),這使得具有不同RTT的流具有相對均等的網路頻寬。到達穩定階段,CUBIC在穩定階段將急速向飽和點增加,但是快到飽和點時增加速度會變慢。該特性使得CUBIC在頻寬延遲積(BDP bandwith and delay product)較大時具有很好的可擴充套件性、穩定性和公平性。立方根計算方法Newton-Raphson,誤差約為0.195%。

首先找慢啟動門限初始值snd_ssthresh,在TCP套接字初始化時tcp_prot的init成員會被呼叫,該函式直接指向tcp_v4_init_sock()。下列程式碼片段的2163行對套接字進行初始化。

net/ipv4/tcp_ipv4.c
2159 static int tcp_v4_init_sock(struct sock *sk)
2160 {
//icsk—意為inet connection sock
2161     struct inet_connection_sock *icsk = inet_csk(sk);
//套接字初始化
2163     tcp_init_sock(sk);
//ipv4連線的套接字操作函式集 
2165     icsk->icsk_af_ops = &ipv4_specific;
2166 
2171     return 0;
2172 }
2852 struct proto tcp_prot = {
2853     .name           = "TCP",
2854     .owner          = THIS_MODULE,
2855     .close          = tcp_close,
2856     .connect        = tcp_v4_connect,
2857     .disconnect     = tcp_disconnect,
2858     .accept         = inet_csk_accept,
2859     .ioctl          = tcp_ioctl,
2860     .init           = tcp_v4_init_sock,
2897 }

tcp_init_sock()用於初始化套接字,由於sk_alloc()函式在為套接字分配記憶體時,已經將一些變數的初始值設定為了0,所以tcp_init_sock()並沒有初始化所有變數。

<net/ipv4/tcp.c>
372 void tcp_init_sock(struct sock *sk)
 373 {
 374     struct inet_connection_sock *icsk = inet_csk(sk);
//如9.1節所述,tcp_sock的結構體中包含了擁塞控制所需的各種變數
 375     struct tcp_sock *tp = tcp_sk(sk);
//存放亂序tcp包的套接字連結串列初始化
 377     skb_queue_head_init(&tp->out_of_order_queue);
//重傳、延遲ack以及探測定時器初始化。
 378     tcp_init_xmit_timers(sk);
//記錄套接字直接拷貝到使用者空間資訊的結構體ucopy初始化
 379     tcp_prequeue_init(tp);
 380     INIT_LIST_HEAD(&tp->tsq_node);
//重傳超時值,起始值設定為1s。 
 382     icsk->icsk_rto = TCP_TIMEOUT_INIT;
//mdev -- medium deviation,用於RTT測量的均方差
 383     tp->mdev = TCP_TIMEOUT_INIT;
//初始擁塞視窗大小,初始值10,這就意味著窗長在大於10時才會進入擁塞演算法,而一開始進入的是慢啟動階段。
390     tp->snd_cwnd = TCP_INIT_CWND;
//慢啟動門限0x7FFFFFFF
 395     tp->snd_ssthresh = TCP_INFINITE_SSTHRESH;
//擁塞視窗最大窗長
 396     tp->snd_cwnd_clamp = ~0;
//mss maximum segment size,初始值設定為536,不包括SACKS(selective ACK)
 397     tp->mss_cache = TCP_MSS_DEFAULT;
 398 
 399     tp->reordering = sysctl_tcp_reordering;
 400     tcp_enable_early_retrans(tp);
 401     icsk->icsk_ca_ops = &tcp_init_congestion_ops;
//時間戳偏移 
 403     tp->tsoffset = 0;
//套接字當前狀態sysctl_tcp_rmem[1]對應的是default,[0]是min,[2]最大值
 405     sk->sk_state = TCP_CLOSE;
//傳送和接收buffer,
416     sk->sk_sndbuf = sysctl_tcp_wmem[1];
 417     sk->sk_rcvbuf = sysctl_tcp_rmem[1];
423 }

CUBIC演算法慢啟動門限ssthresh在兩種情況下會得到更新,一種是在接收到ack應答包,另一種是在發生擁塞時,慢啟動門限回退。對應使用到的處理函式分別是bictcp_acked()和bictcp_recalc_ssthresh()。

434 static struct tcp_congestion_ops cubictcp __read_mostly = {
//CUBIC演算法變數初始化,在tcp三次連線時,回撥用其初始化套接字的擁塞控制變數。
435     .init       = bictcp_init, 
//擁塞時慢啟動門限回退計算
436     .ssthresh   = bictcp_recalc_ssthresh, 
437     .cong_avoid = bictcp_cong_avoid, //擁塞控制
438     .set_state  = bictcp_state, //如果擁塞狀態是TCP_CA_Loss,Reset擁塞演算法CUBIC的各種變數
//擁塞視窗回退。
439     .undo_cwnd  = bictcp_undo_cwnd,
//當tcp_ack呼叫tcp_clean_rtx_queue將收到應答的資料包從重傳佇列刪除時,會呼叫bictcp_acked更新慢啟動閾值
440     .pkts_acked     = bictcp_acked, 
441     .owner      = THIS_MODULE,
442     .name       = "cubic",
443 };

在tcp_ack()函式收到ack包時,會呼叫tcp_clean_rtx_queue()將已經收到應答包的幀從重傳佇列刪除,在這個函式的末尾會呼叫bictcp_acked()更新慢啟動門限值。

396 static void bictcp_acked(struct sock *sk, u32 cnt, s32 rtt_us)
397 {
398     const struct inet_connection_sock *icsk = inet_csk(sk);
399     const struct tcp_sock *tp = tcp_sk(sk);
400     struct bictcp *ca = inet_csk_ca(sk);
401     u32 delay;
//1)混合慢啟動(train和delaed)標誌hystart預設是開啟的,2)當前窗長snd_cwnd應該滿足小於tcp_init_sock()函式設定//值,3)hystart_low_window是核心設定的最小擁塞視窗值16。
429     if (hystart && tp->snd_cwnd <= tp->snd_ssthresh &&
430         tp->snd_cwnd >= hystart_low_window)
431         hystart_update(sk, delay);
432 }
起始時慢啟動門限設定成了很大的值0x7FFFFFFF,由429和431可知,snd_cwnd會一直增加知道該值大於等於hystart_low_window(16)時,將呼叫hystart_update更新慢啟動門限值。
358 static void hystart_update(struct sock *sk, u32 delay)
359 {
360     struct tcp_sock *tp = tcp_sk(sk);
361     struct bictcp *ca = inet_csk_ca(sk);
362 
363     if (!(ca->found & hystart_detect)) {
364         u32 now = bictcp_clock();
//不論是train方法還是delayed方法滿足離開慢啟動條件,這裡將當前的snd_cwnd設定成新的慢啟動門限,即由0x7FFFFFFF
//設定成16。 
388         if (ca->found & hystart_detect)
389             tp->snd_ssthresh = tp->snd_cwnd;
390     }
391 }

9.2 cubic擁塞程式碼實現

慢啟動slow start

tcp_ack()在正確接收到應答包後,有如下程式碼:

icsk->icsk_ca_ops->cong_avoid(sk, ack, in_flight);

該程式碼呼叫tcp_cubic.c檔案的437行函式。

net/ipv4/tcp_cubic.c

305 static void bictcp_cong_avoid(struct sock *sk, u32 ack, u32 in_flight)
306 {
307     struct tcp_sock *tp = tcp_sk(sk);
308     struct bictcp *ca = inet_csk_ca(sk);
//檢查傳送出去還沒收到ACK包的數量是否已達到擁塞控制視窗上限,達到則返回。
310     if (!tcp_is_cwnd_limited(sk, in_flight))
311         return;
//當前窗長小於慢啟動門限,則進入慢啟動控制,否則進入擁塞避免
313     if (tp->snd_cwnd <= tp->snd_ssthresh) {
//判斷是否需要重置sk的CUBIC演算法使用到的變數。
314         if (hystart && after(ack, ca->end_seq))
315             bictcp_hystart_reset(sk);
//慢啟動處理函式
316         tcp_slow_start(tp);
317     } else {
//更新ca(congestion avoid)的cnt成員,擁塞避免函式會使用該成員
318         bictcp_update(ca, tp->snd_cwnd);
//擁塞避免處理演算法
319         tcp_cong_avoid_ai(tp, ca->cnt);
320     }
321 
322 }

RFC2861,檢查是否被應用程式或者擁塞視窗限制,其引數in_flight是已經發送但是還沒有經過確認的資料包,如果被限制則返回1,說明需要進行擁塞控制,否則不需要擁塞控制。

net/ipv4/tcp_cong.c

284 bool tcp_is_cwnd_limited(const struct sock *sk, u32 in_flight)
285 {
286     const struct tcp_sock *tp = tcp_sk(sk);
287     u32 left;
//未確認包數量大於等於當前的窗長,返回true
289     if (in_flight >= tp->snd_cwnd)
290         return true;
//還可以傳送的視窗剩餘量
292     left = tp->snd_cwnd - in_flight;
//判斷SK的sk_route_caps成員是否支援gso,這是軟體實現的功能。
293     if (sk_can_gso(sk) &&
// tcp_tso_win_divisor是sysctl介面,即一個TSO幀可以佔用擁塞視窗長度的百分比。
294         left * sysctl_tcp_tso_win_divisor < tp->snd_cwnd &&
//最大GSO段的大小
295         left * tp->mss_cache < sk->sk_gso_max_size &&
//最多GSO段的個數
296         left < sk->sk_gso_max_segs)
297         return true;
//沒有使用tcp_tso_win_divisor時,最多TSO可以延遲傳送的MSS的最多個數
298     return left <= tcp_max_tso_deferred_mss(tp);
299 }

不論是reno還是cubic等擁塞控制演算法,它們使用慢啟動處理函式是一樣的。當前3.10版本的核心支援RFC2581基本規範。

net/ipv4/tcp_cong.c

309 void tcp_slow_start(struct tcp_sock *tp)
310 {
311     int cnt; /* increase in packets */
312     unsigned int delta = 0;
313     u32 snd_cwnd = tp->snd_cwnd;
//如果管理員使用sysctl介面,配置了慢啟動增加值,就按照管理員的設定來,否則會以指數方式增加窗長
320     if (sysctl_tcp_max_ssthresh > 0 && tp->snd_cwnd > sysctl_tcp_max_ssthresh)
321         cnt = sysctl_tcp_max_ssthresh >> 1; /* limited slow start */將慢啟動門限除以二。
322     else
323         cnt = snd_cwnd;             /* exponential increase */
// snd_cwnd_cnt在擁塞發生時會被重置0,否則其值是一直增長的,如果起始snd_cwnd 等於10
325     tp->snd_cwnd_cnt += cnt;
326     while (tp->snd_cwnd_cnt >= snd_cwnd) {//窗長+1
327         tp->snd_cwnd_cnt -= snd_cwnd;
328         delta++;
329     }
330     tp->snd_cwnd = min(snd_cwnd + delta, tp->snd_cwnd_clamp); //傳送窗長不能超限
331 }

擁塞避免congestion avoid

 擁塞避免:從慢啟動可以看到,cwnd可以很快的增長上來,從而最大程度利用網路頻寬資源,但是cwnd不能一直這樣無限增長下去,一定需要某個限制。TCP使用了一個叫慢啟動門限(ssthresh)的變數,當cwnd超過該值後,慢啟動過程結束,進入擁塞避免階段。對於大多數TCP實現來說,ssthresh的值是65536(同樣以位元組計算)。擁塞避免的主要思想是加法增大,也就是cwnd的值不再指數級往上升,開始加法增加。此時當視窗中所有的報文段都被確認時,cwnd的大小加1,cwnd的值就隨著RTT開始線性增加,這樣就可以避免增長過快導致網路擁塞,慢慢的增加調整到網路的最佳值。

Cubic窗長更新函式如下,更新的公式參考公式1、2:

207 static inline void bictcp_update(struct bictcp *ca, u32 cwnd)
208 {
209     u64 offs;
210     u32 delta, t, bic_target, max_cnt;
211 
212     ca->ack_cnt++;  /* count the number of ACKs */
213 
214     if (ca->last_cwnd == cwnd &&
215         (s32)(tcp_time_stamp - ca->last_time) <= HZ / 32)
216         return;
217 
218     ca->last_cwnd = cwnd;
219     ca->last_time = tcp_time_stamp;
220 
221     if (ca->epoch_start == 0) {
222         ca->epoch_start = tcp_time_stamp;   /* record the beginning of an epoch */
223         ca->ack_cnt = 1;            /* start counting */
224         ca->tcp_cwnd = cwnd;            /* syn with cubic */
225 
226         if (ca->last_max_cwnd <= cwnd) {
227             ca->bic_K = 0;
228             ca->bic_origin_point = cwnd;
229         } else {
230             /* Compute new K based on
231              * (wmax-cwnd) * (srtt>>3 / HZ) / c * 2^(3*bictcp_HZ)
232              */
233             ca->bic_K = cubic_root(cube_factor
234                            * (ca->last_max_cwnd - cwnd));
235             ca->bic_origin_point = ca->last_max_cwnd;
236         }
237     }
//254~303參考公式1和公式2.
254     t = ((tcp_time_stamp + msecs_to_jiffies(ca->delay_min>>3)
255           - ca->epoch_start) << BICTCP_HZ) / HZ;
256 
257     if (t < ca->bic_K)      /* t - K */
258         offs = ca->bic_K - t;
259     else
260         offs = t - ca->bic_K;
261
262     /* c/rtt * (t-K)^3 */
263     delta = (cube_rtt_scale * offs * offs * offs) >> (10+3*BICTCP_HZ);
264     if (t < ca->bic_K)                                  /* below origin*/
265         bic_target = ca->bic_origin_point - delta;
266     else                                                    /* above origin*/
267         bic_target = ca->bic_origin_point + delta;
268 
269     /* cubic function - calc bictcp_cnt*/
270     if (bic_target > cwnd) {
271         ca->cnt = cwnd / (bic_target - cwnd);
272     } else {
273         ca->cnt = 100 * cwnd;              /* very small increment*/
274     }
275 
276     /*
277      * The initial growth of cubic function may be too conservative
278      * when the available bandwidth is still unknown.
279      */
280     if (ca->last_max_cwnd == 0 && ca->cnt > 20)
281         ca->cnt = 20;   /* increase cwnd 5% per RTT */
282 
283     /* TCP Friendly */
284     if (tcp_friendliness) {
285         u32 scale = beta_scale;
286         delta = (cwnd * scale) >> 3;
287         while (ca->ack_cnt > delta) {       /* update tcp cwnd */
288             ca->ack_cnt -= delta;
289             ca->tcp_cwnd++;
290         }
291 
292         if (ca->tcp_cwnd > cwnd){   /* if bic is slower than tcp */
293             delta = ca->tcp_cwnd - cwnd;
294             max_cnt = cwnd / delta;
295             if (ca->cnt > max_cnt)
296                 ca->cnt = max_cnt;
297         }
298     }
299 
300     ca->cnt = (ca->cnt << ACK_RATIO_SHIFT) / ca->delayed_ack;
301     if (ca->cnt == 0)           /* cannot be zero */
302         ca->cnt = 1;
303 }
<net/ipv4/tcp_cong.c>
334 /* In theory this is tp->snd_cwnd += 1 / tp->snd_cwnd (or alternative w) */
335 void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w)
336 {
337     if (tp->snd_cwnd_cnt >= w) {
338         if (tp->snd_cwnd < tp->snd_cwnd_clamp)
339             tp->snd_cwnd++;
340         tp->snd_cwnd_cnt = 0;
341     } else {
342         tp->snd_cwnd_cnt++;
343     }
344 }

後來的“快速恢復”演算法是在上述的“快速重傳”演算法後新增的,當收到3個重複ACK時,TCP最後進入的不是擁塞避免階段,而是快速恢復階段。快速重傳和快速恢復演算法一般同時使用。快速恢復的思想是“資料包守恆”原則,即同一個時刻在網路中的資料包數量是恆定的,只有當“老”資料包離開了網路後,才能向網路中傳送一個“新”的資料包,如果傳送方收到一個重複的ACK,那麼根據TCP的ACK機制就表明有一個數據包離開了網路,於是cwnd加1。如果能夠嚴格按照該原則那麼網路中很少會發生擁塞,事實上擁塞控制的目的也就在修正違反該原則的地方。

快速重傳和快速恢復

當收到亂序包時,tcp可能會立即應答,重複的應答不應該被延遲,重複ACK的目的是讓對端知道一個收到資料包亂序了,並且通知對端其期望的序列號。

由於TCP並不知道一個重複的ACK源於一個丟失的資料包還是資料包的重組,其會繼續等待是否有相同重複的ACK應答包。其基於如果資料包是亂序的,則收到重複的ACK應該數量在一個或者兩個,然後是一個新的ACK到來,如果重複的ACK出現三次及以上,則預示著一個數據包丟失了。TCP然後會立即重傳似乎丟失的資料包而不會等待重傳定時器到期。

在快速重傳似乎丟失的資料包後,擁塞避免演算法,而不是慢啟動演算法被呼叫。這就是快速恢復的意義。這一方法使得在中度擁塞的情況下能有較高的吞吐率。

具體來說快速恢復的主要步驟是:

1.當收到3個重複ACK時,把ssthresh設定為cwnd的一半,把cwnd設定為ssthresh的值加3,然後重傳丟失的報文段,加3的原因是因為收到3個重複的ACK,表明有3個“老”的資料包離開了網路。 

2.再收到重複的ACK時,擁塞視窗增加1。

3.當收到新的資料包的ACK時,把cwnd設定為第一步中的ssthresh的值。原因是因為該ACK確認了新的資料,說明從重複ACK時的資料都已收到,該恢復過程已經結束,可以回到恢復之前的狀態了,也即再次進入擁塞避免狀態。