TCP的定时器系列 — 超时重传定时器
主要内容:TCP定时器概述,超时重传定时器、ER延迟定时器、PTO定时器的实现。
内核版本:3.15.2
我的博客:http://blog.csdn.net/zhangskd
Q:一条TCP连接会使用多少个定时器呢?
A:目前的答案是9个:
超时重传定时器,持续定时器,ER延迟定时器,PTO定时器,ACK延迟定时器,
SYNACK定时器,保活定时器,FIN_WAIT2定时器,TIME_WAIT定时器。
数据结构
几种定时器的标识:
#define ICSK_TIME_RETRANS 1 /* Retransmit timer */
#define ICSK_TIME_DACK 2 /* Delayed ack timer */
#define ICSK_TIME_PROBE0 3 /* Zero window probe timer */
#define ICSK_TIME_EARLY_RETRANS 4 /* Early retransmit timer */
#define ICSK_TIME_LOSS_PROBE 5 /* Tail loss probe timer */
上述5种定时器分别为:超时重传定时器、ACK延迟定时器、持续定时器、ER延迟定时器、PTO定时器。
另外还有保活定时器、FIN_WAIT2定时器、TIME_WAIT定时器、SYNACK定时器。
虽然定义了9个定时器,但是内核中只用了4个实例(timer_list),所以有些定时器是共用一个实例的。
这4个实例分别是:
icsk->icsk_retransmit_timer:超时重传定时器、持续定时器、ER延迟定时器、PTO定时器。
icsk->icsk_delack_timer:ACK延迟定时器。
sk->sk_timer:保活定时器,SYNACK定时器,FIN_WAIT2定时器。
death_row->tw_timer:TIME_WAIT定时器。
创建和删除
(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()
void tcp_init_xmit_timers(struct sock *sk)
{
inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer, &tcp_keepalive_timer);
}
/*
* Using different timers for retransmit, delayed acks and probes.
* We may wish use just one timer maintaining a list of expire jiffies to optimize.
*/ void inet_csk_init_xmit_timers(struct sock *sk,
void (*retransmit_handler) (unsigned long),
void (*delack_handler) (unsigned long),
void (*keepalive_handler) (unsigned long))
{
struct inet_connection_sock *icsk = inet_csk(sk);
setup_timer(&icsk->icsk_retransmit_timer, retransmit_handler, (unsigned long)sk);
setup_timer(&icsk->icsk_delack_timer, delack_timer, (unsigned long)sk);
setup_timer(&sk->sk_timer, keepalive_handler, (unsigned long)sk);
icsk->icsk_pending = icsk->icsk_ack.pending = 0;
}
(2) 定时器的删除
tcp_done
tcp_disconnect
tcp_v4_destroy_sock
|-> tcp_clear_xmit_timers
|-> inet_csk_clear_xmit_timers
void inet_csk_clear_xmit_timers(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
icsk->icsk_pending = icsk->icsk_ack.pending = icsk->icsk_ack.blocked = 0;
sk_stop_timer(sk, &icsk->icsk_retransmit_timer);
sk_stop_timer(sk, &icsk->icsk_delack_timer);
sk_stop_timer(sk, &sk->sk_timer);
}
激活
icsk->icsk_retransmit_timer和icsk->icsk_delack_timer的激活函数为inet_csk_reset_xmit_timer(),
共负责了5个定时器的激活工作。
/*
* Reset the retransmissiion timer
*/
static inline void inet_csk_reset_xmit_timer(struct sock *sk, const int what,
unsigned long when,
const unsigned long max_when)
{
struct inet_connection_sock *icsk = inet_csk(sk); if (when > max_when) {
#ifdef INET_CSK_DEBUG
pr_debug("reset_xmit_timer: sk=%p %d when=0x%lx, caller=%p\n",
sk, what, when, current_text_addr());
#endif
when = max_when;
}
if (what == ICSK_TIME_RETRANS || what == ICSK_TIME_PROBE0 ||
what == ICSK_TIME_EARLY_RETRANS || what == ICSK_TIME_LOSS_PROBE) {
icsk->icsk_pending = what;
icsk->icsk_timeout = jiffies + when; /*数据包超时时刻*/
sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);
} else if (what == ICSK_TIME_DACK) {
icsk->icsk_ack.pending |= ICSK_ACK_TIMER;
icsk->icsk_ack.timeout = jiffies + when; /*Delay ACK定时器超时时刻*/
sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
}
#ifdef INET_CSK_DEBUG
else {
pr_debug("%s", inet_csk_timer_bug_msg);
}
#endif
}
其中,超时重传定时器(ICSK_TIME_RETRANS)在以下几种情况下会被激活:
1. 发现对端把保存在接收缓冲区的SACK段丢弃时。
2. 发送一个数据段时,发现之前网络中不存在发送且未确认的段。
之后每当收到确认了新数据段的ACK,则重置定时器。
3. 发送SYN包后。
4. 一些特殊情况。
超时处理函数
当icsk->icsk_retransmit_timer超时后,会调用其处理函数tcp_write_timer()进行处理。
staic void tcp_write_timer(unsigned long data)
{
struct sock *sk = (struct sock *)data;
bh_lock_sock(sk); if (! sock_owned_by_user(sk)) { /* sk没被用户空间占用 */
tcp_write_timer_handler(sk); } else { /* 否则先设置延迟标志,之后再处理 */
/* delegate our work to tcp_release_cb() */
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定时器,
所以需要判断是哪种定时器触发的,然后采取相应的处理措施。
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_retransmit_timer()进行处理。
void tcp_retransmit_timer(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk); /* Fast Open特性相关,暂时不做分析 */
if (tp->fastopen_rsk) {
...
} /* 如果没有发送且未确认的数据段,没有等待哪来的超时,直接返回 */
if (! tp->packets_out)
goto out; /* 如果发送队列为空,显然不合理 */
WARN_ON(tcp_write_queue_empty(sk)); /* 发生RTO超时表明数据包的确丢失了,所以不用再进行额外检测了 */
tp->tlp_high_seq = 0; /* 如果对端通告窗口为0,sk不处于DEAD状态,TCP连接不处于三次握手 */
/* 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.
*/ if (! tp->snd_wnd && ! sock_flag(sk, SOCK_DEAD) &&
! ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV))) { struct inet_sock *inet = inet_sk(sk);
if (sk->sk_family == AF_INET) {
LIMIT_NETDEBUG(KERN_DEBUG pr_fmt("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)
/* IPv6的处理,此处省略 */
...
#endif /* 距离上次收到ACK的时间超过了最大超时时间,认为有错误发生了*/
if (tcp_time_stamp - tp->rcv_tstamp > TCP_RTO_MAX) {
tcp_write_err(sk); /* 报告错误,调用tcp_done终止连接 */
goto out;
} /* 进入Loss状态,标志丢失的数据段 */
tcp_enter_loss(sk, 0); /* 重传发送队列的第一个数据段 */
tcp_retransmit_skb(sk, tcp_write_queue_head(sk)); __sk_dst_reset(sk); /* 更新路由缓存 */
goto out_reset_timer;
} /* 如果重传次数达到上限,报告错误并关闭套接口。
* 如果资源使用达到上限,放弃本次重传。
*/
if (tcp_write_timeout(sk))
goto out; /* 刚进入超时重传 */
if (icsk->icsk_retransmits == 0) {
int mib_idx; /* Recovery状态中发生超时 */
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) { /* Loss状态中发生超时 */
mib_idx = LINUX_MIB_TCPLOSSFAILURES; } else if (icsk->icsk_ca_state == TCP_CA_Disorder) || tp->sacked_out) {
/* Disorder状态中发生超时 */
if (tcp_is_sack(tp))
mib_idx = LINUX_MIB_TCPSACKFAILURES;
else
mib_idx = LINUX_MIB_TCPRENOFAILURES; } else { /* Open状态或CWR状态中发生超时 */
mib_idx = LINUX_MIB_TCPTIMEOUTS;
} NET_INC_STATS_BH(sock_net(sk), mib_idx);
} /* 进入Loss状态,标志丢失的数据段。
* 值得注意的是不再使用tcp_enter_frto_loss(),FRTO机制被重写了。
*/
tcp_enter_loss(sk, 0); /* 重传发送队列的第一个数据段,如果失败说明是本地拥塞,那么不进行指数退避 */
if (tcp_retransmit_skb(sk, tcp_write_queue_head(sk)) > 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;
} icsk->icsk_backoff++; /* 退避指数,采集到新的RTT样本时清零 */
icsk->icsk_retransmits++; /* 超时次数,当确认了新数据时清零 */ out_reset_timer:
/* 对于符合条件的Thin stream不使用指数退避,重复超时6次后除外(太惨了:) */
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); /* RTO保持原样 */ } else { /* Use normal (exponential) backoff */
icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX); /* RTO翻倍 */
} /* 重置超时定时器,就是上文说的激活 */
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX); /* 当重传多次时,需要根据重传时间间隔,决定是否更新路由缓存 */
if (retransmits_timed_out(sk, sysctl_tcp_retries1 + 1, 0, 0))
__sk_dst_reset(sk); out:;
}
连接的超时
很明显,重传不能无限的进行下去,当重传的次数超过设定的上限时,就会判定连接超时,关闭该连接。
此后相应的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);
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); /* 更新目的路由缓存 */ /* SYN携带Fast Open选项,或SYN携带数据 */
if (tp->syn_fastopen || tp->syn_data)
tcp_fastopen_cache_set(sk, 0, NULL, true);
if (tp->syn_data)
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPFASTOPENACTIVEFAIL);
} /* SYN的最大重传次数,由TCP_SYNCNT选项和tcp_syn_retries参数决定,默认是5次 */
retry_until = icsk->icsk_syn_retries ?: sysctl_tcp_syn_retries;
syn_set = true; } else {
/* tcp_retries1默认为3,当重传次数超过此值时,表示可能遇到了黑洞,需要进行PMTU
* 检测,同时更新路由缓存。
*/
if (retransmits_timed_out(sk, sysctl_tcp_retries1, 0, 0)) {
/* Black hole detection */
tcp_mtu_probing(icsk, sk); /* PMTU检测 */
dst_negative_advic(sk); /* 更新路由缓存 */
} retry_until = sysctl_tcp_retries2; /* 在断开TCP连接之前,最多进行多少次重传,默认值为15 */ /* 如果当前套接口即将关闭 */
if (sock_flag(sk, SOCK_DEAD)) {
const int 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); /* 如果当前的孤儿socket数量超过tcp_max_orphans,或者内存不够时,关闭此连接 */
if (tcp_out_of_resources(sk, do_reset))
return 1;
}
} /* 判定连接是否等待过久,即是否超时 */
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;
}
} /* Calculate maximal number or retries on an orphaned socket. */
static int tcp_orphan_retries (struct sock *sk, int alive)
{
int retries = sysctl_tcp_orphan_retries; /* May be zero. */ /* We know from an ICMP that something is wrong. */
if (sk->sk_err_soft && !alive)
retries = 0; /* However, if socket sent something recently, select some safe number of retries.
* 8 corresponds to > 100 seconds with minimal RTO of 200msec.
*/
if (retries == 0 && alive)
retries = 8; return retries;
}
判断连接是否超时,要分为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选项设置)
struct tcp_sock {
...
/* Timestamp of the last retransmit, also used in SYN-SENT to remember stamp of
* the first SYN.
*/ /* 上面的注释是错误的,变量的含义为:
* 1. 原始SYN包的发送时间点,不是重传的。
* 2. 本次重传时,第一个被重传的包,原始包的发送时间点。
* 这个变量是用来计算连接的超时关闭时间:一个包发送多久还没有得到响应,就判断连接超时。
*/
u32 retrans_stamp;
...
}; /* 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; /* 如果是SYN包,则默认的初始超时时间为1s,否则为200ms。*/
unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN; if (! inet_csk(sk)->icsk_retransmits))
return false; /* 如果之前没有重传过,直接返回false */ /* start_ts为开始计时点,注意是原始包第一次被发送时的时间戳,不是重传包的 */
if (unlikely(! tcp_sk(sk)->retrans_stamp))
start_ts = TCP_SKB_CB(tcp_write_queue_head(sk))->when;
else
start_ts = tcp_sk(sk)->retrans_stamp; /* 包括两种情况:
* SYN包:timeout始终设置为0,因为在三次握手时TCP_USER_TIMEOUT是无效的。
* 非SYN包:用户没有使用TCP_USER_TIMEOUT选项时。
*/
if (likely(timeout == 0)) {
/* 当rto不超过最大值时,能够进行多少次指数退避。
* 以SYN包为例,rto_base为1s,此值向上取整为7。
*/
linear_backoff_thresh = ilog2(TCP_RTO_MAX/rto_base); /* 从计时点开始,经过boundary次重传后,总共花费的时间 */
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; /* 判断等待时间是否过长,即是否要放弃连接 */
}
TCP的定时器系列 — 超时重传定时器的更多相关文章
- TCP的定时器系列 — 零窗口探测定时器
主要内容:零窗口探测定时器的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 出现以下情况时,TCP接收方的接收缓冲区将被塞满数据: 发送方的发送速 ...
- 计算TCP链接的RTO超时重传时间
- TCP的定时器系列 — 保活定时器
主要内容:保活定时器的实现,TCP_USER_TIMEOUT选项的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 原理 HTTP有Keepaliv ...
- TCP系列16—重传—6、基础快速重传(Fast Retransmit)
一.快速重传介绍 按照TCP协议,RTO超时重传是一个非常重要的事件,当RTO超时的时候,TCP会同时通过两种方式非常谨慎的降低发送数据包的速率,一种是基于拥塞控制削减发送窗口的大小,另外一个是通过指 ...
- TCP的核心系列 — 重传队列的更新和时延的采样(二)
在tcp_clean_rtx_queue()中,并非对每个ACK都进行时延采样.是否进行时延采样,跟这个ACK是否为 重复的ACK.这个ACK是否确认了重传包,以及是否使用时间戳选项都有关系. 本文主 ...
- TCP的核心系列 — 重传队列的更新和时延的采样(一)
重传队列实际上就是发送队列(sk->sk_write_queue),保存着发送且未确认的数据段. 当有新的数据段被确认时,需要把这些段从重传队列中删除,同时更新一些变量,包括 packets_o ...
- TCP系列17—重传—7、SACK下的重传
我们之前介绍SACK选项的时候说过,SACK可以把接收端系列号空间的洞反映给发送端,因此发送端可以更充分的理解接收端的情况,而进行更好的重传恢复过程.这种过程有时候也叫做advanced loss r ...
- TCP系列13—重传—3、协议中RTO计算和RTO定时器维护
从上一篇示例中我们可以看到在TCP中有一个重要的过程就是决定何时进行超时重传,也就是RTO的计算更新.由于网络状况可能会受到路由变化.网络负载等因素的影响,因此RTO也必须跟随网络状况动态更新.如果T ...
- TCP定时器 之 重传定时器
注:这部分还没有完全分析透彻,先在此记录,后面回顾的时候再进行补充: 启动定时器: (1) 之前发送的数据段已经得到确认,新发出一个数据段之后设定: (2) 新建连接发送syn之后设定: (3) PM ...
随机推荐
- Tomcat和JDK的内存配置
1.jvm内存管理机制: 1)堆(Heap)和非堆(Non-heap)内存 按照官方的说法:"Java 虚拟机具有一个堆,堆是运行时数据区域,所有类实例和数组的内存均从此处分配.堆是在 Ja ...
- Linux下安装oracle的一般步骤
1.配置内核参数2.创建用户和用户组3.创建安装目录4.配置oracle用户环境5.安装数据库软件6.创建数据库7.配置监听
- django之数据库orm
一.数据库的配置 1 django默认支持sqlite,mysql, oracle,postgresql数据库. <1>sqlite django默认使用sqlite的数据库,默认自带sq ...
- JVM初探- 内存分配、GC原理与垃圾收集器
JVM初探- 内存分配.GC原理与垃圾收集器 标签 : JVM JVM内存的分配与回收大致可分为如下4个步骤: 何时分配 -> 怎样分配 -> 何时回收 -> 怎样回收. 除了在概念 ...
- GDAL C#读取shp中文属性值乱码问题
GDAL的C#版本读取shp中,如果属性值中含有中文,读出来有可能是乱码的问题,根据SWIG生成的C#代码调试发现问题所在,在Ogr.cs文件中有这么一个函数,代码如下: internal stati ...
- python复杂网络库networkx:算法
http://blog.csdn.net/pipisorry/article/details/54020333 Networks算法Algorithms 最短路径Shortest Paths shor ...
- [OpenCV]在显示窗口中截图
[OpenCV]在显示窗口中截图 简介 介绍使用OpenCV实现简单的截图功能.首先阐述实现此功能的基本步骤,然后给出实现代码,最后贴出实验结果以及遇到的问题. 基本步骤 我们需要知道OpenCV使用 ...
- 一个环形公路,上面有N个站点,A1, ..., AN,其中Ai和Ai+1之间的距离为Di,AN和A1之间的距离为D0。 高效的求第i和第j个站点之间的距离,空间复杂度不超过O(N)。
//点数 #define N 10 //点间距 int D[N]; //A1到每个Ai的距离 int A1ToX[N]; void preprocess() { srand(time(0)); //随 ...
- defaultdict的威力
>>> from collections import defaultdict >>> s='mmississippi' >>> d=defaul ...
- PHP和MySQL Web开发学习笔记介绍
前言 从2016年2月1日开始,之后的几个月左右的时间里,我会写一个系列的PHP和MySQL Web开发的学习笔记.我之前一直从事Java语言的开发工作,最近这段时间非常想学习一门语言,就选择了PHP ...