注:本文分析基于3.10.107内核版本
简介
超时重传定时器是TCP连接可靠性的重要保证,其工作原理为TCP连接在发送某一个数据报文或者SYN报文后,该连接就会启动一个定时器,在规定时间内如果没有收到对端回复的ACK报文,那么定时器超时处理函数就重新发送数据,直到发送成功或者达到最大重传次数而上报错误为止。
定时器类型
一个TCP连接有多个定时器来保证TCP连接的可靠性及其传输效率。在include/net/inet_connection_sock.h中定义了以下几个定时器
#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 */
上面五个定时器分别为:超时重传定时器、ACK延迟定时器、零窗口探测定时器、ER延迟定时器、尾部丢失探测定时器。
另外还有保活定时器、FIN_WAIT2定时器、TIME_WAIT定时器、SYNACK定时器。
但是在内核中并没有为每个定时器提供一个实例(timer_list),有些定时器共用一个实例,目前内核里有4个实例:
icsk->icsk_retransmit_timer:超时重传定时器、零窗口探测定时器、ER延迟定时器、尾部丢失探测定时器。
icsk->icsk_delack_timer:ACK延迟定时器。
sk->sk_timer:保活定时器,SYNACK定时器,FIN_WAIT2定时器。
death_row->tw_timer:TIME_WAIT定时器。
定时器的创建
这里我们主要关注超时重传定时器的创建流程,如下:
tcp_v4_init_sock
—->tcp_init_sock
——–>tcp_init_xmit_timers
————>inet_csk_init_xmit_timers
在连接初始化时,除了会创建超时重传定时器,ACK延迟定时器和保活定时器也会被一起创建。
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_handler,
(unsigned long)sk);
setup_timer(&sk->sk_timer, keepalive_handler, (unsigned long)sk);
icsk->icsk_pending = icsk->icsk_ack.pending = 0;
}
定时器的激活
超时重传定时器的激活有很多种场景,目前个人有接触主要有以下两种:
1、TCP建链发送SYN报文时;
2、发送数据时,每收到对端的ACK报文,也会reset定时器。
我们从发送SYN报文流程里看:
/* Build a SYN and send it off. */
int tcp_connect(struct sock *sk)
{
...
/* 这里是初始化传输控制块中与连接相关的成员
其中就包括对重传超时时间RTO的初始化,对于
SYN报文时1s,不同内核版本该值会不一样,在
内核版本3.0.101(SUSE11SP3)上该值是3s。
*/
tcp_connect_init(sk);
...
/* Timer for repeating the SYN until an answer.
这里便是激活超时重传定时器了,其中TCP_RTO_MAX的值为120s。
*/
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
return 0;
}
inet_csk_reset_xmit_timer函数里不仅负责对超时重传定时器进行重置,还负责其他4个定时器的激活。
/*
* Reset the retransmission 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) {
when = max_when;
}
/* 重传定时器,零窗口探测定时器,ER定时器,PTO定时器以及迟定ACK定时器都通过该接口激活 */
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;
sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
}
}
超时处理函数
从上面定时器创建流程中我们知道,超时重传定时器的超时处理函数是tcp_write_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() */
if (!test_and_set_bit(TCP_WRITE_TIMER_DEFERRED, &tcp_sk(sk)->tsq_flags))
sock_hold(sk);
}
bh_unlock_sock(sk);
sock_put(sk);
}
tcp_write_timer_handler()函数中根据触发的定时器类型,采取对应处理,如果是超时重传定时器会进入tcp_retransmit_timer()函数里处理。
/*
* The 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);
if (tp->fastopen_rsk) {//fast open,让TCP三次握手过程也能交换数据,没太研究,略过
WARN_ON_ONCE(sk->sk_state != TCP_SYN_RECV &&
sk->sk_state != TCP_FIN_WAIT1);
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;
}
//packets_out表示发送出去尚未收到对端确认的数据包,如果没有,超时就没意义,返回
if (!tp->packets_out)
goto out;
WARN_ON(tcp_write_queue_empty(sk));
tp->tlp_high_seq = 0;
//收到对端窗口为0,sk状态不为SOCK_DEAD,同时也不处于TCP三次握手期间,也就是在established状态下出现了拥塞
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);
}
// 距离上次接收到ACK的时间超过TCP_RTO_MAX,120s,就认为有错误发生了
if (tcp_time_stamp - tp->rcv_tstamp > TCP_RTO_MAX) {
tcp_write_err(sk);//最终通过ICMP报文上报错误到应用层
goto out;
}
//报文丢失,要进行拥塞处理和标识丢失数据段
tcp_enter_loss(sk, 0);
/* 重传发送队列里的第一个数据段 */
tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
__sk_dst_reset(sk);//刷路由
goto out_reset_timer;
}
// TCP三次握手期间的定时器超时会走到下面的流程,判断连接是否超时
if (tcp_write_timeout(sk))
goto out;
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_BH(sock_net(sk), mib_idx);
}
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++;//增加指数退避次数
icsk->icsk_retransmits++;//增加重传次数
out_reset_timer:
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 {
/* 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);//重置定时器
//当距离原始包发送6.2s后还未收到应答包时,就要刷新路由
if (retransmits_timed_out(sk, sysctl_tcp_retries1 + 1, 0, 0))
__sk_dst_reset(sk);//刷路由
out:;
}
重传到一定次数后就该返回超时了,在tcp_write_timeout里判断连接是否超时。
/* 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);
int retry_until;
bool do_reset, syn_set = false;
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {//TCP三次握手期间
if (icsk->icsk_retransmits)
dst_negative_advice(sk);//之前重传过,那就要更新路由缓存了
// 获取重传次数,对于SYN报文系统默认5次,即内核参数sysctl_tcp_syn_retries,
// 用户可以通过setsockopt的TCP_SYNCNT选项设置该参数,也可以通过修改/proc里面的内核参数
retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries;
syn_set = true;//设置syn标识
} else {
// tcp_retries1默认为3,当数据报文重传次数超过此值时,也就是距离原始包发送3s后还未收到ACK报文
// 表示可能遇到了黑洞,需要进行PMTU检测,并刷新路由
if (retransmits_timed_out(sk, sysctl_tcp_retries1, 0, 0)) {
/* Black hole detection */
tcp_mtu_probing(icsk, sk);
dst_negative_advice(sk);
}
retry_until = sysctl_tcp_retries2;//数据报文重传次数默认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);
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);//最终通过ICMP报文上报错误
return 1;
}
return 0;
}
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;
//在tcp_connect()函数中会对retrans_stamp进行赋值,因此超时时间计算使用的是原始包发送的时间戳来计算
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,那timeout也为0
if (likely(timeout == 0)) {
//对于SYN包,rto_base为1s,可求得最大指数退避次数为log2(120),向上取整为7次
//SYN包的boundary为5次
linear_backoff_thresh = ilog2(TCP_RTO_MAX/rto_base);
if (boundary <= linear_backoff_thresh)
//正常情况都是按这个计算超时时间,对于SYN报文超时时间是63s
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;
}
也就是说,对于SYN报文,需要重传5次,历时63s才能最终返回超时。
这个可以通过一个实验来证实。在A、B两台服务器上,A向B发起连接,B上把防火墙默认规则设置为DROP,然后用tcpdump抓包,就可以看到A一共向B发送了6次SYN报文,然后A发起的连接才返回timeout。
参考资料:
1、http://blog.youkuaiyun.com/zhangskd/article/details/35281345