1、坚持定时器在接收方通告接收窗口为0,阻止发送端继续发送数据时设定。

由于连接接收端的发送窗口通告不可靠(只有数据才会确认),如果一个确认丢失了,双方就有可能因为等待对方而使连接终止:

接收放等待接收数据(因为它已经向发送方通过了一个非0窗口),而发送方在等待允许它继续发送数据的窗口更新。

为了防止上面的情况,发送方在接收到0窗口通告后,启动一个坚持定时器来周期的发送1字节的数据,以便发现接收方窗口是否已经增大。

这些从发送方发出的报文段称为窗口探测;

Q1:什么时候启动persist 定时器

1、收到ack的时候-----------------

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 { /* 否则根据退避指数重置零窗口探测定时器 */
unsigned long when = tcp_probe0_when(sk, TCP_RTO_MAX); inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
when, TCP_RTO_MAX);
}
}
/* This routine deals with incoming acks, but not outgoing ones. */
/*
接收到一个ACK的时候,如果之前网络中没有发送且未确认的数据段,
本端又有待发送的数据段,说明可能遇到对端接收窗口为0的情况。
这个时候会根据此ACK是否打开了接收窗口来进行零窗口探测定时器的处理:
1. 如果此ACK打开接收窗口。此时对端的接收窗口不为0了,可以继续发送数据包。??? 那么清除超时时间的退避指数,删除零窗口探测定时器。
2. 如果此ACK是接收方对零窗口探测报文的响应,且它的接收窗口依然为0。那么根据指数退避算法,??? 重新设置零窗口探测定时器的下次超时时间,超时时间的设置和超时重传定时器的一样。
*/
//tcp_ack()用于处理接收到的带有ACK标志的段,会检查是否要删除或重置零窗口探测定时器。
static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
s/* We very likely will need to access write queue head. *//* We passed data and got it acked, remove any soft error
* log. Something worked...
*/
///* 清零探测次数,所以如果对端有响应ACK,实际上是没有次数限制的 */
sk->sk_err_soft = 0;
icsk->icsk_probes_out = 0;
tp->rcv_tstamp = tcp_time_stamp;
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, is_dupack, &flag, &rexmit);
/* 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导致的,这时候便进行零窗口探测定时器的处理。
*/ /* 如果ACK打开了接收窗口,则删除零窗口探测定时器。否则根据退避指数,给予重置 */
if (tcp_send_head(sk))
tcp_ack_probe(sk); if (tp->tlp_high_seq)
tcp_process_tlp_ack(sk, ack, flag);
return 1; invalid_ack:
SOCK_DEBUG(sk, "Ack %u after %u:%u\n", ack, tp->snd_una, tp->snd_nxt);
return -1; old_ack:
/* If data was SACKed, tag it and see if we should send more data.
* If data was DSACKed, see if we can undo a cwnd reduction.
*/
if (TCP_SKB_CB(skb)->sacked) {
flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una,
&sack_state);
tcp_fastretrans_alert(sk, acked, is_dupack, &flag, &rexmit);
tcp_xmit_recovery(sk, rexmit);
} SOCK_DEBUG(sk, "Ack %u before %u:%u\n", ack, tp->snd_una, tp->snd_nxt);
return 0;
}

2、TCP使用__tcp_push_pending_frames发送数据时:

/* 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.
把sk发送队列中所有的skb全部发送出去
只发送队列上的第一个SKB采用tcp_push_one 最终都要调用tcp_write_xmit
*/
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);
/*
当网络中没有发送且未确认的数据包,且本端有待发送的数据包时,启动零窗口探测定时器。 为什么要有这两个限定条件呢? 如果网络中有发送且未确认的数据包,那这些包本身就可以作为探测包,对端的ACK即将到来。 如果没有待发送的数据包,那对端的接收窗口为不为0根本不需要考虑。
*/
}

对porbe的分析如下:

