TCP零窗口探测
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