TCP零窗口探測用於獲取觸發對端的窗口更新報文,防止在窗口更新報文丟失之後,導致的死循環。其也有助於本端Qdisc滿或者數據被髮送節奏(Pacing)阻止導致的發送停滯。
窗口探測開啓
在TCP報文發送函數tcp_write_xmit的處理中,如果最終未能發送任何報文,而且網絡中報文爲空(packets_out),套接口的發送隊列中有數據,將返回true。造成此情況可能是由於惰性窗口綜合徵(SWS),或者其它原因,如擁塞窗口限制、接收窗口限制等。
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
...
if (likely(sent_pkts)) {
...
return false;
}
return !tp->packets_out && !tcp_write_queue_empty(sk);
}
在發送暫緩的報文時,如果以上函數tcp_write_xmit返回true,調用函數tcp_check_probe_timer檢查探測定時器。
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_mask(sk, GFP_ATOMIC)))
tcp_check_probe_timer(sk);
}
如果此時網絡中沒有任何發送的報文,packets_oout爲空,並且本地也沒有啓動任何定時器,icsk_pending爲空意味着重傳定時器、亂序定時器、TLP定時器和窗口探測定時器都沒有啓動(這四個定時器由內核中的一個定時器結構實現,以icsk_pending中的標誌位區分)。此種情況下啓動零窗口探測定時器。
static inline void tcp_check_probe_timer(struct sock *sk)
{
if (!tcp_sk(sk)->packets_out && !inet_csk(sk)->icsk_pending)
tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
tcp_probe0_base(sk), TCP_RTO_MAX,
NULL);
}
零窗口定時器的時長由函數tcp_probe0_base決定,取值爲當前的RTO時長,但是最短不低於TCP_RTO_MIN(200毫秒)。如下函數的註釋可見,其定時器除了用於零窗口探測,也會因本端的Qdisc滿或者發送節奏導致的發送失敗,而啓動。
/* Something is really bad, we could not queue an additional packet,
* because qdisc is full or receiver sent a 0 window, or we are paced.
* We do not want to add fuel to the fire, or abort too early,
* so make sure the timer we arm now is at least 200ms in the future,
* regardless of current icsk_rto value (as it could be ~2ms)
*/
static inline unsigned long tcp_probe0_base(const struct sock *sk)
{
return max_t(unsigned long, inet_csk(sk)->icsk_rto, TCP_RTO_MIN);
}
窗口探測定時器
定時器的超時處理由函數tcp_probe_timer完成。如果網絡中存在發送的報文,packets_out有值,或者套接口發送隊列中沒有數據,退出不進行處理。
static void tcp_probe_timer(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct sk_buff *skb = tcp_send_head(sk);
struct tcp_sock *tp = tcp_sk(sk);
if (tp->packets_out || !skb) {
icsk->icsk_probes_out = 0;
return;
}
如果用戶設置了UTO(變量icsk_user_timeout的值),並且發送隊列中還有待發送報文,此報文等待的時長不能超過UTO,否則,認爲此連接已經出錯。
/* RFC 1122 4.2.2.17 requires the sender to stay open indefinitely as
* long as the receiver continues to respond probes. We support this by
* default and reset icsk_probes_out with incoming ACKs. But if the
* socket is orphaned or the user specifies TCP_USER_TIMEOUT, we
* kill the socket when the retry count and the time exceeds the
* corresponding system limit. We also implement similar policy when
* we use RTO to probe window in tcp_retransmit_timer().
*/
start_ts = tcp_skb_timestamp(skb);
if (!start_ts)
skb->skb_mstamp_ns = tp->tcp_clock_cache;
else if (icsk->icsk_user_timeout &&
(s32)(tcp_time_stamp(tp) - start_ts) > icsk->icsk_user_timeout)
goto abort;
以下代碼涉及到兩個PROC文件中的控制變量:tcp_retries2和tcp_orphan_retries,前者表示在對端不響應時,進行的最大重傳次數;後者表示本地已經關閉的套接,接收不到對端響應時的最大重傳次數。
$ cat /proc/sys/net/ipv4/tcp_retries2
15
$ cat /proc/sys/net/ipv4/tcp_orphan_retries
0
如果套接口設置了SOCK_DEAD標誌,表明本端已經關閉(Orphaned套接口),按照重傳退避係數計數的當前RTO值小於最大值TCP_RTO_MAX(120秒),說明此連接還不應斷開。之後,檢查內核設置的孤兒套接口的重傳次數(tcp_orphan_retries),如果alive爲零並且退避次數已經超出最大的Orphaned套接口探測次數,斷開連接。否則檢查TCP資源使用是否超限,如果超限,將在tcp_out_of_resources函數中斷開連接,第二個參數true表明將向對端發送RESET復位報文。
注意,內核默認的tcp_orphan_retries值爲零,所以如果alive爲零,即當前RTO值超出TCP_RTO_MAX,以下的if判斷條件成立,將導致連接的立即斷開。這種情況下,連接超時依賴於初始(第一次)RTO的值,如果其值爲最小值TCP_RTO_MIN(200ms),那麼在經過9次退避之後,RTO值將超過TCP_RTO_MAX。
max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2;
if (sock_flag(sk, SOCK_DEAD)) {
const bool alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX;
max_probes = tcp_orphan_retries(sk, alive);
if (!alive && icsk->icsk_backoff >= max_probes)
goto abort;
if (tcp_out_of_resources(sk, true))
return;
}
如果以上都沒有成立,並且,當前的探測次數小於等於以上計算的最大探測次數(tcp_retries2或者Orphan套接口探測次數),調用tcp_send_probe0發送探測報文。否則,使用tcp_write_err終止連接。
if (icsk->icsk_probes_out >= max_probes) {
abort: tcp_write_err(sk);
} else {
/* Only send another probe if we didn't close things up. */
tcp_send_probe0(sk);
}
}
如下tcp_orphan_retries函數,如果alive爲零,並且接收到ICMP報錯報文(如ICMP_PARAMETERPROB、ICMP_DEST_UNREACH等),不再進行重傳,將重傳次數設置爲零。否則,如果tcp_orphan_retries設置爲零,並且alive爲真,將重傳次數設置爲8,對於RTO最小值TCP_RTO_MIN(200ms)而言,經過8此退避之後的值將大於100秒(2**9 * 200 = 102.4秒),符合RFC1122中的規定。
static int tcp_orphan_retries(struct sock *sk, bool alive)
{
int retries = sock_net(sk)->ipv4.sysctl_tcp_orphan_retries; /* May be zero. */
/* We know from an ICMP that something is wrong. */
if (sk->sk_err_soft && !alive)
retries = 0;
/* However, if socket sent something recently, select some safe
* number of retries. 8 corresponds to >100 seconds with minimal
* RTO of 200msec. */
if (retries == 0 && alive)
retries = 8;
return retries;
}
發送窗口探測
調用tcp_write_wakeup函數時,套接口的發送隊列中一定是有數據,不然沒有必要進行窗口探測,如果隊列中的首報文序號位於發送窗口範圍內,表明一定數量的數據可發送(窗口不爲零,可能由發送端的SWS預防導致)。此情況下將發送新數據作爲探測報文,首先更新pushed_seq,之後發送的數據報文將設置TCPHDR_PSH控制位,對端接收到後應儘快將接收數據提交到應用,以便釋放可用的接收空間,打開接收窗口。
int tcp_write_wakeup(struct sock *sk, int mib)
{
struct tcp_sock *tp = tcp_sk(sk);
skb = tcp_send_head(sk);
if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) {
unsigned int mss = tcp_current_mss(sk);
unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq))
tp->pushed_seq = TCP_SKB_CB(skb)->end_seq;
接下來,看一下允許發送的報文長度,如果發送窗口允許的發送長度小於發送隊列中首報文的長度,或者首報文長度大於當前的發送MSS長度,將對首報文進行分片處理,得到一個長度爲seg_size長度(不大於MSS)的報文,接下來將發送此報文。
如果以上兩個條件都不成立,即可發送窗口允許發送首報文,並且首報文長度小於MSS,此情況下,如果首報文的tcp_gso_segs分段爲零,使用函數tcp_set_skb_tso_segs設置GSO參數,由於此時報文長度小於等於mss,分段數量tcp_gso_segs將設置爲1,接下來發送一個gso分段。但是,如果發送隊列中的首報文由多個小報文分段組成,將發送多個小報文做探測。
注意,在上一種情況中,在tcp_fragment函數中調用了tcp_set_skb_tso_segs函數進行了gso相關設置。
/* We are probing the opening of a window
* but the window size is != 0
* must have been a result SWS avoidance ( sender )
*/
if (seg_size < TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq || skb->len > mss) {
seg_size = min(seg_size, mss);
TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
if (tcp_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE,
skb, seg_size, mss, GFP_ATOMIC))
return -1;
} else if (!tcp_skb_pcount(skb))
tcp_set_skb_tso_segs(skb, mss);
TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);
if (!err)
tcp_event_new_data_sent(sk, skb);
return err;
在發送隊列中沒有數據,或者對端接收窗口變爲零時,以下嘗試發送ACK探測報文,由函數tcp_xmit_probe_skb完成。如果緊急指針SND.UP包含在(SND.UNA, SND.UNA+64K)範圍內,第二個參數urgent設置爲1。
} else {
if (between(tp->snd_up, tp->snd_una + 1, tp->snd_una + 0xFFFF))
tcp_xmit_probe_skb(sk, 1, mib);
return tcp_xmit_probe_skb(sk, 0, mib);
}
如下探測報文發送函數tcp_xmit_probe_skb,發送ACK報文,如果urgent不爲真,ACK報文序號爲SND.UNA減去1(ACK報文不佔用新序號),由於此序號已經使用過,並且對端已經接收並確認,所以對端在接收到此重複序號的ACK報文之後,將丟棄此報文,並回復ACK報文通告正確的序號。
否則,如果緊急指針urgent爲真,ACK報文序號爲SND.UNA,對端並沒有確認此序號,所以,對端可能將正常接收此ACK報文,並儘快進行Urgent數據的處理(前提是已經接收到了Urgent數據),釋放接收緩存並打開接收窗口。
static int tcp_xmit_probe_skb(struct sock *sk, int urgent, int mib)
{
struct tcp_sock *tp = tcp_sk(sk);
/* We don't queue it, tcp_transmit_skb() sets ownership. */
skb = alloc_skb(MAX_TCP_HEADER, sk_gfp_mask(sk, GFP_ATOMIC | __GFP_NOWARN));
if (!skb) return -1;
/* Reserve space for headers and set control bits. */
skb_reserve(skb, MAX_TCP_HEADER);
/* Use a previous sequence. This should cause the other
* end to send an ack. Don't queue or clone SKB, just send it.
*/
tcp_init_nondata_skb(skb, tp->snd_una - !urgent, TCPHDR_ACK);
NET_INC_STATS(sock_net(sk), mib);
return tcp_transmit_skb(sk, skb, 0, (__force gfp_t)0);
在上一節介紹的函數tcp_probe_timer的最後,調用tcp_send_probe0函數發送探測報文,如果在發送探測報文之後,檢測到用戶層發送了新報文,或者套接口發送隊列爲空,不在需要進行探測,清空探測計數,清空退避計數。
/* 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);
err = tcp_write_wakeup(sk, LINUX_MIB_TCPWINPROBE);
if (tp->packets_out || tcp_write_queue_empty(sk)) {
/* Cancel probe timer, if it is not required. */
icsk->icsk_probes_out = 0;
icsk->icsk_backoff = 0;
return;
}
如果tcp_write_wakeup成功發送探測報文,增加探測計數,如果退避計數小於tcp_retries2中限定的值,增加退避計數(icsk_backoff)。
if (err <= 0) {
if (icsk->icsk_backoff < net->ipv4.sysctl_tcp_retries2)
icsk->icsk_backoff++;
icsk->icsk_probes_out++;
probe_max = TCP_RTO_MAX;
否則,如果探測報文未能成功發送,不增加退避計數和探測計數,而是將探測定時器的超時時長限定在TCP_RESOURCE_PROBE_INTERVAL(500ms)內。以上探測報文發送成功時,此限定值爲TCP_RTO_MAX(120秒)。再次啓動探測定時器。
} 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;
probe_max = TCP_RESOURCE_PROBE_INTERVAL;
}
tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
tcp_probe0_when(sk, probe_max),
TCP_RTO_MAX,
NULL);
由以上的介紹可知,tcp_probe0_base取得的是連接的RTO時長(不低於200ms),以下tcp_probe0_when函數,執行退避操作設置探測超時時長。最長不超過限定值參數max_when。
/* Variant of inet_csk_rto_backoff() used for zero window probes */
static inline unsigned long tcp_probe0_when(const struct sock *sk, unsigned long max_when)
{
u64 when = (u64)tcp_probe0_base(sk) << inet_csk(sk)->icsk_backoff;
return (unsigned long)min_t(u64, when, max_when);
}
處理探測響應
如果對端回覆了ACK報文,但是本端套接口發送隊列無數據,直接返回不做處理。只有在發送窗口大小足以容納發送隊列的首個報文時,內核纔會停止窗口探測定時器。否則,重設探測定時器超時時間,時長由上節介紹的tcp_probe0_when函數計算而得。
static void tcp_ack_probe(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct sk_buff *head = tcp_send_head(sk);
const struct tcp_sock *tp = tcp_sk(sk);
/* Was it a usable window open? */
if (!head) return;
if (!after(TCP_SKB_CB(head)->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 {
unsigned long when = tcp_probe0_when(sk, TCP_RTO_MAX);
tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
when, TCP_RTO_MAX, NULL);
內核版本 5.0