延时定时器 & 重传定时器 (上传之前看的log)
} /* * 延时ACK"定时器在TCP收到必须被确认但无需马上发出 * 确认的段时设定,TCP在200ms后发送确认响应。如果在 * 这200ms内,有数据要在该连接上发送,延时ACK响应就 * 可随数据一起发送回对端,称为捎带确认。 */ static void tcp_delack_timer(unsigned long data) { struct sock *sk = (struct sock *)data; bh_lock_sock(sk); if (!sock_owned_by_user(sk)) { tcp_delack_timer_handler(sk); } else { inet_csk(sk)->icsk_ack.blocked = 1; __NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKLOCKED); /* deleguate our work to tcp_release_cb() */ if (!test_and_set_bit(TCP_DELACK_TIMER_DEFERRED, &tcp_sk(sk)->tsq_flags)) sock_hold(sk); } bh_unlock_sock(sk); sock_put(sk); }
/* Called with BH disabled */ void tcp_delack_timer_handler(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct inet_connection_sock *icsk = inet_csk(sk); /* * 回收缓存 */ sk_mem_reclaim_partial(sk); /* * 如果TCP状态为CLOSE,或者没有启动延时发送 * ACK定时器,则无需作进一步处理。 */ if (sk->sk_state == TCP_CLOSE || !(icsk->icsk_ack.pending & ICSK_ACK_TIMER)) goto out; /* * 如果超时时间还未到,则重新复位定时器, * 然后退出。 */ if (time_after(icsk->icsk_ack.timeout, jiffies)) { sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout); goto out; } /* * 检测完成后,正式进入延迟确认处理之前, * 需去掉ICSK_ACK_TIMER标志。 */ icsk->icsk_ack.pending &= ~ICSK_ACK_TIMER; /* * 如果ucopy控制块中的prequeue队列不为空,则 * 通过sk_backlog_rcv接口处理sk_backlog_rcv队列中的 * SKB。TCP中sk_backlog_rcv接口为tcp_v4_do_rcv(),由这个函数添加到sk->sk_receive_queue队列中。 */ if (!skb_queue_empty(&tp->ucopy.prequeue)) { struct sk_buff *skb; __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPSCHEDULERFAILED); while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL) sk_backlog_rcv(sk, skb); tp->ucopy.memory = 0; } /* * 如果此时有ACK需要发送,则调用tcp_send_ack()构造 * 并发送ACK段,在发送ACK段之前需先离开pingpong * 模式,并重新设定延时确认的估算值。 */ if (inet_csk_ack_scheduled(sk)) { if (!icsk->icsk_ack.pingpong) { /* Delayed ACK missed: inflate ATO. */ icsk->icsk_ack.ato = min(icsk->icsk_ack.ato << 1, icsk->icsk_rto); } else { /* Delayed ACK missed: leave pingpong mode and * deflate ATO.在延迟ACK定时器的超时处理函数中,如果检查到套接口开启了pingpong模式,将执行关闭。
可见在ACK超时之前,本地并没有发送任何数据到对端,表明套接口可能并非交互式应用。 */
icsk->icsk_ack.pingpong = 0; icsk->icsk_ack.ato = TCP_ATO_MIN; } tcp_send_ack(sk); __NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKS); } out: if (tcp_under_memory_pressure(sk)) sk_mem_reclaim(sk);
在TCP三次握手过程中,当客户端套接口接收到服务端回复的SYN+ACK报文后,如果客户端套接口设置了ACK的pingpong模式,表明马上会有数据发送,将延后ACK的回复,等待和数据一起发送。
static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th) { struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); struct tcp_fastopen_cookie foc = { .len = -1 }; int saved_clamp = tp->rx_opt.mss_clamp; ----------------------------------- if (sk->sk_write_pending || icsk->icsk_accept_queue.rskq_defer_accept || icsk->icsk_ack.pingpong) { /* Save one ACK. Data will be ready after * several ticks, if write_pending is set. * * It may be deleted, but with this feature tcpdumps * look so _wonderfully_ clever, that I was not able * to stand against the temptation 8) --ANK 在TCP三次握手过程中,当客户端套接口接收到服务端回复的SYN+ACK报文后, 如果客户端套接口设置了ACK的pingpong模式,表明马上会有数据发送, 将延后ACK的回复,等待和数据一起发送。 */ inet_csk_schedule_ack(sk); icsk->icsk_ack.lrcvtime = tcp_time_stamp; tcp_enter_quickack_mode(sk); inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK, TCP_DELACK_MAX, TCP_RTO_MAX); discard: tcp_drop(sk, skb); return 0; } else { tcp_send_ack(sk); } return -1; } /*TCP两端的套接口为交互模式时,数据在两个方向交叉发送,pingpong模式可减少单独ACK报文的发送。没有必要单独发送ack报文 */
延时确认定时器的激活: tcp的第三次握手可能出现;
比如:tcp_defer_accept 或者不在快速确认模式下; 在准备立即发送ack时,没有内存了,不能构造包、、、延时发送
重传定时器
(1) 定时器的创建
tcp_v4_init_sock
|-> tcp_init_sock
|-> tcp_init_xmit_timers
|-> inet_csk_init_xmit_timers
在初始化连接时,设置三个定时器实例的处理函数:
icsk->icsk_retransmit_timer的处理函数为tcp_write_timer()
icsk->icsk_delack_timer的处理函数为tcp_delack_timer()
sk->sk_timer的处理函数为tcp_keepalive_timer()
这4个实例分别是:
icsk->icsk_retransmit_timer:超时重传定时器、持续定时器、ER延迟定时器、PTO定时器。
icsk->icsk_delack_timer:ACK延迟定时器。
sk->sk_timer:保活定时器,SYNACK定时器,FIN_WAIT2定时器。
death_row->tw_timer:TIME_WAIT定时器。
(2) 定时器的删除
tcp_done
tcp_disconnect
tcp_v4_destroy_sock
|-> tcp_clear_xmit_timers
|-> inet_csk_clear_xmit_timers
激活
icsk->icsk_retransmit_timer和icsk->icsk_delack_timer的激活函数为inet_csk_reset_xmit_timer(),
其中,超时重传定时器(ICSK_TIME_RETRANS)在以下几种情况下会被激活:
1. 发现对端把保存在接收缓冲区的SACK段丢弃时。
2. 发送一个数据段时,发现之前网络中不存在发送且未确认的段。
之后每当收到确认了新数据段的ACK,则重置定时器。
3. 发送SYN包后。
4. 一些特殊情况。
/* * 重传定时器在TCP发送数据时设定,如果定时器 * 已超时而对端确认还未到达,则TCP将重传数据。 * 重传定时器的超时时间值是动态计算的,取决于 * TCP为该连接测量的往返时间以及该段已被重传 * 的次数。 */ //tcp_write_timer包括数据报重传tcp_retransmit_timer和窗口探测定时器tcp_probe_timer static void tcp_write_timer(unsigned long data) { struct sock *sk = (struct sock *)data; bh_lock_sock(sk); if (!sock_owned_by_user(sk)) { tcp_write_timer_handler(sk); } else { /* deleguate our work to tcp_release_cb() *///tcp_release_cb /*如果icsk->icsk_retransmit_timer超时时socket被应用进程锁定, 则设置TCP_WRITE_TIMER_DEFERRED标记,这样在应用进程释放socket时会调用tcp_release_cb函数 最后 根据tsq_flags调tcp_write_timer_handler */ if (!test_and_set_bit(TCP_WRITE_TIMER_DEFERRED, &tcp_sk(sk)->tsq_flags)) sock_hold(sk); } bh_unlock_sock(sk); sock_put(sk); }
icsk->icsk_retransmit_timer可同时作为:超时重传定时器、持续定时器、ER延迟定时器、PTO定时器,
所以需要判断是哪种定时器触发的,然后采取相应的处理措施。
/* 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:
/*
如果拥塞窗口较小且数据的最后一段数据丢失时,快速重传算法会因为无法收到足够数量的ACK而无法及时重传丢失的报文。尾部丢失探测(Tail 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);
/* * The TCP retransmit timer. */ /*tcp_write_timer包括数据报重传tcp_retransmit_timer和窗口探测定时器tcp_probe_timer //见tcp_event_new_data_sent,prior_packets为0时才会重启定时器, 而prior_packets则是发送未确认的段的个数,也就是说 如果发送了很多段,如果前面的段没有确认,那么后面发送的时候不会重启这个定时器. //tcp_rearm_rto ///为0说明所有的传输的段都已经acked。此时remove定时器 。否则重启定时器。 */ void tcp_retransmit_timer(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct net *net = sock_net(sk); struct inet_connection_sock *icsk = inet_csk(sk); if (tp->fastopen_rsk) { WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV && sk->sk_state != TCP_FIN_WAIT1); /* fastopen重传syn+ack */ tcp_fastopen_synack_timer(sk); /* Before we receive ACK to our SYN-ACK don't retransmit * anything else (e.g., data or FIN segments). */ return; } /* 发送队列列出的段都已经得到确认 没有发送且未确认的数据段 */ if (!tp->packets_out) goto out; WARN_ON(tcp_write_queue_empty(sk)); tp->tlp_high_seq = 0;//发生RTO超时表明数据包的确丢失了 /* 对端窗口为0,套接口状态不是DEAD, 连接不是出于连接过程中的状态 */ if (!tp->snd_wnd && !sock_flag(sk, SOCK_DEAD) && !((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV))) { /* Receiver dastardly shrinks window. Our retransmits * become zero probes, but we should not timeout this * connection. If the socket is an orphan, time it out, * we cannot allow such beasts to hang infinitely. */ struct inet_sock *inet = inet_sk(sk); if (sk->sk_family == AF_INET) { net_dbg_ratelimited("Peer %pI4:%u/%u unexpectedly shrunk window %u:%u (repaired)\n", &inet->inet_daddr, ntohs(inet->inet_dport), inet->inet_num, tp->snd_una, tp->snd_nxt); } #if IS_ENABLED(CONFIG_IPV6) else if (sk->sk_family == AF_INET6) { net_dbg_ratelimited("Peer %pI6:%u/%u unexpectedly shrunk window %u:%u (repaired)\n", &sk->sk_v6_daddr, ntohs(inet->inet_dport), inet->inet_num, tp->snd_una, tp->snd_nxt); } #endif /* * 在重传过程中,如果超时重传超时上限TCP_RTO_MAX(120s)还没有接收 * 到对方的确认,则认为有错误发生,调用tcp_write_err()报告错误并 * 关闭套接字,然后返回;否则TCP进入拥塞控制的LOSS状态,并重新 * 传送重传队列中的第一个段。 */ if (tcp_time_stamp - tp->rcv_tstamp > TCP_RTO_MAX) { tcp_write_err(sk); goto out; } tcp_enter_loss(sk); /* 进入Loss状态,标志丢失的数据段 */ /* 重传发送队列的head 第一个数据段 */ tcp_retransmit_skb(sk, tcp_write_queue_head(sk), 1); /* * 由于发生了重传,传输控制块中的路由缓存项需更新, * 因此将其清除,最后跳转到out_reset_timer标签处处理。 */ __sk_dst_reset(sk); goto out_reset_timer; } //走到下面说明是处于连接建立阶段或者对方的滑动窗口为0了 /* * 当发生重传之后,需要检测当前的资源使用 * 情况和重传的次数.如果重传次数达到上限, * 则需要报告错误并强行关闭套接字.如果只 * 是使用的资源达到使用的上限,则不进行此 * 次重传. */ if (tcp_write_timeout(sk)) goto out; /* * 如果重传次数为0,说明刚进入重传阶段,则 * 根据不同的拥塞状态进行相关的数据统计. 第一次重传可能是对方滑动窗口满,需要进行拥塞控制 */ if (icsk->icsk_retransmits == 0) { int mib_idx; if (icsk->icsk_ca_state == TCP_CA_Recovery) { if (tcp_is_sack(tp)) mib_idx = LINUX_MIB_TCPSACKRECOVERYFAIL; else mib_idx = LINUX_MIB_TCPRENORECOVERYFAIL; } else if (icsk->icsk_ca_state == TCP_CA_Loss) { mib_idx = LINUX_MIB_TCPLOSSFAILURES; } else if ((icsk->icsk_ca_state == TCP_CA_Disorder) || tp->sacked_out) { if (tcp_is_sack(tp)) mib_idx = LINUX_MIB_TCPSACKFAILURES; else mib_idx = LINUX_MIB_TCPRENOFAILURES; } else { mib_idx = LINUX_MIB_TCPTIMEOUTS; } __NET_INC_STATS(sock_net(sk), mib_idx); } /*老版本内核为 * 判断是否可使用F-RTO算法进行处理, * 如果可以则调用tcp_enter_frto()进行F-RTO * 算法的处理,否则调用tcp_enter_loss()进入 * 常规的RTO慢启动重传恢复阶段. *现在最新的版本为 直接进入loss版本 */ tcp_enter_loss(sk); /* * 如果发送重传队列上的第一个SKB失败,则复位 * 重传定时器,等待下次重传. */ if (tcp_retransmit_skb(sk, tcp_write_queue_head(sk), 1) > 0) { /* Retransmission failed because of local congestion, * do not backoff. */ if (!icsk->icsk_retransmits) icsk->icsk_retransmits = 1; inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, min(icsk->icsk_rto, TCP_RESOURCE_PROBE_INTERVAL), TCP_RTO_MAX); goto out; } /* Increase the timeout each time we retransmit. Note that * we do not increase the rtt estimate. rto is initialized * from rtt, but increases here. Jacobson (SIGCOMM 88) suggests * that doubling rto each time is the least we can get away with. * In KA9Q, Karn uses this for the first few times, and then * goes to quadratic. netBSD doubles, but only goes up to *64, * and clamps at 1 to 64 sec afterwards. Note that 120 sec is * defined in the protocol as the maximum possible RTT. I guess * we'll have to use something other than TCP to talk to the * University of Mars. * * PAWS allows us longer timeouts and large windows, so once * implemented ftp to mars will work nicely. We will have to fix * the 120 second clamps though! */ /* * 发送成功后,递增指数退避算法指数icsk_backoff * 和累计重传次数icsk_retransmits. */ icsk->icsk_backoff++; icsk->icsk_retransmits++; out_reset_timer: /* If stream is thin, use linear timeouts. Since 'icsk_backoff' is * used to reset timer, set to 0. Recalculate 'icsk_rto' as this * might be increased if the stream oscillates between thin and thick, * thus the old value might already be too high compared to the value * set by 'tcp_set_rto' in tcp_input.c which resets the rto without * backoff. Limit to TCP_THIN_LINEAR_RETRIES before initiating * exponential backoff behaviour to avoid continue hammering * linear-timeout retransmissions into a black hole */ if (sk->sk_state == TCP_ESTABLISHED && (tp->thin_lto || sysctl_tcp_thin_linear_timeouts) && tcp_stream_is_thin(tp) && icsk->icsk_retransmits <= TCP_THIN_LINEAR_RETRIES) { icsk->icsk_backoff = 0; icsk->icsk_rto = min(__tcp_set_rto(tp), TCP_RTO_MAX); } else {//计算rto,并重启定时器,这里使用karn算法,也就是下次超时时 /* Use normal (exponential) backoff */ icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX); } /* * 完成重传后,需要设重传超时时间,然后复位重传 * 定时器,等待下次重传. */ inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX); if (retransmits_timed_out(sk, net->ipv4.sysctl_tcp_retries1 + 1, 0, 0)) __sk_dst_reset(sk); out: return; }
很明显,重传不能无限的进行下去,当重传的次数超过设定的上限时,就会判定连接超时,关闭该连接。
此后相应的socket函数,比如connect和send,就会返回-1,errno设为ETIMEDOUT,表示连接超时。判定连接是否超时,如果超过了最大等待时间,就放弃此连接。
/* A write timeout has occurred. Process the after effects.很明显,重传不能无限的进行下去,当重传的次数超过设定的上限时,就会判定连接超时,关闭该连接 */ static int tcp_write_timeout(struct sock *sk) { struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); struct net *net = sock_net(sk); int retry_until; bool do_reset, syn_set = false; /* * 在建立连接阶段超时,则需要检测使用的 * 路由缓存项,并获取重试次数的最大值. */ if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) { if (icsk->icsk_retransmits) { dst_negative_advice(sk); if (tp->syn_fastopen || tp->syn_data) tcp_fastopen_cache_set(sk, 0, NULL, true, 0); if (tp->syn_data && icsk->icsk_retransmits == 1) NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFASTOPENACTIVEFAIL); } retry_until = icsk->icsk_syn_retries ? : net->ipv4.sysctl_tcp_syn_retries; syn_set = true; } else { /* * 当重传次数达到sysctl_tcp_retries1时,则需要进行 * 黑洞检测.完成黑洞检测后还需检测使用的 * 路由缓存项. * 系统启用路径MTU发现时,如果路径MTU发现的 * 控制数据块中的开关没有开启,则将其开启, * 并根据PMTU同步MSS.否则,将当前路径MTU发现 * 区间左端点的一半作为新区间的左端点重新 * 设定路径MTU发现区间,并根据路径MTU同步MSS. */ /* tcp_retries1默认为3,当重传次数超过此值时,表示可能遇到了黑洞,需要进行PMTU * 检测,同时更新路由缓存。 */ if (retransmits_timed_out(sk, net->ipv4.sysctl_tcp_retries1, 0, 0)) { /* Some middle-boxes may black-hole Fast Open _after_ * the handshake. Therefore we conservatively disable * Fast Open on this path on recurring timeouts with * few or zero bytes acked after Fast Open. */ if (tp->syn_data_acked && tp->bytes_acked <= tp->rx_opt.mss_clamp) { tcp_fastopen_cache_set(sk, 0, NULL, true, 0); if (icsk->icsk_retransmits == net->ipv4.sysctl_tcp_retries1) NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFASTOPENACTIVEFAIL); } /* Black hole detection */ tcp_mtu_probing(icsk, sk); dst_negative_advice(sk); } /* * 如果当前套接字连接已断开并即将关闭,则 * 需要对当前使用的资源进行检测.当前的孤 * 儿套接字数量达到sysctl_tcp_max_orphans或者当前 * 已使用内存达到硬性限制时,需要即刻关闭 * 该套接字,这虽然不符合TCP的规范,但为了防 * 止DoS攻击必须这么处理. */ retry_until = net->ipv4.sysctl_tcp_retries2; /* 在断开TCP连接之前,最多进行多少次重传,默认值为15 */ if (sock_flag(sk, SOCK_DEAD)) { const bool alive = icsk->icsk_rto < TCP_RTO_MAX; retry_until = tcp_orphan_retries(sk, alive); do_reset = alive || !retransmits_timed_out(sk, retry_until, 0, 0); if (tcp_out_of_resources(sk, do_reset)) return 1; } } /* * 当重传次数达到建立连接重传上限、超时 * 重传上限或确认连接异常期间重试上限这 * 三种上限之一时,都必须关闭套接字,并 * 且需要报告相应的错误。 判断连接是否超时,要分为3种情况。 1. SYN包:当SYN包的重传次数达到上限时,判定连接超时。(默认允许重传5次,初始超时时间为1s,总共历时31s) 2. 非SYN包,用户使用TCP_USER_TIMEOUT:当数据包发出去后的等待时间超过用户设置的时间时,判定连接超时。 3. 非SYN包,用户没有使用TCP_USER_TIMEOUT:当数据包发出去后的等待时间超过以TCP_RTO_MIN为初始超时时间 ,重传boundary次所花费的时间后,判定连接超时。?如果返回值为真,判定连接超时,则关闭连接,把errno设置为ETIMEDOUT,Socket函数返回-1。 boundary为最大重传次数,timeout为用户设置的超时时间。(通过TCP_USER_TIMEOUT选项设置) 全文地址请点击:https://blog.csdn.net/zhangskd/article/details/35281345?utm_source=copy */ if (retransmits_timed_out(sk, retry_until, syn_set ? 0 : icsk->icsk_user_timeout, syn_set)) { /* Has it gone just too far? */ tcp_write_err(sk); return 1; } return 0; }
/* This function calculates a "timeout" which is equivalent to the timeout of a * TCP connection after "boundary" unsuccessful, exponentially backed-off * retransmissions with an initial RTO of TCP_RTO_MIN or TCP_TIMEOUT_INIT if * syn_set flag is set. */ static bool retransmits_timed_out(struct sock *sk, unsigned int boundary, unsigned int timeout, bool syn_set) { unsigned int linear_backoff_thresh, start_ts; unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN; if (!inet_csk(sk)->icsk_retransmits) return false; /* start_ts为开始计时点,注意是原始包第一次被发送时的时间戳,不是重传包的 */ start_ts = tcp_sk(sk)->retrans_stamp; if (unlikely(!start_ts)) start_ts = tcp_skb_timestamp(tcp_write_queue_head(sk)); /* 包括两种情况: * SYN包:timeout始终设置为0,因为在三次握手时TCP_USER_TIMEOUT是无效的。 * 非SYN包:用户没有使用TCP_USER_TIMEOUT选项时。 */ if (likely(timeout == 0)) { linear_backoff_thresh = ilog2(TCP_RTO_MAX/rto_base); if (boundary <= linear_backoff_thresh) timeout = ((2 << boundary) - 1) * rto_base; else timeout = ((2 << linear_backoff_thresh) - 1) * rto_base + (boundary - linear_backoff_thresh) * TCP_RTO_MAX; } return (tcp_time_stamp - start_ts) >= timeout; }

浙公网安备 33010602011771号