TCP的定時器系列 — 零視窗探測定時器
主要內容:零視窗探測定時器的實現。
核心版本:3.15.2
出現以下情況時,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);
}
}