這是我的第五篇博客,鑒於前面已經有很多人對前四個題目如三次握手等做了很透徹的分析,本博客將對擁塞控制算法做一個介紹。
首先我會簡要介紹下TCP協議,其次給出擁塞控制介紹和源代碼分析,最后結合源代碼具體分析擁塞控制算法。
一、TCP協議
關於TCP協議,其實在我的第二篇博客中:https://www.cnblogs.com/xiaofengustc/p/12012638.html 已有簡要的介紹,並且在該博客中我還拿TCP協議與HTTP協議、UDP協議做了相關對比。有興趣的同學可以參見我的第二篇博客做進一步的了解。
網上關於TCP協議的內容介紹很多:https://www.jianshu.com/p/e916bfb27daa 這篇文章介紹的極其詳細,總結一些必備的基礎知識如下:
1.TCP協議產生背景:互聯網絡與單個網絡有很大的不同,因為互聯網絡的不同部分可能有截然不同的拓撲結構、帶寬、延遲、數據包大小和其他參數,且不同主機的應用層之間經常需要可靠的、像管道一樣的連接,但是IP層不提供這樣的流機制,而是提供不可靠的包交換。
2.TCP是能夠動態地適應互聯網絡的這些特性,而且具備面對各種故障時的健壯性,且能夠在不可靠的互聯網絡上提供可靠的端到端字節流而專門設計的一個傳輸協議。
3.TCP作用原理過程:
應用層向TCP層發送用於網間傳輸的、用8位字節表示的數據流,然后TCP把數據流分區成適當長度的報文段(通常受該計算機連接的網絡的數據鏈路層的最大傳輸單元(MTU)的限制)。之后TCP把結果包傳給IP層,由它來通過網絡將包傳送給接收端實體的TCP層。TCP為了保證不發生丟包,就給每個包一個序號,同時序號也保證了傳送到接收端實體的包的按序接收。然后接收端實體對已成功收到的包發回一個相應的確認(ACK);如果發送端實體在合理的往返時延(RTT)內未收到確認,那么對應的數據包就被假設為已丟失將會被進行重傳。TCP用一個校驗和函數來檢驗數據是否有錯誤;在發送和接收時都要計算校驗和。
4.TCP協議作用過程的7個要點:數據分片、到達確認、超時重發、滑動窗口、失序處理、重復處理、數據校驗(具體可參見百度百科對TCP的解釋)
5.TCP首部格式圖:

(圖片引用自:https://blog.51cto.com/11418774/1835048)
幾個重要參數解釋如下:
緊急 URG —— 當 URG =1 時,表明緊急指針字段有效。它告訴系統此報文段中有緊急數據,應盡快傳送(相當於高優先級的數據)。
確認 ACK —— 只有當 ACK = 1 時確認號字段才有效。當 ACK =0 時,確認號無效。
推送 PSH (PuSH) —— 接收 TCP 收到 PSH = 1 的報文段,就盡快地交付接收應用進程,而不再等到整個緩存都填滿了后再向上交付。
復位 RST (ReSeT) —— 當 RST =1 時,表明 TCP 連接中出現嚴重差錯(如由於主機崩潰或其他原因),必須釋放連接,然后再重新建立運輸連接。
同步 SYN —— 同步 SYN = 1 表示這是一個連接請求或連接接受報文。
終止 FIN (FINis) —— 用來釋放一個連接。FIN =1 表明此報文段的發送端的數據已發送完畢,並要求釋放運輸連接。
6.TCP工作方式:采用三次握手來建立連接,四次揮手來釋放連接,這部分的內容網上資料極其詳細,感興趣的小伙伴可以參見百度百科TCP的內容
如下是TCP連接與終止的過程圖:

二、擁塞控制介紹和源代碼分析
首先在控制擁塞之前,我們需要了解擁塞指代的為何物。
百度百科解釋如下:擁塞現象是指到達通信子網中某一部分的分組數量過多,使得該部分網絡來不及處理,以致引起這部分乃至整個網絡性能下降的現象,嚴重時甚至會導致網絡通信業務陷入停頓,即出現死鎖現象。這種現象跟公路網中經常所見的交通擁擠一樣,當節假日公路網中車輛大量增加時,各種走向的車流相互干擾,使每輛車到達目的地的時間都相對增加(即延遲增加),甚至有時在某段公路上車輛因堵塞而無法開動(即發生局部死鎖)。
造成擁塞的原因由如下兩點:
在不同的層處理擁塞有不同的方法,具體如下:
3.在數據鏈路層可采用:重傳策略、亂序緩存策略、確認策略和流控制策略。
本博客重點介紹的是傳輸層下TCP協議的擁塞控制手段
TCP傳統的擁塞控制AIMD算法的四個部分如下:
AIMD傳統算法現在已經很少使用了,故不再貼出對它的源代碼分析
下面分析目前應用最廣泛且較為成熟的Reno算法,該算法所包含的慢啟動、擁塞避免和快速重傳、快速恢復機制,是現有的眾多算法的基礎。
源代碼位於內核linux5.0.1/net/ipv4/tcp_cong.c中,貼出部分代碼分析如下:
/* * TCP Reno congestion control * This is special case used for fallback as well. */ /* This is Jacobson's slow start and congestion avoidance. * SIGCOMM '88, p. 328. */ void tcp_reno_cong_avoid(struct sock *sk, u32 ack, u32 acked) { struct tcp_sock *tp = tcp_sk(sk); if (!tcp_is_cwnd_limited(sk)) return; /* In "safe" area, increase. */ if (tcp_in_slow_start(tp)) { acked = tcp_slow_start(tp, acked); if (!acked) return; } /* In dangerous area, increase slowly. */ tcp_cong_avoid_ai(tp, tp->snd_cwnd, acked); } EXPORT_SYMBOL_GPL(tcp_reno_cong_avoid); /* Slow start threshold is half the congestion window (min 2) */ u32 tcp_reno_ssthresh(struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); return max(tp->snd_cwnd >> 1U, 2U); } EXPORT_SYMBOL_GPL(tcp_reno_ssthresh); u32 tcp_reno_undo_cwnd(struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); return max(tp->snd_cwnd, tp->prior_cwnd); } EXPORT_SYMBOL_GPL(tcp_reno_undo_cwnd); struct tcp_congestion_ops tcp_reno = { .flags = TCP_CONG_NON_RESTRICTED, .name = "reno", .owner = THIS_MODULE, .ssthresh = tcp_reno_ssthresh, .cong_avoid = tcp_reno_cong_avoid, .undo_cwnd = tcp_reno_undo_cwnd, };
從Reno運行機制中很容易看出,為了維持一個動態平衡,必須周期性地產生一定量的丟失,再加上AIMD機制--減少快,增長慢,尤其是在大窗口環境下,由於一個數據報的丟失所帶來的窗口縮小要花費很長的時間來恢復,這樣,帶寬利用率不可能很高且隨着網絡的鏈路帶寬不斷提升,這種弊端將越來越明顯。公平性方面,根據統計數據,Reno的公平性還是得到了相當的肯定,它能夠在較大的網絡范圍內理想地維持公平性原則。
Reno算法以其簡單、有效和魯棒性成為主流,被廣泛的采用。
但是它不能有效的處理多個分組從同一個數據窗口丟失的情況。這一問題在New Reno算法中得到解決。
近幾年來,隨着高帶寬延時網絡(High Bandwidth-Delay product network)的普及,針對提高TCP帶寬利用率這一點上,又涌現出許多新的基於丟包反饋的TCP協議改進,這其中包括HSTCP、STCP、BIC-TCP、CUBIC和H-TCP。
總的來說,基於丟包反饋的協議是一種被動式的擁塞控制機制,其依據網絡中的丟包事件來做網絡擁塞判斷。即便網絡中的負載很高時,只要沒有產生擁塞丟包,協議就不會主動降低自己的發送速度。這種協議可以最大程度的利用網絡剩余帶寬,提高吞吐量。然而,由於基於丟包反饋協議在網絡近飽和狀態下所表現出來的侵略性,一方面大大提高了網絡的帶寬利用率;但另一方面,對於基於丟包反饋的擁塞控制協議來說,大大提高網絡利用率同時意味着下一次擁塞丟包事件為期不遠了,所以這些協議在提高網絡帶寬利用率的同時也間接加大了網絡的丟包率,造成整個網絡的抖動性加劇。
下面給出BIC-TCP源代碼分析如下:位於linux5.0.1/net/ipv4/tcp_bic.c中
/* BIC TCP Parameters */ struct bictcp { u32 cnt; /* increase cwnd by 1 after ACKs */ u32 last_max_cwnd; /* last maximum snd_cwnd */ u32 last_cwnd; /* the last snd_cwnd */ u32 last_time; /* time when updated last_cwnd */ u32 epoch_start; /* beginning of an epoch */ #define ACK_RATIO_SHIFT 4 u32 delayed_ack; /* estimate the ratio of Packets/ACKs << 4 */ }; static inline void bictcp_reset(struct bictcp *ca) { ca->cnt = 0; ca->last_max_cwnd = 0; ca->last_cwnd = 0; ca->last_time = 0; ca->epoch_start = 0; ca->delayed_ack = 2 << ACK_RATIO_SHIFT; } static void bictcp_init(struct sock *sk) { struct bictcp *ca = inet_csk_ca(sk); bictcp_reset(ca); if (initial_ssthresh) tcp_sk(sk)->snd_ssthresh = initial_ssthresh; } /* * Compute congestion window to use. */ static inline void bictcp_update(struct bictcp *ca, u32 cwnd) { if (ca->last_cwnd == cwnd && (s32)(tcp_jiffies32 - ca->last_time) <= HZ / 32) return; ca->last_cwnd = cwnd; ca->last_time = tcp_jiffies32; if (ca->epoch_start == 0) /* record the beginning of an epoch */ ca->epoch_start = tcp_jiffies32; /* start off normal */ if (cwnd <= low_window) { ca->cnt = cwnd; return; } /* binary increase */ if (cwnd < ca->last_max_cwnd) { __u32 dist = (ca->last_max_cwnd - cwnd) / BICTCP_B; if (dist > max_increment) /* linear increase */ ca->cnt = cwnd / max_increment; else if (dist <= 1U) /* binary search increase */ ca->cnt = (cwnd * smooth_part) / BICTCP_B; else /* binary search increase */ ca->cnt = cwnd / dist; } else { /* slow start AMD linear increase */ if (cwnd < ca->last_max_cwnd + BICTCP_B) /* slow start */ ca->cnt = (cwnd * smooth_part) / BICTCP_B; else if (cwnd < ca->last_max_cwnd + max_increment*(BICTCP_B-1)) /* slow start */ ca->cnt = (cwnd * (BICTCP_B-1)) / (cwnd - ca->last_max_cwnd); else /* linear increase */ ca->cnt = cwnd / max_increment; } /* if in slow start or link utilization is very low */ if (ca->last_max_cwnd == 0) { if (ca->cnt > 20) /* increase cwnd 5% per RTT */ ca->cnt = 20; } ca->cnt = (ca->cnt << ACK_RATIO_SHIFT) / ca->delayed_ack; if (ca->cnt == 0) /* cannot be zero */ ca->cnt = 1; } static void bictcp_cong_avoid(struct sock *sk, u32 ack, u32 acked) { struct tcp_sock *tp = tcp_sk(sk); struct bictcp *ca = inet_csk_ca(sk); if (!tcp_is_cwnd_limited(sk)) return; if (tcp_in_slow_start(tp)) tcp_slow_start(tp, acked); else { bictcp_update(ca, tp->snd_cwnd); tcp_cong_avoid_ai(tp, ca->cnt, 1); } }
可以看出其在大大提高了自身吞吐率的同時,也嚴重影響了Reno流的吞吐率。基於丟包反饋的協議產生如此低劣的TCP友好性的組要原因在於這些協議算法本身的侵略性擁塞窗口管理機制,這些協議通常認為網絡只要沒有產生丟包就一定存在多余的帶寬,從而不斷提高自己的發送速率。其發送速率從時間的宏觀角度上來看呈現出一種凹形的發展趨勢,越接近網絡帶寬的峰值發送速率增長得越快。這不僅帶來了大量擁塞丟包,同時也惡意吞並了網絡中其它共存流的帶寬資源,造成整個網絡的公平性下降。
三、擁塞控制實驗演示
可利用Wireshark 記錄若干TCP 短流(少於5 秒,如訪問web 頁面,收發郵件等)和TCP長流(長於1 分鍾,如FTP 下載大文件,用HTTP 觀看在線視頻等)。
對於每個TCP 流,可畫出其congestion window 隨時間的變化曲線,指出擁塞控制的慢啟動、擁塞避免、快恢復等階段。

詳情可參見:https://blog.csdn.net/justice0/article/details/73697213
總結:選題之前沒有意識到擁塞控制在內核中的實現實在太難了,網上對源代碼的分析很少很少,本博客介紹的特別膚淺,后續還需要加強對擁塞控制的理解。
