Linux内核提供了可设置的TCP用户超时时长(TCP User Timeout),其控制发送的未确认数据可保持多长时间,之后强制关闭连接。但是,内核不支持RFC5482定义的TCP UTO选项(User Timeout Option),不会将此设置通告给对端,其为本地超时时长。
应用层可通过setsockopt选项TCP_USER_TIMEOUT设置超时时长,内核将其保存在icsk_user_timeout变量中,单位为毫秒。
static int do_tcp_setsockopt(struct sock *sk, int level,
int optname, char __user *optval, unsigned int optlen)
{
switch (optname) {
case TCP_USER_TIMEOUT:
/* Cap the max time in ms TCP will retry or probe the window
* before giving up and aborting (ETIMEDOUT) a connection.
*/
if (val < 0)
err = -EINVAL;
else
icsk->icsk_user_timeout = val;
break;
如下函数tcp_clamp_rto_to_user_timeout,其确保RTO值不超出UTO定义的时长。如果用户未设置UTO,或者当前连接也未进行过报文重传,使用变量icsk_rto中的RTO值,来设置TCP的重传超时定时器。否则,计算重传报文开始到当前经过的时长,之后计算到UTO时长剩余的时长,如果剩余时长小于等于零,取值1,否者,取RTO值和剩余时长两者之间的较小值,作为重传超时定时器的时长。
static u32 tcp_clamp_rto_to_user_timeout(const struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
u32 elapsed, start_ts;
s32 remaining;
start_ts = tcp_retransmit_stamp(sk);
if (!icsk->icsk_user_timeout || !start_ts)
return icsk->icsk_rto;
elapsed = tcp_time_stamp(tcp_sk(sk)) - start_ts;
remaining = icsk->icsk_user_timeout - elapsed;
if (remaining <= 0)
return 1; /* user timeout has passed; fire ASAP */
return min_t(u32, icsk->icsk_rto, msecs_to_jiffies(remaining));
}
函数tcp_retransmit_timer中的调用inet_csk_reset_xmit_timer,使用以上的函数计算要设置的ICSK_TIME_RETRANS定时器的时长。注意最后一个参数为TCP_RTO_MAX(120秒),其定义了超时的最大时间值,UTO的设置值如果过大,将不会生效。
void tcp_retransmit_timer(struct sock *sk)
{
...
inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
tcp_clamp_rto_to_user_timeout(sk), TCP_RTO_MAX);
if (retransmits_timed_out(sk, net->ipv4.sysctl_tcp_retries1 + 1, 0))
__sk_dst_reset(sk);
在报文超时之后,函数tcp_write_timeout负责超时相关处理,判断此连接是否已不可用。
static int tcp_write_timeout(struct sock *sk)
{
...
if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
i...
} else {
...
expired = retransmits_timed_out(sk, retry_until, icsk->icsk_user_timeout);
在函数retransmits_timed_out中判断此链接是否已经超时,如果第三个参数UTO不为零,检测在报文重传之后,是否已经经过了UTO的时长,为真的话,返回1,表示此连接已经超时。否则,在UTO为零时,通过重传次数和退避算法计算超时时长。
static bool retransmits_timed_out(struct sock *sk,
unsigned int boundary, unsigned int timeout)
{
const unsigned int rto_base = TCP_RTO_MIN;
unsigned int linear_backoff_thresh, start_ts;
if (!inet_csk(sk)->icsk_retransmits)
return false;
start_ts = tcp_retransmit_stamp(sk);
if (!start_ts) return false;
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;
timeout = jiffies_to_msecs(timeout);
}
return (s32)(tcp_time_stamp(tcp_sk(sk)) - start_ts - timeout) >= 0;
如下零窗口探测定时器函数tcp_probe_timer,如果用户设置了UTO(变量icsk_user_timeout的值),并且发送队列中还有待发送报文,此报文等待的时长不能超过UTO,否则,认为此连接已经出错。
static void tcp_probe_timer(struct sock *sk)
{
struct inet_connection_sock *icsk = inet_csk(sk);
struct sk_buff *skb = tcp_send_head(sk);
...
start_ts = tcp_skb_timestamp(skb);
if (!start_ts)
skb->skb_mstamp_ns = tp->tcp_clock_cache;
else if (icsk->icsk_user_timeout &&
(s32)(tcp_time_stamp(tp) - start_ts) > icsk->icsk_user_timeout)
goto abort;
...
if (icsk->icsk_probes_out >= max_probes) {
abort: tcp_write_err(sk);
函数tcp_keepalive_timer,如果此连接自从最后一次接收到对端ACK或者数据报文,到当前时刻还没有接收到其它报文,如果时长超出UTO时长,并且本地已经发送过探测报文,还是没有收到相应,则判定此连接已经出错。
在未设置UTO的情况下,此处以发送探测报文的次数为判定连接出错的依据。
static void tcp_keepalive_timer (struct timer_list *t)
{
struct sock *sk = from_timer(sk, t, sk_timer);
...
elapsed = keepalive_time_elapsed(tp);
if (elapsed >= keepalive_time_when(tp)) {
if ((icsk->icsk_user_timeout != 0 &&
elapsed >= msecs_to_jiffies(icsk->icsk_user_timeout) &&
icsk->icsk_probes_out > 0) ||
(icsk->icsk_user_timeout == 0 &&
icsk->icsk_probes_out >= keepalive_probes(tp))) {
tcp_send_active_reset(sk, GFP_ATOMIC);
tcp_write_err(sk);
goto out;
}
内核版本 5.0