1. 程式人生 > >linux關於tcp協議ack以及亂序報文暫存的實現--立即ack/延遲ack/捎帶ack

linux關於tcp協議ack以及亂序報文暫存的實現--立即ack/延遲ack/捎帶ack

static 報文 tail 越界 並不是 truct 冗余 receive 基於

tcp需要ack,可是為了效率,並不是每發送一個數據都要等待ack,而是盡可能利用窗口機制,積累發送ack的,當然在某些特殊情況下還是需要馬上發送ack的,比如接收到亂序的數據,這種情況下,雖然接收端可以將亂序的數據包暫存,但是接收方必須發送一個ack號為按序的期望的序列號的ack給發送端,另外就是接收窗口需要調整,此時就要立刻發送ack,否則則可以延遲發送ack,看一下linux的這方面的代碼:
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
struct tcp_sock *tp = tcp_sk(sk);
//rcv_mss是估算的對端的mss,它對本端接收窗口的計算也有很大意義
if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss //如果接收到了大於一個的報文,那麽就發送ack,一下子確認兩個報文
&& __tcp_select_window(sk) >= tp->rcv_wnd) || //需要調整窗口,最大化吞吐量
tcp_in_quickack_mode(sk) ||
(ofo_possible && //收到亂序的包
skb_peek(&tp->out_of_order_queue))) {
tcp_send_ack(sk);
} else {
tcp_send_delayed_ack(sk);
}
}
大體上,上述的函數實現了RFC1122和RFC2581的關於ack的建議。
不是說ack可以在send數據的時候捎帶發送嗎?確實是這樣,每當發送數據的時候,ack都會被發送,但是發送數據是應用層的事,如果應用層不發送呢?那豈不是永遠都無法ack了嗎?所以還必須有傳輸層本身的一套機制來支持ack的發送,捎帶ack僅僅是一個補充,傳輸層的ack發送就是立即模式和延遲模式,正如上面的__tcp_ack_snd_check所展示的那樣。
發送ack其實很簡單,就是填寫一個tcp數據,ack字段設置為接收窗口最左邊的那個數據的序列號加1,延遲發送不怕和捎帶發送重復,RFC2581規定每個到來的報文只能生成一個ack,除非需要發送端重傳才會發送冗余ack,如果tcp進入了等待延遲發送ack的狀態,當接收端有數據要發送的時候就會將ack捎帶到發送端,同時清除延遲ack定時器的pendding,如此延遲ack定時器到期後就不會再發送ack了。
穩定的理想情況下,接收端的窗口也是穩定的,不需要調整的,如果接收端不發送數據只是接收數據,ack幾乎全部以延遲的方式發送給發送端,如果接收端同時也發送數據,那麽ack就會以捎帶的方式發給發送端,立即ack只會在幾種特殊的異常情況才發送。
一種是收到了一個以上的完整的tcp段,並且可能要放大窗口,為了使得吞吐量最大化,放大的窗口決不能浪費掉,於是需要立即發送ack給發送端,發送端接收到這個ack以後就會繼續發送其發送窗口中後面的數據了。發送端的mss和接收端的窗口大小相關聯,接收端的窗口設定為發送端mss的整數倍比較好,這樣內存的利用率最高,確定好接收端可以承受的窗口大小之後,如果其比當前窗口大,那麽立即發送ack使得發送端可以盡快發送數據。
另外一種是在所謂的quick模式,quick模式並不是經常的,只有在非交互的tcp連接才可能進入quick模式,因為交互的連接表明ack已經足夠快了,沒有必要立即發送ack了,一般都是捎帶ack或者延遲發送ack的,那麽如何判斷是否是交互連接呢?內核中tcp_opt結構體中有一個ack子結構體,內部有一個quick和pingpong兩個字段,其中pingpong就是判斷交互連接的,內核會在很多地方進行抉擇,根據很多參數,比如收發間隔或者用戶配置等判斷是否一個連接是交互的,如果不是交互的,那麽就存在一系列問題:1.由於用戶進程長期不取出收到的數據導致一系列的問題,於是需要協議棧瞬間回復ack,2.積壓的ack沒有回復,影響了發送端的發送速率。此時就會給quick賦予一定的數值,每發送一個ack就會消耗掉一些quick值,直到用盡了quick而進入延遲模式,quick的值和窗口相關,因為接收端最多只能確認接收窗口這麽大的數據。
立即ack的最後一種可能就是收到亂序包,表明數據已經可能丟失了,那麽應該盡快地進入補救階段,就是說要盡快進入快速重傳,此時ack也要立即發送(內核發現越界包[和亂序包有區別]後會調用tcp_send_dupack發送一個ack後丟次報文而返回),內核收到亂序報文後會在out_of_order_queue隊列緩存該亂序報本,最後會調用tcp_ack_snd_check再次發送一個ack,這個ack確認的是按序報文的最後一個,以前應該發送過該ack,這樣接收端收到亂序報文後就會發送一個冗余的ack,如果下次接收的數據仍然是亂序的,那麽就再發送一個前兩個相同的ack,這樣發送端可能就會連續接收到三個一模一樣的ack,在接收端,第三次接收到的仍然是亂序報文時,再次發送冗余ack,只有這第4個ack被發送端收到後才會進行快速重傳。這裏的一個細節就是發送端收到了4個相同的ack(3個冗余ack),從而作為進入快速重傳的標誌,linux是這麽實現的,符合了rfc的建議,但是這種實現所依賴的是其背後的一個思想。
一個報文談不上順序,最少兩個報文才有順序的概念,正如字節序一樣,utf8以一個字節為編碼單位,因此就沒有字節序的問題,同樣的,僅僅來了一個報文也不能說它對於當前按序的報文來講是亂序的,只有當第二個報文到來的時候,如果當前按序報文,第一個報文,第二個報文拼不成順序才能說明後來的這兩個報文是亂序的,當然這也是一種權衡的結果,正如三次握手為何是三次一樣,即使接收端收到了第三個亂序報文,仍有可能被第四個填充後成為按序報文,沒完沒了等下去是不行的,必須在發送端接收到確定的,不是很大的數目冗余ack的時候進入快速重傳,同時也不能頻繁的快速重傳,因此就選擇了3個冗余ack,當然這個數字是可以配置的。
最後看一下亂序報文的重新調整。linux的協議棧實現中將亂序的報文按照序列號大小順序插入到一個隊列當中,此隊列是基於連接的,如果該亂序隊列有報文暫存的話,每接收到一個報文都會嘗試調用tcp_ofo_queue函數,它的意義在於努力將亂序的報文順序化,正如上述冗余ack相關的背後的思想,每個新的報文都有可能填補按序報文和亂序報文之間的缺口,換句話說,每一個新到的報文都可能直接拼接到按序報文隊列最後一個的後面,同時也有可能完成這種拼接後,和後面的亂序隊列的最前面一個或者幾個或者全部的報文拼接,最終成為一系列按序的報文:
static void tcp_ofo_queue(struct sock *sk)
{
struct tcp_opt *tp = tcp_sk(sk);
__u32 dsack_high = tp->rcv_nxt;
struct sk_buff *skb;
while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {
if (after(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) //最前面的skb都拼不上
break;
...
if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
__skb_unlink(skb, skb->list); //曾經接收的報文段,繼續
__kfree_skb(skb);
continue;
}
__skb_unlink(skb, skb->list); //可以拼接,更新tp的rcv_next字段
__skb_queue_tail(&sk->sk_receive_queue, skb);
tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
...
}
}

再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!希望你也加入到我們人工智能的隊伍中來!https://blog.csdn.net/jiangjunshow

linux關於tcp協議ack以及亂序報文暫存的實現--立即ack/延遲ack/捎帶ack