tcp keepalive定时器
http server 和client端需要防止“僵死”链接过多!也就是建立了tcp链接,但是没有报文交互, 或者client 由于主机突然掉电!但是server 不知道! 所以需要有一种检测机制,检查tcp连接是否活着在也就是有报文交互!!
也就是检测:对方是否down了
在启用了保活定时器的情况下,如果连接超过空闲时间没有数据交互,则保活定时器超时,向对端发送保活探测包,
若(1)收到回复则说明对端工作正常,重置定时器等下下次达到空闲时间;
(2) 收到其他回复,则确定对端已重启,关闭连接;
(3) 超过探测次数仍未得到回复,则认为对端主机已经崩溃,关闭连接;
case SO_KEEPALIVE://开启 if (sk->sk_protocol == IPPROTO_TCP && sk->sk_type == SOCK_STREAM) tcp_set_keepalive(sk, valbool); sock_valbool_flag(sk, SOCK_KEEPOPEN, valbool); break; case TCP_KEEPIDLE: //keepalive时间, 超过该时间才会开始探测 val = keepalive_time_when(tp) / HZ; break; case TCP_KEEPINTVL://超过keepalive时间后,每次探测的间隔时间 val = keepalive_intvl_when(tp) / HZ; break; case TCP_KEEPCNT://keepalive最大探测次数 val = keepalive_probes(tp); break; static inline int keepalive_time_when(const struct tcp_sock *tp) { struct net *net = sock_net((struct sock *)tp); return tp->keepalive_time ? : net->ipv4.sysctl_tcp_keepalive_time; } static inline int keepalive_probes(const struct tcp_sock *tp) { struct net *net = sock_net((struct sock *)tp); return tp->keepalive_probes ? : net->ipv4.sysctl_tcp_keepalive_probes; } static inline int keepalive_intvl_when(const struct tcp_sock *tp) { struct net *net = sock_net((struct sock *)tp); return tp->keepalive_intvl ? : net->ipv4.sysctl_tcp_keepalive_intvl; } void tcp_set_keepalive(struct sock *sk, int val) { if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN)) return; if (val && !sock_flag(sk, SOCK_KEEPOPEN))////第一次setsockopt enable 但是还没有启动定时器则启动定时器 inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk)));//设置定时器回调函数 else if (!val) inet_csk_delete_keepalive_timer(sk); }
Q1:定时器何时启动??
1、对非listen socket设置SO_KEEPALIVE的时候, 或者已经设置了SO_KEEPALIVE的socket上,设置TCP_KEEPIDLE的时候重置定时器时间
case SO_KEEPALIVE: #ifdef CONFIG_INET if (sk->sk_protocol == IPPROTO_TCP && sk->sk_type == SOCK_STREAM) tcp_set_keepalive(sk, valbool); #endif sock_valbool_flag(sk, SOCK_KEEPOPEN, valbool); break;
void tcp_set_keepalive(struct sock *sk, int val) { if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN)) return;//close和listen状态不需要设置定时器 if (val && !sock_flag(sk, SOCK_KEEPOPEN))//第一次setsockopt inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk))); else if (!val)//删除定时器 inet_csk_delete_keepalive_timer(sk); }
2、客户端收到synack,进入TCP_ESTABLISHED的时候,如果设置了SO_KEEPALIVE;
查看tcp_finish_connect 函数实现可知
static void tcp_keepalive_timer (unsigned long data) { struct sock *sk = (struct sock *) data; struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); u32 elapsed; /* Only process if socket is not in use. */ bh_lock_sock(sk); if (sock_owned_by_user(sk)) { //应用程序在使用该sock则不处理 /* Try again later. */ inet_csk_reset_keepalive_timer (sk, HZ/20); goto out; } if (sk->sk_state == TCP_LISTEN) { pr_err("Hmm... keepalive on a LISTEN ??? "); goto out; } /* 连接释放期间,用作FIN_WAIT2定时器 */ /* * 处理FIN_WAIT_2状态定时器时,TCP状态必须为 * FIN_WAIT_2且套接字状态为DEAD。 */ //tcp_rcv_state_process中收到第一个FIN ack后会进入TCP_FIN_WAIT2状态 if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) { //TCP关闭过程中的定时器处理过程,从tcp_rcv_state_process跳转过来 /* * 停留在FIN_WAIT_2状态的时间大于或等于0的情况下, * 如果FIN_WAIT_2定时器剩余时间大于0,则调用 * tcp_time_wait()继续处理;否则给对端发送RST后 * 关闭套接字。 */ /* TIME_WAIT_2定时器超时触发,如果linger2<0,或者等待时间<=TIMEWAIT_LEN, 直接发送reset关闭连接;如果linger2>=0,且等待时间>TIMEWAIT_LEN, 则进入TIME_WAIT接管; */ if (tp->linger2 >= 0) {/* 停留在FIN_WAIT_2的停留时间>=0 */ const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;/* 获取时间差值 */ if (tmo > 0) { /* 差值>0,等待时间>TIME_WAIT时间,则进入TIME_WAIT状态 */ tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); goto out; } } tcp_send_active_reset(sk, GFP_ATOMIC); goto death; } if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE) goto out; elapsed = keepalive_time_when(tp); /* It is alive without keepalive 8) */ /* * 如果有已输出未确认的段,或者发送队列中还 * 存在未发送的段,则无需作处理,只需重新设 * 定保活定时器的超时时间。 */ /* 1 tp->packets_out判断是否有任何已经传输可是还没有确认的数据包。 * 2 tcp_send_head用来判断是否有将要发送的包 * 如果上面有任何一个条件为真,就说明这个连接并不是处于idle状态,此时我们就重启定 *时器。 */ if (tp->packets_out || tcp_send_head(sk)) goto resched; /* 连接经历的空闲时间,即上次收到报文至今的时间 */ elapsed = keepalive_time_elapsed(tp); /*接下来比较idle时间有没有超过keep alive的设置的间隔时间,如果超过了,则说明我 *们需要 发送探测包了。如果没有,则我们需要重新调整keep alive的超时时间。 */ if (elapsed >= keepalive_time_when(tp)) { /* If the TCP_USER_TIMEOUT option is enabled, use that * to determine when to timeout instead. */ /* * 如果持续空闲时间超过了允许时间,并且在未设置 * 保活探测次数时,已发送保活探测段数超过了系统 * 默认的允许数tcp_keepalive_probes;或者在已设置保活探测 * 段的次数时,已发送次数超过了保活探测次数,则 * 需要断开连接,给对方发送RST段,并报告相应错误, * 关闭相应的传输控制块。 */ if ((icsk->icsk_user_timeout != 0 && elapsed >= 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; } /* 发送保活段,并计算下次激活保活定时器的时间。*/ if (tcp_write_wakeup(sk, LINUX_MIB_TCPKEEPALIVE) <= 0) { icsk->icsk_probes_out++; elapsed = keepalive_intvl_when(tp); } else { /* If keepalive was lost due to local congestion, * try harder. */ elapsed = TCP_RESOURCE_PROBE_INTERVAL; } } else { /* It is tp->rcv_tstamp + keepalive_time_when(tp) */ elapsed = keepalive_time_when(tp) - elapsed; } sk_mem_reclaim(sk); resched: inet_csk_reset_keepalive_timer (sk, elapsed); goto out; death: tcp_done(sk); out: bh_unlock_sock(sk); sock_put(sk); }
//?lrcvtime是最后一次接收到数据报的时间 //rcv_tstamp是最后一次接收到ACK的时间 static inline u32 keepalive_time_elapsed(const struct tcp_sock *tp) { const struct inet_connection_sock *icsk = &tp->inet_conn; return min_t(u32, tcp_time_stamp - icsk->icsk_ack.lrcvtime, tcp_time_stamp - tp->rcv_tstamp); }
/* Initiate keepalive or window probe from timer. */ /* * tcp_write_wakeup()用来输出持续探测段。如果传输 * 控制块处于关闭状态,则直接返回失败,否 * 则传输持续探测段,过程如下: * 1)如果发送队列不为空,则利用那些待发送 * 段来发送探测段,当然这些待发送的段至 * 少有一部分在对方的接收窗口内。 * 2)如果发送队列为空,则构造需要已确认, * 长度为零的段发送给对端。也就是否则最终会发送序号为snd_una-1,长度为0的ack包 * 其返回值如下: * 0: 表示发送持续探测段成功 * 小于0: 表示发送持续探测段失败 * 大于0: 表示由于本地拥塞而导致发送持续探测段失败。 */ int tcp_write_wakeup(struct sock *sk, int mib) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; if (sk->sk_state == TCP_CLOSE) return -1; skb = tcp_send_head(sk); if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) { int err; /* * 如果发送队列中有段需要发送,并且最先 * 待发送的段至少有一部分在对端接收窗口 * 内,那么可以直接利用该待发送的段来发 * 送持续探测段。 */ unsigned int mss = tcp_current_mss(sk); /* * 获取当前的MSS以及待分段的段长。分段得到 * 的新段必须在对方接收窗口内,待分段的段 * 长初始化为SND.UNA-SND_WND-SKB.seq. */ unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq; /* * 如果该段的序号已经大于pushed_seq,则需要 * 更新pushed_seq。 */ if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq)) tp->pushed_seq = TCP_SKB_CB(skb)->end_seq; /* We are probing the opening of a window * but the window size is != 0 * must have been a result SWS avoidance ( sender ) */ /* * 如果待分段段长大于剩余等待发送数据,或者段长度 * 大于当前MSS,则对该段进行分段,分段段长取待分段 * 段长与当前MSS两者中的最小值,以保证只发送出一个 * 段到对方。 */ if (seg_size < TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq || skb->len > mss) { seg_size = min(seg_size, mss); TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; if (tcp_fragment(sk, skb, seg_size, mss, GFP_ATOMIC)) return -1; } else if (!tcp_skb_pcount(skb)) tcp_set_skb_tso_segs(skb, mss); /* * 将探测段发送出去,如果发送成功, * 则更新发送队首等标志。 */ TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC); if (!err) tcp_event_new_data_sent(sk, skb); return err; } else { /* * 如果发送队列为空,则构造并发送一个需要已确认、 * 长度为零的段给对端。如果处于紧急模式,则多发送 * 一个序号为SND.UNA的段给对端。
* Current solution: to send TWO zero-length segments in urgent mode:
* one is with SEG.SEQ=SND.UNA to deliver urgent pointer, another is
* out-of-date with SND.UNA-1 to probe window. */ if (between(tp->snd_up, tp->snd_una + 1, tp->snd_una + 0xFFFF)) tcp_xmit_probe_skb(sk, 1, mib); return tcp_xmit_probe_skb(sk, 0, mib); } }
tcp_keepalive_timer函数为保活定时器和FIN_WAIT_2定时器共用。。。。内核的实现。。复用代码
当TCP主动关闭一端调用了close()来执行连接的完全关闭时会执行以下流程,本端发送FIN给对端,对端回复ACK,本端进入FIN_WAIT_2状态,此时只有对端发送了FIN,本端才会进入TIME_WAIT状态,为了防止对端不发送关闭连接的FIN包给本端,将会在进入FIN_WAIT_2状态时,设置一个FIN_WAIT_2定时器,如果该连接超过一定时限,则进入CLOSE状态;涉及到TCP_LINGER2 选项
上述是针对close调用完全关闭连接的情况,shutdown执行半关闭会不会启动FIN_WAIT_2定时器;-----》应该不会----
/*启动FIN_WAIT_2定时器两个相关逻辑差不多,1、进程调用close系统调用而socekt正处于TCP_FIN_WAIT2状态时 2、孤儿socket进入FIN_WAIT2状态时:从fin1-->fin2
在tcp_close函数中,如果判断状态为FIN_WAIT2,则需要进一步判断linger2配置;
如下所示,在linger2<0的情况下,关闭连接到CLOSE状态,并且发送rst;
在linger2 >= 0的情况下,需判断该值与TIME_WAIT等待时间TCP_TIMEWAIT_LEN值的关系,
如果linger2 > TCP_TIMEWAIT_LEN,则启动FIN_WAIT_2定时器,其超时时间为二者的差值;
如果linger2<0,则直接进入到TIME_WAIT状态,该TIME_WAIT的子状态是FIN_WAIT2,
实际上就是由TIME_WAIT控制块进行了接管,统一交给TIME_WAIT控制块来处理
*/
/* 处于fin_wait2且socket即将关闭,用作FIN_WAIT_2定时器 */
if (sk->sk_state == TCP_FIN_WAIT2) {
struct tcp_sock *tp = tcp_sk(sk);
if (tp->linger2 < 0) { /* linger2小于0,无需等待 */
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, GFP_ATOMIC);/* 发送rst */
__NET_INC_STATS(sock_net(sk),
LINUX_MIB_TCPABORTONLINGER);
} else {
const int tmo = tcp_fin_time(sk); /* 获取FIN_WAIT_2超时时间 */
if (tmo > TCP_TIMEWAIT_LEN) { /* FIN_WAIT_2超时时间> TIME_WAIT时间,加FIN_WAIT_2定时器 */
inet_csk_reset_keepalive_timer(sk,
tmo - TCP_TIMEWAIT_LEN);
} else {/* 小于TIME_WAIT时间,则进入TIME_WAIT */
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
goto out;
}
}
/* 连接释放期间,用作FIN_WAIT2定时器 */
/*
* 处理FIN_WAIT_2状态定时器时,TCP状态必须为
* FIN_WAIT_2且套接字状态为DEAD。
*/ //tcp_rcv_state_process中收到第一个FIN ack后会进入TCP_FIN_WAIT2状态
if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
//TCP关闭过程中的定时器处理过程,从tcp_rcv_state_process跳转过来
/*
* 停留在FIN_WAIT_2状态的时间大于或等于0的情况下,
* 如果FIN_WAIT_2定时器剩余时间大于0,则调用
* tcp_time_wait()继续处理;否则给对端发送RST后
* 关闭套接字。
*/
/*
TIME_WAIT_2定时器超时触发,如果linger2<0,或者等待时间<=TIMEWAIT_LEN,
直接发送reset关闭连接;如果linger2>=0,且等待时间>TIMEWAIT_LEN,
则进入TIME_WAIT接管;
*/
if (tp->linger2 >= 0) {/* 停留在FIN_WAIT_2的停留时间>=0 */
const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;/* 获取时间差值 */
if (tmo > 0) { /* 差值>0,等待时间>TIME_WAIT时间,则进入TIME_WAIT状态 */
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
goto out;
}
}
tcp_send_active_reset(sk, GFP_ATOMIC);
goto death;
}
那么在设置RCV_SHUTDOWN的tcp socket 中 recvmsg 会怎样呢??
查看tcp_recvmsg代码可知:会直接返回0;读不到数据没有发出reset;
if (sk->sk_shutdown & RCV_SHUTDOWN) break;//一个字节都没拷贝到,但如果shutdown关闭了socket,一样直接返回
Q:shutdown执行半关闭会不会启动FIN_WAIT_2定时器;-----应该不会----
int inet_shutdown(struct socket *sock, int how) { /* Hack to wake up other listeners, who can poll for POLLHUP, even on eg. unconnected UDP sockets -- RR */ default: sk->sk_shutdown |= how; if (sk->sk_prot->shutdown) sk->sk_prot->shutdown(sk, how); break; /* Wake up anyone sleeping in poll. */ sk->sk_state_change(sk);//sock_def_wakeup release_sock(sk); return err; }//也就是 设置sk_shut_down掩码调用tcp_port的tcp_shutdown
/*
* Shutdown the sending side of a connection. Much like close except
* that we don't receive shut down or sock_set_flag(sk, SOCK_DEAD).
*/
/*
tcp_shutdown函数完成设置关闭之后的状态,并且发送fin
;注意只有接收端关闭时,不发送fin,只是在recvmsg系统调用中判断状态
,不接收数据;
*/
void tcp_shutdown(struct sock *sk, int how)
{
/* We need to grab some memory, and put together a FIN,
* and then put it into the queue to be sent.
* Tim MacKenzie(tym@dibbler.cs.monash.edu.au) 4 Dec '92.
*/
if (!(how & SEND_SHUTDOWN))
return;
/* 以下这几个状态发fin */
/* If we've already sent a FIN, or it's a closed state, skip this. */
if ((1 << sk->sk_state) &
(TCPF_ESTABLISHED | TCPF_SYN_SENT |
TCPF_SYN_RECV | TCPF_CLOSE_WAIT)) {
/* Clear out any half completed packets. FIN if needed. */
if (tcp_close_state(sk))
tcp_send_fin(sk);
}
}
tcp socket shutdown SEND_SHUTDOWN;----可以看到只是 发送了一个fin;仅仅只是设置 掩码 sk->sk_shutdown |= how;
但是对于调用close --》tcp_close---》sk->sk_shutdown = SHUTDOWN_MASK;一个全部关闭,
同时设置为孤儿socket 状态设置为sock_dead 对应了定时器中需要检测sock_dead状态
/* Detach socket from process context. * Announce socket dead, detach it from wait queue and inode. * Note that parent inode held reference count on this struct sock, * we do not release it in this function, because protocol * probably wants some additional cleanups or even continuing * to work with this socket (TCP). */ static inline void sock_orphan(struct sock *sk) { write_lock_bh(&sk->sk_callback_lock); sock_set_flag(sk, SOCK_DEAD); sk_set_socket(sk, NULL); sk->sk_wq = NULL; write_unlock_bh(&sk->sk_callback_lock); }
SO_LINGER,该选项是socket层面的选项,通过struct linger结构来设置信息,如果启用该选项,那么使用close()和shutdown()(注意:关闭socket,将会等待发送队列中的数据发送完成或者等待超时;
如果不启用该选项,那么调用会立即返回,关闭任务在后台完成;注意:如果是调用exit()函数关闭socket,那么无论是否启用SO_LINGER选项,socket总会在后台执行linger等待
TCP_LINGER2,该选项是TCP层面的,用于设定孤儿套接字在FIN_WAIT2状态的生存时间,该选项可以用来替代系统级别的tcp_fin_timeout配置;
在用于移植的代码中不应该使用该选项;另外,需要注意,不要混淆该选项与socket的SO_LINGER选项;
记住: tcp的fin_time_wait2状态---》可能导致 发出rst 或者进入timewait状态 ---》 TIME_WAIT定时器超时触发,定时器超时,将tw控制块从ehash和bhash中删除,在收到数据段会发送reset;