TCP的定时器系列 — 零窗口探测定时器(有图有代码有真相!!!)
轉(zhuǎn)載
主要內(nèi)容:零窗口探測(cè)定時(shí)器的實(shí)現(xiàn)。
內(nèi)核版本:3.15.2
我的博客:http://blog.csdn.net/zhangskd
?
出現(xiàn)以下情況時(shí),TCP接收方的接收緩沖區(qū)將被塞滿數(shù)據(jù):
發(fā)送方的發(fā)送速度大于接收方的接收速度。
接收方的應(yīng)用程序未能及時(shí)從接收緩沖區(qū)中讀取數(shù)據(jù)。
?
當(dāng)接收方的接收緩沖區(qū)滿了以后,會(huì)把響應(yīng)報(bào)文中的通告窗口字段置為0,從而阻止發(fā)送方的繼續(xù)發(fā)送,
這就是TCP的流控制。當(dāng)接收方的應(yīng)用程序讀取了接收緩沖區(qū)中的數(shù)據(jù)以后,接收方會(huì)發(fā)送一個(gè)ACK,通過(guò)
通告窗口字段告訴發(fā)送方自己又可以接收數(shù)據(jù)了,發(fā)送方收到這個(gè)ACK之后,就知道自己可以繼續(xù)發(fā)送數(shù)據(jù)了。
?
Q:那么問(wèn)題來(lái)了,當(dāng)接收方的接收窗口重新打開之后,如果它發(fā)送的ACK丟失了,發(fā)送方還能得知這一消息嗎?
A:答案是不能。正常的ACK報(bào)文不需要確認(rèn),因而也不會(huì)被重傳,如果這個(gè)ACK丟失了,發(fā)送方將無(wú)法得知對(duì)端
的接收窗口已經(jīng)打開了,也就不會(huì)繼續(xù)發(fā)送數(shù)據(jù)。這樣一來(lái),會(huì)造成傳輸死鎖,接收方等待對(duì)端發(fā)送數(shù)據(jù)包,而發(fā)送
方等待對(duì)端的ACK,直到連接超時(shí)關(guān)閉。
?
為了避免上述情況的發(fā)生,發(fā)送方實(shí)現(xiàn)了一個(gè)零窗口探測(cè)定時(shí)器,也叫做持續(xù)定時(shí)器:
當(dāng)接收方的接收窗口為0時(shí),每隔一段時(shí)間,發(fā)送方會(huì)主動(dòng)發(fā)送探測(cè)包,通過(guò)迫使對(duì)端響應(yīng)來(lái)得知其接收窗口有無(wú)打開。
這就是山不過(guò)來(lái),我就過(guò)去:)
?
激活
?
(1) 發(fā)送數(shù)據(jù)包時(shí)
在發(fā)送數(shù)據(jù)包時(shí),如果發(fā)送失敗,會(huì)檢查是否需要啟動(dòng)零窗口探測(cè)定時(shí)器。
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)) { /* 發(fā)送隊(duì)列不為空 */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;/* 如果發(fā)送失敗 */if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_atomic(sk, GFP_ATOMIC)))tcp_check_probe_timer(sk); /* 檢查是否需要啟用0窗口探測(cè)定時(shí)器*/ }?
當(dāng)網(wǎng)絡(luò)中沒(méi)有發(fā)送且未確認(rèn)的數(shù)據(jù)包,且本端有待發(fā)送的數(shù)據(jù)包時(shí),啟動(dòng)零窗口探測(cè)定時(shí)器。
為什么要有這兩個(gè)限定條件呢?
如果網(wǎng)絡(luò)中有發(fā)送且未確認(rèn)的數(shù)據(jù)包,那這些包本身就可以作為探測(cè)包,對(duì)端的ACK即將到來(lái)。
如果沒(méi)有待發(fā)送的數(shù)據(jù)包,那對(duì)端的接收窗口為不為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);/* 如果網(wǎng)絡(luò)中沒(méi)有發(fā)送且未確認(rèn)的數(shù)據(jù)段,并且零窗口探測(cè)定時(shí)器尚未啟動(dòng),* 則啟用0窗口探測(cè)定時(shí)器。*/if (! tp->packets_out && ! icsk->icsk_pending)inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,icsk->icsk_rto, TCP_RTO_MAX); }?
(2) 接收到ACK時(shí)
tcp_ack()用于處理接收到的帶有ACK標(biāo)志的段,會(huì)檢查是否要?jiǎng)h除或重置零窗口探測(cè)定時(shí)器。
static int tcp_ack (struct sock *sk, const struct sk_buff *skb, int flag) {...icsk->icsk_probes_out = 0; /* 清零探測(cè)次數(shù),所以如果對(duì)端有響應(yīng)ACK,實(shí)際上是沒(méi)有次數(shù)限制的 */tp->rcv_tstamp = tcp_time_stamp; /* 記錄最近接收到ACK的時(shí)間點(diǎn),用于保活定時(shí)器 *//* 如果之前網(wǎng)絡(luò)中沒(méi)有發(fā)送且未確認(rèn)的數(shù)據(jù)段 */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.*//* 如果還有待發(fā)送的數(shù)據(jù)段,而之前網(wǎng)絡(luò)中卻沒(méi)有發(fā)送且未確認(rèn)的數(shù)據(jù)段,* 很可能是因?yàn)閷?duì)端的接收窗口為0導(dǎo)致的,這時(shí)候便進(jìn)行零窗口探測(cè)定時(shí)器的處理。*/if (tcp_send_head(sk)) /* 如果ACK打開了接收窗口,則刪除零窗口探測(cè)定時(shí)器。否則根據(jù)退避指數(shù),給予重置 */tcp_ack_probe(sk); }?
接收到一個(gè)ACK的時(shí)候,如果之前網(wǎng)絡(luò)中沒(méi)有發(fā)送且未確認(rèn)的數(shù)據(jù)段,本端又有待發(fā)送的數(shù)據(jù)段,
說(shuō)明可能遇到對(duì)端接收窗口為0的情況。
這個(gè)時(shí)候會(huì)根據(jù)此ACK是否打開了接收窗口來(lái)進(jìn)行零窗口探測(cè)定時(shí)器的處理:
1. 如果此ACK打開接收窗口。此時(shí)對(duì)端的接收窗口不為0了,可以繼續(xù)發(fā)送數(shù)據(jù)包。
??? 那么清除超時(shí)時(shí)間的退避指數(shù),刪除零窗口探測(cè)定時(shí)器。
2. 如果此ACK是接收方對(duì)零窗口探測(cè)報(bào)文的響應(yīng),且它的接收窗口依然為0。那么根據(jù)指數(shù)退避算法,
??? 重新設(shè)置零窗口探測(cè)定時(shí)器的下次超時(shí)時(shí)間,超時(shí)時(shí)間的設(shè)置和超時(shí)重傳定時(shí)器的一樣。
#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 ?* 對(duì)端是否有足夠的接收緩存,即我們能否發(fā)送一個(gè)包。*/if (! after(TCP_SKB_CB(tcp_send_head(sk))->end_seq, tcp_wnd_end(tp))) {icsk->icsk_backoff = 0; /* 清除退避指數(shù) */inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0); /* 清除零窗口探測(cè)定時(shí)器*//* Socket must be waked up by subsequent tcp_data_snd_check().* This function is not for random using!*/} else { /* 否則根據(jù)退避指數(shù)重置零窗口探測(cè)定時(shí)器 */inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX), TCP_RTO_MAX);} }/* 返回發(fā)送窗口的最后一個(gè)字節(jié)序號(hào) */ /* 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; }?
超時(shí)處理函數(shù)
?
icsk->icsk_retransmit_timer可同時(shí)作為:超時(shí)重傳定時(shí)器、ER延遲定時(shí)器、PTO定時(shí)器,
還有零窗口探測(cè)定時(shí)器,它們的超時(shí)處理函數(shù)都為tcp_write_timer_handler(),在函數(shù)內(nèi)則
根據(jù)超時(shí)事件icsk->icsk_pending來(lái)做區(qū)分。
?
具體來(lái)說(shuō),當(dāng)網(wǎng)絡(luò)中沒(méi)有發(fā)送且未確認(rèn)的數(shù)據(jù)段時(shí),icsk->icsk_retransmit_timer才會(huì)用作零窗口探測(cè)定時(shí)器。
而其它三個(gè)定時(shí)器的使用場(chǎng)景則相反,只在網(wǎng)絡(luò)中有發(fā)送且未確認(rèn)的數(shù)據(jù)段時(shí)使用。??
和超時(shí)重傳定時(shí)器一樣,零窗口探測(cè)定時(shí)器也使用icsk->icsk_rto和退避指數(shù)來(lái)計(jì)算超時(shí)時(shí)間。
void tcp_write_timer_handler(struct sock *sk) {struct inet_connection_sock *icsk = inet_csk(sk);int event;/* 如果連接處于CLOSED狀態(tài),或者沒(méi)有定時(shí)器在計(jì)時(shí) */if (sk->sk_state == TCP_CLOSE || !icsk->icsk_pending)goto out;/* 如果定時(shí)器還沒(méi)有超時(shí),那么繼續(xù)計(jì)時(shí) */if (time_after(icsk->icsk_timeout, jiffies)) {sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);goto out;}event = icsk->icsk_pending; /* 用于表明是哪種定時(shí)器 */switch(event) {case ICSK_TIME_EARLY_RETRANS: /* ER延遲定時(shí)器觸發(fā)的 */tcp_resume_early_retransmit(sk); /* 進(jìn)行early retransmit */break;case ICSK_TIME_LOSS_PROBE: /* PTO定時(shí)器觸發(fā)的 */tcp_send_loss_probe(sk); /* 發(fā)送TLP探測(cè)包 */break;case ICSK_TIME_RETRANS: /* 超時(shí)重傳定時(shí)器觸發(fā)的 */icsk->icsk_pending = 0;tcp_retransmit_timer(sk);break;case ICSK_TIME_PROBE0: /* 零窗口探測(cè)定時(shí)器觸發(fā)的 */icsk->icsk_pending = 0;tcp_probe_timer(sk);break;}out:sk_mem_reclaim(sk); }可見零窗口探測(cè)定時(shí)器的真正處理函數(shù)為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;/* 如果網(wǎng)絡(luò)中有發(fā)送且未確認(rèn)的數(shù)據(jù)包,或者沒(méi)有待發(fā)送的數(shù)據(jù)包。* 這個(gè)時(shí)候不需要使用零窗口探測(cè)定時(shí)器。前一種情況時(shí)已經(jīng)有現(xiàn)成的探測(cè)包了,* 后一種情況中根本就不需要發(fā)送數(shù)據(jù)了。*/if (tp->packets_out || ! tcp_send_head(sk)) {icsk->icsk_probes_out = 0; /* 清零探測(cè)包的發(fā)送次數(shù) */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; /* 當(dāng)沒(méi)有收到ACK時(shí),運(yùn)行發(fā)送探測(cè)包的最大次數(shù),之后連接超時(shí) */if (sock_flag(sk, SOCK_DEAD)) { /* 如果套接口即將關(guān)閉 */const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);max_probes = tcp_orphan_retries(sk, alive); /* 決定重傳的次數(shù) *//* 如果當(dāng)前的孤兒socket數(shù)量超過(guò)tcp_max_orphans,或者內(nèi)存不夠時(shí),關(guān)閉此連接 */if (tcp_out_of_resource(sk, alive || icsk->icsk_probes_out <= max_probes))return;}/* 如果發(fā)送出的探測(cè)報(bào)文的數(shù)目達(dá)到最大值,卻依然沒(méi)有收到對(duì)方的ACK時(shí),關(guān)閉此連接 */if (icsk->icsk_probes_out > max_probes) { /* 實(shí)際上每次收到ACK后,icsk->icsk_probes_out都會(huì)被清零 */tcp_write_err(sk);} else {/* Only send another probe if we didn't close things up. */tcp_send_probe0(sk); /* 發(fā)送零窗口探測(cè)報(bào)文 */} }?
發(fā)送0 window探測(cè)報(bào)文和發(fā)送Keepalive探測(cè)報(bào)文用的是用一個(gè)函數(shù)tcp_write_wakeup():
1. 有新的數(shù)據(jù)段可供發(fā)送,且對(duì)端接收窗口還沒(méi)被塞滿。發(fā)送新的數(shù)據(jù)段,來(lái)作為探測(cè)包。
2. 沒(méi)有新的數(shù)據(jù)段可供發(fā)送,或者對(duì)端的接收窗口滿了。發(fā)送序號(hào)為snd_una - 1、長(zhǎng)度為0的ACK包作為探測(cè)包。
?
和保活探測(cè)定時(shí)器不同,零窗口探測(cè)定時(shí)器總是使用第二種方法,因?yàn)榇藭r(shí)對(duì)端的接收窗口為0。
所以會(huì)發(fā)送一個(gè)序號(hào)為snd_una - 1、長(zhǎng)度為0的ACK包,對(duì)端收到此包后會(huì)發(fā)送一個(gè)ACK響應(yīng)。
如此一來(lái)本端就能夠知道對(duì)端的接收窗口是否打開了。
/* 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;/* 發(fā)送一個(gè)序號(hào)為snd_una - 1,長(zhǎng)度為0的ACK包作為零窗口探測(cè)報(bào)文 */err = tcp_write_wakeup(sk);/* 如果網(wǎng)絡(luò)中有發(fā)送且未確認(rèn)的數(shù)據(jù)包,或者沒(méi)有待發(fā)送的數(shù)據(jù)包。* 這個(gè)時(shí)候不需要使用零窗口探測(cè)定時(shí)器。前一種情況時(shí)已經(jīng)有現(xiàn)成的探測(cè)包了,* 后一種情況中根本就不需要發(fā)送數(shù)據(jù)了。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++; /* 退避指數(shù) */icsk->icsk_probes_out++; /* 探測(cè)包的發(fā)送次數(shù) */inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX), TCP_RTO_MAX); /* 重置零窗口探測(cè)定時(shí)器 */} else { /* 如果由于本地?fù)砣麑?dǎo)致無(wú)法發(fā)送探測(cè)包 *//* 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;/* 使零窗口探測(cè)定時(shí)器更快的超時(shí) */inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, min(icsk->icsk_rto << icsk->icsk->icsk_backoff, TCP_RESOURCE_PROBE_INTERVAL),TCP_RTO_MAX);} }?
總結(jié)
以上是生活随笔為你收集整理的TCP的定时器系列 — 零窗口探测定时器(有图有代码有真相!!!)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 微积分的本质
- 下一篇: 万物皆“数”:你最好学学微积分,它是上帝