TCP的定时器系列 — 零窗口探测定时器(有图有代码有真相!!!)
轉載
主要內容:零窗口探測定時器的實現。
內核版本:3.15.2
我的博客:http://blog.csdn.net/zhangskd
?
出現以下情況時,TCP接收方的接收緩沖區將被塞滿數據:
發送方的發送速度大于接收方的接收速度。
接收方的應用程序未能及時從接收緩沖區中讀取數據。
?
當接收方的接收緩沖區滿了以后,會把響應報文中的通告窗口字段置為0,從而阻止發送方的繼續發送,
這就是TCP的流控制。當接收方的應用程序讀取了接收緩沖區中的數據以后,接收方會發送一個ACK,通過
通告窗口字段告訴發送方自己又可以接收數據了,發送方收到這個ACK之后,就知道自己可以繼續發送數據了。
?
Q:那么問題來了,當接收方的接收窗口重新打開之后,如果它發送的ACK丟失了,發送方還能得知這一消息嗎?
A:答案是不能。正常的ACK報文不需要確認,因而也不會被重傳,如果這個ACK丟失了,發送方將無法得知對端
的接收窗口已經打開了,也就不會繼續發送數據。這樣一來,會造成傳輸死鎖,接收方等待對端發送數據包,而發送
方等待對端的ACK,直到連接超時關閉。
?
為了避免上述情況的發生,發送方實現了一個零窗口探測定時器,也叫做持續定時器:
當接收方的接收窗口為0時,每隔一段時間,發送方會主動發送探測包,通過迫使對端響應來得知其接收窗口有無打開。
這就是山不過來,我就過去:)
?
激活
?
(1) 發送數據包時
在發送數據包時,如果發送失敗,會檢查是否需要啟動零窗口探測定時器。
tcp_rcv_established
??? |--> tcp_data_snd_check
?????????????? |--> tcp_push_pending_frames
static inline void tcp_push_pending_frames(struct sock *sk) {if (tcp_send_head(sk)) { /* 發送隊列不為空 */struct tcp_sock *tp = tcp_sk(sk);__tcp_push_pending_frames(sk, tcp_current_mss(sk), tp->nonagle);} }/* Push out any pending frames which were held back due to TCP_CORK* or attempt at coalescing tiny packets.* The socket must be locked by the caller.*/ void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle) {/* If we are closed, the bytes will have to remain here.* In time closedown will finish, we empty the write queue and* all will be happy.*/if (unlikely(sk->sk_state == TCP_CLOSE))return;/* 如果發送失敗 */if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_atomic(sk, GFP_ATOMIC)))tcp_check_probe_timer(sk); /* 檢查是否需要啟用0窗口探測定時器*/ }?
當網絡中沒有發送且未確認的數據包,且本端有待發送的數據包時,啟動零窗口探測定時器。
為什么要有這兩個限定條件呢?
如果網絡中有發送且未確認的數據包,那這些包本身就可以作為探測包,對端的ACK即將到來。
如果沒有待發送的數據包,那對端的接收窗口為不為0根本不需要考慮。
static inline void tcp_check_probe_timer(struct sock *sk) {struct tcp_sock *tp = tcp_sk(sk);const struct inet_connection_sock *icsk = inet_csk(sk);/* 如果網絡中沒有發送且未確認的數據段,并且零窗口探測定時器尚未啟動,* 則啟用0窗口探測定時器。*/if (! tp->packets_out && ! icsk->icsk_pending)inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,icsk->icsk_rto, TCP_RTO_MAX); }?
(2) 接收到ACK時
tcp_ack()用于處理接收到的帶有ACK標志的段,會檢查是否要刪除或重置零窗口探測定時器。
static int tcp_ack (struct sock *sk, const struct sk_buff *skb, int flag) {...icsk->icsk_probes_out = 0; /* 清零探測次數,所以如果對端有響應ACK,實際上是沒有次數限制的 */tp->rcv_tstamp = tcp_time_stamp; /* 記錄最近接收到ACK的時間點,用于保活定時器 *//* 如果之前網絡中沒有發送且未確認的數據段 */if (! prior_packets) goto no_queue;... no_queue:/* If data was DSACKed, see if we can undo a cwnd reduction. */if (flag & FLAG_DSACKING_ACK)tcp_fastretrans_alert(sk,acked, prior_unsacked, is_dupack, flag);/* If this ack opens up a zero window, clear backoff.* It was being used to time the probes, and is probably far higher than* it needs to be for normal retransmission.*//* 如果還有待發送的數據段,而之前網絡中卻沒有發送且未確認的數據段,* 很可能是因為對端的接收窗口為0導致的,這時候便進行零窗口探測定時器的處理。*/if (tcp_send_head(sk)) /* 如果ACK打開了接收窗口,則刪除零窗口探測定時器。否則根據退避指數,給予重置 */tcp_ack_probe(sk); }?
接收到一個ACK的時候,如果之前網絡中沒有發送且未確認的數據段,本端又有待發送的數據段,
說明可能遇到對端接收窗口為0的情況。
這個時候會根據此ACK是否打開了接收窗口來進行零窗口探測定時器的處理:
1. 如果此ACK打開接收窗口。此時對端的接收窗口不為0了,可以繼續發送數據包。
??? 那么清除超時時間的退避指數,刪除零窗口探測定時器。
2. 如果此ACK是接收方對零窗口探測報文的響應,且它的接收窗口依然為0。那么根據指數退避算法,
??? 重新設置零窗口探測定時器的下次超時時間,超時時間的設置和超時重傳定時器的一樣。
#define ICSK_TIME_PROBE0 3 /* Zero window probe timer */static void tcp_ack_probe(struct sock *sk) {const struct tcp_sock *tp = tcp_sk(sk);struct inet_connection_sock *icsk = inet_csk(sk);/* Was it a usable window open ?* 對端是否有足夠的接收緩存,即我們能否發送一個包。*/if (! after(TCP_SKB_CB(tcp_send_head(sk))->end_seq, tcp_wnd_end(tp))) {icsk->icsk_backoff = 0; /* 清除退避指數 */inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0); /* 清除零窗口探測定時器*//* Socket must be waked up by subsequent tcp_data_snd_check().* This function is not for random using!*/} else { /* 否則根據退避指數重置零窗口探測定時器 */inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX), TCP_RTO_MAX);} }/* 返回發送窗口的最后一個字節序號 */ /* Returns end sequence number of the receiver's advertised window */ static inline u32 tcp_wnd_end(const struct tcp_sock *tp) {return tp->snd_una + tp->snd_wnd; }?
超時處理函數
?
icsk->icsk_retransmit_timer可同時作為:超時重傳定時器、ER延遲定時器、PTO定時器,
還有零窗口探測定時器,它們的超時處理函數都為tcp_write_timer_handler(),在函數內則
根據超時事件icsk->icsk_pending來做區分。
?
具體來說,當網絡中沒有發送且未確認的數據段時,icsk->icsk_retransmit_timer才會用作零窗口探測定時器。
而其它三個定時器的使用場景則相反,只在網絡中有發送且未確認的數據段時使用。??
和超時重傳定時器一樣,零窗口探測定時器也使用icsk->icsk_rto和退避指數來計算超時時間。
void tcp_write_timer_handler(struct sock *sk) {struct inet_connection_sock *icsk = inet_csk(sk);int event;/* 如果連接處于CLOSED狀態,或者沒有定時器在計時 */if (sk->sk_state == TCP_CLOSE || !icsk->icsk_pending)goto out;/* 如果定時器還沒有超時,那么繼續計時 */if (time_after(icsk->icsk_timeout, jiffies)) {sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);goto out;}event = icsk->icsk_pending; /* 用于表明是哪種定時器 */switch(event) {case ICSK_TIME_EARLY_RETRANS: /* ER延遲定時器觸發的 */tcp_resume_early_retransmit(sk); /* 進行early retransmit */break;case ICSK_TIME_LOSS_PROBE: /* PTO定時器觸發的 */tcp_send_loss_probe(sk); /* 發送TLP探測包 */break;case ICSK_TIME_RETRANS: /* 超時重傳定時器觸發的 */icsk->icsk_pending = 0;tcp_retransmit_timer(sk);break;case ICSK_TIME_PROBE0: /* 零窗口探測定時器觸發的 */icsk->icsk_pending = 0;tcp_probe_timer(sk);break;}out:sk_mem_reclaim(sk); }可見零窗口探測定時器的真正處理函數為tcp_probe_timer()。
static void tcp_probe_timer(struct sock *sk) {struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);int max_probes;/* 如果網絡中有發送且未確認的數據包,或者沒有待發送的數據包。* 這個時候不需要使用零窗口探測定時器。前一種情況時已經有現成的探測包了,* 后一種情況中根本就不需要發送數據了。*/if (tp->packets_out || ! tcp_send_head(sk)) {icsk->icsk_probes_out = 0; /* 清零探測包的發送次數 */return;}/* icsk_probes_out is zeroed by incoming ACKs even if they advertise zero window.* Hence, connection is killed only if we received no ACKs for normal connection timeout.* It is not killed only because window stays zero for some time, window may be zero until* armageddon and even later. We are full accordance with RFCs, only probe timer combines* both retransmission timeout and probe timeout in one bottle.*/max_probes = sysctl_tcp_retries2; /* 當沒有收到ACK時,運行發送探測包的最大次數,之后連接超時 */if (sock_flag(sk, SOCK_DEAD)) { /* 如果套接口即將關閉 */const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);max_probes = tcp_orphan_retries(sk, alive); /* 決定重傳的次數 *//* 如果當前的孤兒socket數量超過tcp_max_orphans,或者內存不夠時,關閉此連接 */if (tcp_out_of_resource(sk, alive || icsk->icsk_probes_out <= max_probes))return;}/* 如果發送出的探測報文的數目達到最大值,卻依然沒有收到對方的ACK時,關閉此連接 */if (icsk->icsk_probes_out > max_probes) { /* 實際上每次收到ACK后,icsk->icsk_probes_out都會被清零 */tcp_write_err(sk);} else {/* Only send another probe if we didn't close things up. */tcp_send_probe0(sk); /* 發送零窗口探測報文 */} }?
發送0 window探測報文和發送Keepalive探測報文用的是用一個函數tcp_write_wakeup():
1. 有新的數據段可供發送,且對端接收窗口還沒被塞滿。發送新的數據段,來作為探測包。
2. 沒有新的數據段可供發送,或者對端的接收窗口滿了。發送序號為snd_una - 1、長度為0的ACK包作為探測包。
?
和保活探測定時器不同,零窗口探測定時器總是使用第二種方法,因為此時對端的接收窗口為0。
所以會發送一個序號為snd_una - 1、長度為0的ACK包,對端收到此包后會發送一個ACK響應。
如此一來本端就能夠知道對端的接收窗口是否打開了。
/* A window probe timeout has occurred.* If window is not closed, send a partial packet else a zero probe.*/void tcp_send_probe0(struct sock *sk) {struct inet_connection_sock *icsk = inet_csk(sk);struct tcp_sock *tp = tcp_sk(sk);int err;/* 發送一個序號為snd_una - 1,長度為0的ACK包作為零窗口探測報文 */err = tcp_write_wakeup(sk);/* 如果網絡中有發送且未確認的數據包,或者沒有待發送的數據包。* 這個時候不需要使用零窗口探測定時器。前一種情況時已經有現成的探測包了,* 后一種情況中根本就不需要發送數據了。check again 8)*/if (tp->packets_out || ! tcp_send_head(sk)) {/* Cancel probe timer, if it is not required. */icsk->icsk_probes_out = 0;icsk->icsk_backoff = 0;return;}/* err:0成功,-1失敗 */if (err < = 0) {if (icsk->icsk_backoff < sysctl_tcp_retries2)icsk->icsk_backoff++; /* 退避指數 */icsk->icsk_probes_out++; /* 探測包的發送次數 */inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX), TCP_RTO_MAX); /* 重置零窗口探測定時器 */} else { /* 如果由于本地擁塞導致無法發送探測包 *//* If packet was not sent due to local congestion,* do not backoff and do not remember icsk_probes_out.* Let local senders to fight for local resources.* Use accumulated backoff yet.*/if (! icsk->icsk_probes_out)icsk->icsk_probes_out = 1;/* 使零窗口探測定時器更快的超時 */inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, min(icsk->icsk_rto << icsk->icsk->icsk_backoff, TCP_RESOURCE_PROBE_INTERVAL),TCP_RTO_MAX);} }?
總結
以上是生活随笔為你收集整理的TCP的定时器系列 — 零窗口探测定时器(有图有代码有真相!!!)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 微积分的本质
- 下一篇: 万物皆“数”:你最好学学微积分,它是上帝