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. CSS的元素显示模式与转换

    CSS的元素显示模式与转换 1. CSS的元素显示模式 1.1 块元素 <div>标签是最典型的块元素.另外常见的块元素有h1~h6.p.ul.ol.li等. 特点: 独占一行 高度.宽度 ...

  2. 【Curl】【转】curl用法!

    curl基础用法! www.ruanyifeng.com/blog/2019/09/curl-reference.html

  3. selenium-绕过登录

    第一种方式:chrome-debug 1.现在终端输入一下命令,启动Chrome-debug模式 #windows a.首先将chrome的环境添加到PATH中 b.进入cmd 命令栏,输入:chro ...

  4. C++ Primer第5版 第一章课后练习

    练习1.9 #include <iostream> int main() { int sum = 0, val = 50; while (val <= 100) { sum += v ...

  5. HTML 5标签中<button>的新属性

    <button> 标签HTML5 中的新属性 属性 值 描述 autofocus autofocus 规定当页面加载时按钮应当自动地获得焦点. disabled disabled 规定应该 ...

  6. Linux运维学习第一周记

    1 当年白岳伴清游, 2 江石台空一苇浮. 3 缥渺临风闻郢曲, 4 殷勤歧路看吴钩. 老气横秋方知世间沧桑! 以前一直忙,没有时间沉浸下来学习,一直都是浮着. 至此大疫,给生命按下了暂停键. 踏踏实 ...

  7. 攻防世界-mfw

    打开题目,让我们看看about这个链接是什么,我们看到了这个 他说他写这个站点用了git.php.bootstrap这很容易就能让我们想到,git源码泄露,这我们直接掏出githack, python ...

  8. IDEA配置类和方法注释模板

    1定义java文件头部的注释 2给java类中的方法添加上注释 2.1第一步勾选Enable Live  Templates 2.2第二步新建一个Group 2.3第三步新建一个Template 2. ...

  9. Spring Boot 加载application.properties顺序

    1.准备四份application.properties a.项目根目录下config/application.properties ,内容为:  test.user.name = a b.项目根目录 ...

  10. array_walk_recursive 地址引用报错的问题

    今天看十八哥的视频,学习array_walk_recursive的用法,发现一直报错: PHP版本:5.6.19 代码界面: 报错界面: 查了很长时间,不知道什么问题,后来在网上终于找到原因所在: + ...