static inline void tcp_check_probe_timer(struct sock *sk)
{
if (!tcp_sk(sk)->packets_out && !inet_csk(sk)->icsk_pending)
inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
tcp_probe0_base(sk), TCP_RTO_MAX);
}
/* Called with BH disabled */
void tcp_write_timer_handler(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
int event;
/*
* TCP状态为CLOSE或未定义定时器事件,则
* 无需作处理。
*/
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; /*
* 由于重传定时器和持续定时器功能是共用了
* 一个定时器实现的,因此需根据定时器事件
* 来区分激活的是哪种定时器;如果event为
* ICSK_TIME_RETRANS,则调用tcp_retransmit_timer()进行重传
* 处理;如果为ICSK_TIME_PROBE0,则调用tcp_probe_timer()
* 进行持续定时器的处理.
*/
switch (event) {
case ICSK_TIME_EARLY_RETRANS:
tcp_resume_early_retransmit(sk);
break;
case ICSK_TIME_LOSS_PROBE:
tcp_send_loss_probe(sk);
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);
}
/*
/*
* "持续"定时器在对端通告接收窗口为0,阻止TCP继续发送
* 数据时设定。由于连接对端发送的窗口通告不可靠(只有
* 数据才会确认,ACK不会确认),允许TCP继续发送数据的后
* 续窗口更新有可能丢失,因此,如果TCP有数据发送,而
* 对端通告接收窗口为0,则持续定时器启动,超时后向
* 对端发送1字节的数据,以判断对端接收窗口是否已打开。
* 与重传定时器类似,持续定时器的超时值也是动态计算的,
* 取决于连接的往返时间,在5~60s之间取值。
* tcp_probe_timer()为持续定时器超时的处理函数。探测定时器就是当接收到对端的window为0的时候,需要探测对端窗口是否变大,
*/ //真正的probe报文发送在tcp_send_probe0中的tcp_write_wakeup 探测定时器在tcp_ack函数中激活, 或者在__tcp_push_pending_frames中的tcp_check_probe_timer激活
//tcp_write_timer包括数据报重传tcp_retransmit_timer和窗口探测定时器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;
u32 start_ts;
/*
(1)如果存在发送出去未被确认的段,
要么被确认返回窗口,要么重传,无需额外构造探测包
(2)或者发送队列有待发送的段,无数据需要发,
不关心窗口情况
则无需另外组织探测数据
*/
if (tp->packets_out || !tcp_send_head(sk)) {
icsk->icsk_probes_out = 0;
return;
} /* 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(tcp_send_head(sk));
if (!start_ts)
skb_mstamp_get(&tcp_send_head(sk)->skb_mstamp);
else if (icsk->icsk_user_timeout &&/* 有时间戳则判断是否超过了用户设置时间 */
(s32)(tcp_time_stamp - start_ts) > icsk->icsk_user_timeout)
goto abort;
/* 最大探测次数设置为连接状态的重试次数 */
max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2;
/*
* TCP协议规定RTT的最大值为120s(TCP_RTO_MAX),因此
* 可以通过将指数退避算法得出的超时时间与
* RTT最大值相比,来判断是否需要给对方发送
* RST。
*////这里的处理和上面的tcp_write_timeout很类似。
if (sock_flag(sk, SOCK_DEAD)) {
const bool alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX; /*
* 如果连接已断开,套接字即将关闭,则获取在
* 关闭本端TCP连接前重试次数的上限。
*/ /* 获取在本端关闭tcp前重试次数上限 */
max_probes = tcp_orphan_retries(sk, alive);
if (!alive && icsk->icsk_backoff >= max_probes)
goto abort;
/*
* 释放资源,如果该套接字在释放过程中被关闭,
* 就无需再发送持续探测段了。
*/
if (tcp_out_of_resources(sk, true))
return;
}
/* 探测次数超过了最大探测次数,错误处理,关闭连接 */
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);
}
}
/* Initiate keepalive or window probe from timer. */
/*
* tcp_write_wakeup()用来输出持续探测段。如果传输
* 控制块处于关闭状态,则直接返回失败,否
* 则传输持续探测段,过程如下:
* 1)如果发送队列不为空,则利用那些待发送
* 段来发送探测段,当然这些待发送的段至
* 少有一部分在对方的接收窗口内。
* 2)如果发送队列为空,则构造需要已确认,
* 长度为零的段发送给对端。也就是否则最终会发送序号为snd_una-1,长度为0的ack包
* 其返回值如下:
* 0: 表示发送持续探测段成功
* 小于0: 表示发送持续探测段失败
* 大于0: 表示由于本地拥塞而导致发送持续探测段失败。
*/
int tcp_write_wakeup(struct sock *sk, int mib)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb; if (sk->sk_state == TCP_CLOSE)
return -1; skb = tcp_send_head(sk);
if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) {
int err;
/*
* 如果发送队列中有段需要发送,并且最先
* 待发送的段至少有一部分在对端接收窗口
* 内,那么可以直接利用该待发送的段来发
* 送持续探测段。
*/
unsigned int mss = tcp_current_mss(sk);
/*
* 获取当前的MSS以及待分段的段长。分段得到
* 的新段必须在对方接收窗口内,待分段的段
* 长初始化为SND.UNA-SND_WND-SKB.seq.
*/
unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
/*
* 如果该段的序号已经大于pushed_seq,则需要
* 更新pushed_seq。
*/
if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq))
tp->pushed_seq = TCP_SKB_CB(skb)->end_seq; /* We are probing the opening of a window
* but the window size is != 0
* must have been a result SWS avoidance ( sender )
*/
/*
* 如果待分段段长大于剩余等待发送数据,或者段长度
* 大于当前MSS,则对该段进行分段,分段段长取待分段
* 段长与当前MSS两者中的最小值,以保证只发送出一个
* 段到对方。
*/
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, 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;
} else {
/*
* 如果发送队列为空,则构造并发送一个需要已确认、
* 长度为零的段给对端。如果处于紧急模式,则多发送
* 一个序号为SND.UNA的段给对端。
*/
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);
}
}
/* A window probe timeout has occurred.  If window is not closed send
* a partial packet else a zero probe.
*/
/*
* 当持续定时器超时之后,会调用tcp_send_probe0()
* 进行探测。
*/
void tcp_send_probe0(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
struct net *net = sock_net(sk);
unsigned long probe_max;
int err;
/*
* 输出持续探测段。
*/ /* 发送一个序号为snd_una - 1,长度为0的ACK包作为零窗口探测报文 */
err = tcp_write_wakeup(sk, LINUX_MIB_TCPWINPROBE);
/*
* 如果有已发送但未确认的段,或者发送队列为空,
* 这两种情况都无需再发送持续探测段了,因此需要
* 将icsk_probes_out和icsk_backoff清零,然后返回。
*/
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;
} if (err <= 0) {
/*
* 如果重传成功或并非由于本地拥塞而发送失败,
* 则更新icsk_backoff和icsk_probes_out,然后复位持续定时器。
*/
if (icsk->icsk_backoff < net->ipv4.sysctl_tcp_retries2)
icsk->icsk_backoff++;
icsk->icsk_probes_out++;
probe_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.
*/
/*
* 如果由于本地拥塞而导致发送失败,则不需要累计
* icsk_probes_out,同时复位持续定时器,缩短超时时间,
* 尽可能争取资源。
*/
if (!icsk->icsk_probes_out)
icsk->icsk_probes_out = 1;
probe_max = TCP_RESOURCE_PROBE_INTERVAL;
}
inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
tcp_probe0_when(sk, probe_max),
TCP_RTO_MAX);
}

