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);
    }
}
时间: 2024-10-15 14:59:41

TCP的定时器系列 — 零窗口探测定时器的相关文章

TCP协议中的四种定时器

TCP四种定时器 重传计时器.坚持计时器.保活计时器.时间等待计时器 重传计时器: 在TCP发送报文时创建,用来确认报文是否成功发送,超过预定时间,则重新发送,设置重传计时器之后,通常有两种情况: 1.在计时器截止时间到达之前收到了对以发送报文的确认信号,则撤销此计数器: 2.计时器时间到达仍未收到确认信号,则重新发送该报文,并将计时器复位. 坚持计时器: 这种计时器通常是和窗口大小有关的. 先考虑这样一种场景:发送端由于发送速度太快,接收端的窗口大小为零,这是接收段就会发送信号告诉发送端,我现

TCP/IP之坚持定时器、报活定时器

TCP中的四个定时器: 1.超时定时器(最复杂的一个) 2.坚持定时器 3.保活定时器 4.2MSL定时器 坚持定时器用于防止通告窗口为0以后c/s双方相互等待死锁的情况:而保活定时器则用于处理半开发连接: 一. 坚持定时器 坚持定时器的原理是简单的,当TCP服务器收到了客户端的0滑动窗口报文的时候,就启动一个定时器来计时,并在定时器溢出的时候向向客户端查询窗口是否已经增大,如果得到非零的窗口就重新开始发送数据,如果得到0窗口就再开一个新的定时器准备下一次查询.通过观察可以得知,TCP的坚持定时

【Netty4 简单项目实践】六、断掉未鉴权的TCP长连接--ChannelHandelContext中的定时器用法

在TCP长连接模式下,我们需要及时释放那些未授权的TCP链接,让系统运行得更稳健一些. 首先是connect上来的TCP报文需要设置一个存活期,通过在pipleline上设置超时处理器ReadTimeoutHandler ch.pipeline().addLast(new ReadTimeoutHandler(120)); 使得一个TCP在120秒内没有收到数据就断掉. 这样做的目的是让连接者必须发TCP报文才能维持连接. 下一步在业务层对ChannelHandlerContext进行鉴权.与H

补习系列(9)-springboot 定时器,你用对了吗

目录 简介 一.应用启动任务 二.JDK 自带调度线程池 三.@Scheduled 定制 @Scheduled 线程池 四.@Async 定制 @Async 线程池 小结 简介 大多数的应用程序都离不开定时器,通常在程序启动时.运行期间会需要执行一些特殊的处理任务. 比如资源初始化.数据统计等等,SpringBoot 作为一个灵活的框架,有许多方式可以实现定时器或异步任务. 我总结了下,大致有以下几种: 使用 JDK 的 TimerTask 使用 JDK 自带调度线程池 使用 Quartz 调度

深入理解定时器系列第一篇——理解setTimeout和setInterval

很长时间以来,定时器一直是javascript动画的核心技术.但是,关于定时器,人们通常只了解如何使用setTimeout()和setInterval(),对它们的内在运行机制并不理解,对于与预想不同的实际运行状况也无法解决.本文将详细介绍定时器的相关内容 setTimeout() setTimeout()方法用来指定某个函数或字符串在指定的毫秒数之后执行.它返回一个整数,表示定时器的编号,这个值可以传递给clearTimeout()用于取消这个函数的执行 以下代码中,控制台先输出0,大概过10

PyQt5系列教程(八)定时器QTimer的使用

软硬件环境 OS X EI Capitan Python 3.5.1 PyQt 5.5.1 前言 如果需要在程序中周期性地进行某项操作,比如检测某种设备的状态,就会用到定时器.本文就来看看PyQT5中的QTimer的使用. QTimer示例 假设要实现每过一秒计数一次这个功能,来看看QTimer怎么实现? self.timer = QTimer(self) self.count = 0 self.timer.timeout.connect(self.showNum) self.startCoun

深入理解定时器系列第二篇——被誉为神器的requestAnimationFrame

前面的话 与setTimeout和setInterval不同,requestAnimationFrame不需要设置时间间隔.这有什么好处呢?为什么requestAnimationFrame被称为神器呢?本文将详细介绍HTML5新增的定时器requestAnimationFrame 引入 计时器一直是javascript动画的核心技术.而编写动画循环的关键是要知道延迟时间多长合适.一方面,循环间隔必须足够短,这样才能让不同的动画效果显得平滑流畅:另一方面,循环间隔还要足够长,这样才能确保浏览器有能

深入理解定时器系列第三篇——定时器应用(时钟、倒计时、秒表和闹钟)

前面的话 本文属于定时器的应用部分,分别用于实现与时间相关的四个应用,包括时钟.倒计时.秒表和闹钟.与时间相关需要用到时间和日期对象Date,详细情况移步至此 时钟 最简单的时钟制作办法是通过正则表达式的exec()方法,将时间对象的字符串中的时间部分截取出来,使用定时器刷新即可 <div id="myDiv"></div> <script> myDiv.innerHTML = /\d\d:\d\d:\d\d/.exec(new Date().toS

Win10系列:VC++ 定时器

计时器机制俗称"心跳",表示以特定的频率持续触发特定事件和执行特定程序的机制.在开发Windows应用商店应用的过程中,可以使用定义在Windows::UI::Xaml命名空间中的DispatcherTimer类来创建计时器.DispatcherTimer类包含了如下的成员: Tick事件,周期性触发的事件. Start函数,用于启动计时器. Stop函数,用于停止计时器. Interval属性,设置触发Tick事件的时间周期,此属性值的类型为TimeSpan. 简单介绍了Dispat