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的定时器系列 — 零窗口探测定时器的更多相关文章
- TCP的定时器系列 — 超时重传定时器
主要内容:TCP定时器概述,超时重传定时器.ER延迟定时器.PTO定时器的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd Q:一条TCP连接会使用 ...
- TCP的发送系列 — tcp_sendmsg()的实现(二)
主要内容:Socket发送函数在TCP层的实现 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 在上篇blog中分析了tcp_sendmsg()这个主要函 ...
- TCP的定时器系列 — 保活定时器
主要内容:保活定时器的实现,TCP_USER_TIMEOUT选项的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 原理 HTTP有Keepaliv ...
- TCP/IP之坚持定时器、报活定时器
TCP中的四个定时器: 1.超时定时器(最复杂的一个) 2.坚持定时器 3.保活定时器 4.2MSL定时器 坚持定时器用于防止通告窗口为0以后c/s双方相互等待死锁的情况:而保活定时器则用于处理半开发 ...
- TCP定时器 之 延迟确认定时器
TCP在收到数据段但是无需马上确认时设定,如果在超时时间之内有数据要发送到对端,则确认会随着数据一起发送,即捎带ACK,如果达到超时时间则执行定时器回调立即发送ack: 启动定时器: 延迟确认定时器调 ...
- runloop 和 CFRunLoop - 定时器 - NSTimer 和 GCD定时器
1. 2. #import "ViewController.h" @interface ViewController () @property (nonatomic, strong ...
- TCP的定时器系列 — SYNACK定时器
主要内容:SYNACK定时器的实现,TCP_DEFER_ACCPET选项的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 在上一篇博客中,已经连带 ...
- 深入理解定时器系列第一篇——理解setTimeout和setInterval
× 目录 [1]setTimeout [2]setInterval [3]运行机制[4]作用[5]应用 前面的话 很长时间以来,定时器一直是javascript动画的核心技术.但是,关于定时器,人们通 ...
- 补习系列(9)-springboot 定时器,你用对了吗
目录 简介 一.应用启动任务 二.JDK 自带调度线程池 三.@Scheduled 定制 @Scheduled 线程池 四.@Async 定制 @Async 线程池 小结 简介 大多数的应用程序都离不 ...
随机推荐
- 关于mysql安装到最后一步老是停留在starting server,显示无响应
从昨天晚上到今天安装MySQL花了好长的时间,一直是在后面starting server 这部就显示无响应,查资料了解到是MySQL有残留,有些注册表文件需要手动清理,下面是具体方法. 1.先用卸载软 ...
- ACM KMP 格式输入导致TLE
在写 Oulipo POJ - 3461 时候遇上的奇怪的问题 在格式输入上不一样,提交的时候返回TLE,两段代码如下: A#include<iostream> #include< ...
- 为什么要用 Docker
作为一种新兴的虚拟化方式,Docker 跟传统的虚拟化方式相比具有众多的优势. 首先,Docker 容器的启动可以在秒级实现,这相比传统的虚拟机方式要快得多. 其次,Docker 对系统资源的利用率很 ...
- 解决ASP.NET MVC 检测到有潜在危险的 Request.Form 值
提交使用html编辑器编辑后的数据,由于Request时出现有HTML或JavaScript等字符串时,系统会认为是危险性值.立马报错. "从客户端 ... 中检测到有潜在危险的 Reque ...
- 安卓高级7 vitamio 视频框架 从raw文件下获取文件uri
vitamio免费的拥有多种解码器 而且容易操作 我们先来看看原生视频播放器的怎么使用 原生的: package qianfeng.com.videoviewdemo; import android. ...
- DrawerLayout案例
布局文件: <?xml version="1.0" encoding="utf-8"?> <android.support.v4.widget ...
- Bootstrap3 栅格系统-嵌套列
为了使用内置的栅格系统将内容再次嵌套,可以通过添加一个新的 .row 元素和一系列 .col-sm-* 元素到已经存在的 .col-sm-* 元素内.被嵌套的行(row)所包含的列(column)的个 ...
- linux下内存的统计和内存泄露类问题的定位
在产品的开发中,通过对当前系统消耗内存总量的统计,可以对产品所需内存总量进行精确的评估,从而选择合适的内存芯片与大小,降低产品的成本.在遇到内存泄露类问题时,经常会对此束手无策,本文通过对proc下进 ...
- cassandra 概述
摘要 本篇文章主要是介绍cassandra与其他NoSQL的区别以及自身的特点与应用场景.在关系数据库我们没必要选择数据库,通常需要适配oracle/mysql/sql server/db2 等多种数 ...
- Unity UGUI实现分段式血条
我们可以看到像英雄联盟等游戏里英雄头顶的血条显示并非是纯色的,而是根据血量的多少而显示一定量的格子,这种方式明显是比较友好.比较美观的,事实上我们的游戏里面也想实现这样的效果,那该怎么办呢?根据血量的 ...