坚持定时器发送探测报文并期望对端能对探测报文发送ACK,这样TCP就能得到最新的窗口信息。一旦窗口增加到可以发送数据,则正常的数据交互就可以尽快恢复。这个和keepalive类似!!!!

TCP Persist 坚持定时器的更多相关文章

  1. TCP常见的定时器三次握手与四次挥手

    1.TCP常见的定时器 在TCP协议中有的时候需要定期或者按照某个算法对某个事件进行触发,那么这个时候,TCP协议是使用定时器进行实现的.在TCP中,会有七种定时器: 建立连接定时器(connecti ...

  2. TCP常见的定时器及三次握手与四次挥手

    1.TCP常见的定时器 在TCP协议中有的时候需要定期或者按照某个算法对某个事件进行触发,那么这个时候,TCP协议是使用定时器进行实现的.在TCP中,会有七种定时器: 建立连接定时器(connecti ...

  3. 《TCP/IP具体解释》读书笔记(22章)-TCP的坚持定时器

    TCP通过让接收方指明希望从发送方接收的数据字节数(即窗体大小)来进行流量控制. 假设窗体大小为0会发生什么情况呢?这将有效阻止发送方传送数据,直到窗体变为非0为止. ACK的传输并不可靠,也就是说, ...

  4. 14.TCP的坚持定时器和保活定时器

    一.坚持定时器   1.坚持定时器的由来         TCP通过让接收方指明希望从发送方接受的窗口大小来进行流量控制.设置窗口大小为0可以组织发送方传送数据,直至窗口变为非0为止.         ...

  5. 【TCP/IP详解 卷一:协议】第二十二章 TCP的坚持定时器

    这两章来到了TCP的定时器部分,在 TCP的超时与重传 和 TCP的三握四挥 我们介绍了 TCP的重传定时器 和 TCP的2MSL定时器. 本随笔介绍 防止返回ACK丢失的死锁情况 的 坚持定时器 和 ...

  6. 《TCP/IP详解 卷一》读书笔记-----TCP persist &Keeplive timer

    1.persist timer:当接收方建议的窗口大小为0时,发送方就会停止发送,直到接收方有缓存空间时再用一个窗口值非零的ACK提示发送方可以继续发送.但是这个称为window update的ACK ...

  7. TCP四种定时器--学习笔记

    TCP使用四种定时器: 重传定时器(Retransmission Timer).坚持定时器(Persistent Timer).保活定时器(Keeplive Timer).时间等待定时器(Time_W ...

  8. 《TCP/IP详细解释》札记(23章)-TCP该保活定时器

    可能有这样的备用现实TCP连接:流通过. 也就是说.假设TCP连接的两方都没有向对方发送数据.则在两个TCP模块之间不交换不论什么信息,这意味着我们能够启动一个客户与server建立连接,然后长时间不 ...

  9. 解读TCP 四种定时器

    TCP 是提供可靠的传输层,它使用的方法之一就是确认从另一端收到的数据.但是数据和确认都可能会丢失.TCP 通过在发送时设置一个定时器来解决这个问题.如果当定时器溢出时还没收到确认,它就会重传该数据. ...

随机推荐

  1. 多测师_svn_004

    svn 版本控制工具项目管理中的版本控制问题:解决代码冲突困难容易引发bug难于恢复至正确版本无法进行权限控制项目版本发布困难 什么是版本控制是指维护工程蓝图的表座做法,能追踪工程蓝图从诞生到定案的过 ...

  2. 99%的Android开发不得不面对的三道坎,到底该怎么破?

    今年比往年要特殊一些,受疫情的影响,很多公司都出现了裁员现象.以至于最近很多技术同学也在纷纷向我倒苦水. 王鹏便是其中的一员,王鹏之前是在一线城市的一家小型互联网公司做Android应用开发.从毕业实 ...

  3. rabbitmq 交换机模式 -主题模式 topic

    建立一个交换机 tpc 并且绑定了各自的路由到 Q1 Q2 <?php require_once "./vendor/autoload.php"; use PhpAmqpLi ...

  4. spring boot:用redis+redisson实现分布式锁(redisson3.11.1/spring boot 2.2)

    一,为什么要使用分布式锁? 如果在并发时锁定代码的执行,java中用synchronized锁保证了线程的原子性和可见性 但java锁只在单机上有效,如果是多台服务器上的并发访问,则需要使用分布式锁, ...

  5. HTML5/HTML 4.01/XHTML

    HTML5/HTML 4.01/XHTML 元素和有效的 DTD 下面的表格列出了所有的 HTML5/HTML 4.01/XHTML 元素,以及它们会出现在什么文档类型 (DTD) 中: 标签 HTM ...

  6. 不可不知的资源管理调度器Hadoop Yarn

    Yarn(Yet Another Resource Negotiator)是一个资源调度平台,负责为运算程序如Spark.MapReduce分配资源和调度,不参与用户程序内部工作.同样是Master/ ...

  7. C. Vladik and Memorable Trip 解析(思維、DP)

    Codeforce 811 C. Vladik and Memorable Trip 解析(思維.DP) 今天我們來看看CF811C 題目連結 題目 給你一個數列,一個區段的數列的值是區段內所有相異數 ...

  8. E. Copying Data 解析(線段樹)

    Codeforce 292 E. Copying Data 解析(線段樹) 今天我們來看看CF292E 題目連結 題目 給你兩個陣列\(a,b\),有兩種操作:把\(a\)的一段複製到\(b\),或者 ...

  9. BP神经网络算法程序实现鸢尾花(iris)数据集分类

    作者有话说 最近学习了一下BP神经网络,写篇随笔记录一下得到的一些结果和代码,该随笔会比较简略,对一些简单的细节不加以说明. 目录 BP算法简要推导 应用实例 PYTHON代码 BP算法简要推导 该部 ...

  10. Luogu P5450 [THUPC2018]淘米神的树

    题意 写的很明白了,不需要解释. \(\texttt{Data Range:}1\leq n\leq 234567\) 题解 国 际 计 数 水 平 首先考虑一开始只有一个黑点的情况怎么做. 我们钦定 ...