网络协议栈(16)套接口异步关闭及重传
一、TCP的异步发送
在之前的日志中,已经看到tcp报文的发送时异步的,也就是应用层的send函数向一个TCP套接口发送数据,当send函数返回之后,send中的发送数据可能还没有到达网卡,更不要说到达对方并获得对方的ACK。这样做的好处是数据的准备和发送可以异步进行,和文件的缓冲一样,当write函数返回之后,fsync之前,谁也不能保证写入的内容已经冲刷到了硬盘上;对应地,当send返回的时候,写入的报文可能还没有注入到网络中。
对于一些简单的单进程串行任务,此时send的快速返回有利于应用层把更多的时间放在逻辑的处理以及报文的接受处理等更为消耗CPU的操作上。
同样是作为类比,我们看一下send是否提供了一个类似于fsync机制的功能呢,或者说只有等待到send的内容得到了对方的确认之后再返回?随便在google上搜索了一下,底层是没有办法完成的,即使一个报文得到了确认,此时也只能说这个发送的报文得到了对方的内核的接收,而app应用层可能依然还没有接收到这个报文。没有接收到的原因可能是app侧甚至根本就没有在执行read操作(搜索的结果帖子位于http://stackoverflow.com/questions/8218785/how-can-i-explicitly-wait-for-a-tcp-ack-before-proceeding,这里不得不叹服google搜索功能的强大,一般搜索结果的第一项就是你想要的结果)。
既然send是异步的,就意味着send返回之后,套接口的发送缓冲区中可能已经累计和非常多的报文没有发送或者没有得到对方的确认。对于一些负载比较重的服务器程序,当一个请求到来之后,服务器可能在获得了请求的应答(如一个http页面)内容之后,send返回给客户端,然后执行close操作,一个交互就此结束。
二、拥挤的发送内容处理
按照常理,当执行close的时候,此时应该是一个同步操作,这样才能够将之前欠下的发送请求归还。但是此时还是和文件操作做一个简单的类比,在文件close的时候,同样数据没有写回到永久存储介质上,所以当一个套接口close的时候同样可以认为数据没有注入网络。这样做从逻辑上说的依据是:当主动执行close之后,此时说明发送方已经不会再发送内容,并且它也不再需要从网络中获得新的数据,所以对逻辑层来说,它的逻辑功能已经完成,这个套接口的历史使命已经结束,所以可以快速释放这个资源。
三、内核中的处理
当执行一个socket的close函数之后,此时执行的操作为内核的
inet_release
int inet_release(struct socket *sock)
{
struct sock *sk = sock->sk;
if (sk) {
long timeout;
/* Applications forget to leave groups before exiting */
ip_mc_drop_socket(sk);
/* If linger is set, we don't return until the close
* is complete. Otherwise we return immediately. The
* actually closing is done the same either way.
*
* If the close is due to the process exiting, we never
* linger..
*/
timeout = 0;
if (sock_flag(sk, SOCK_LINGER) &&
!(current->flags & PF_EXITING))
timeout = sk->sk_lingertime;
sock->sk = NULL;
sk->sk_prot->close(sk, timeout);
}
return 0;
}
这里可以看到有一个SOCK_LINGER选项将会对文件关闭时拥挤报文的发送返回,注释中也说到:如果lingger时间已经设置,那么等待lingertime中设定的时间,当报文内容全部发送之后返回,或者超时返回。这里的SOCK_LINGER标志位通过 setsockopt中的 SO_LINGER:来设置。该超时时间对于tcp来说就是位于
void tcp_close(struct sock *sk, long timeout)--->>>sk_stream_wait_close(sk, timeout)
void sk_stream_wait_close(struct sock *sk, long timeout)
{
if (timeout) {
DEFINE_WAIT(wait);
do {
prepare_to_wait(sk->sk_sleep, &wait,
TASK_INTERRUPTIBLE);
if (sk_wait_event(sk, &timeout, !sk_stream_closing(sk)))
break;
} while (!signal_pending(current) && timeout);
finish_wait(sk->sk_sleep, &wait);
}
}
static inline int sk_stream_closing(struct sock *sk)
{
return (1 << sk->sk_state) &
(TCPF_FIN_WAIT1 | TCPF_CLOSING | TCPF_LAST_ACK);
}
在sk_stream_wait_close函数中,其中是一个简单而典型的等待操作,其中超时的最长等待时间就是linger中设置的等待时间,但是如果说在这个时间之内报文发送完毕的话,此时同样可以返回,因为之后还有一个对于套接口状态的判断。对于简单的交互来说,当服务器把内容全部发送之后,伴随着之前的close将会向对方发送fin,此时对方在处理完这些数据之后就会给予确认,此时套接口将会满足上面sk_stream_closing中的一个状态而在等待时间没有到达时返回。
四、如果linger时间为零
对于linger时间为零的端口
void tcp_close(struct sock *sk, long timeout)
if (data_was_unread) {
/* Unread data was tossed, zap the connection. */
NET_INC_STATS_USER(LINUX_MIB_TCPABORTONCLOSE);
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, GFP_KERNEL);
} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {如果开启了linger选项,并且linger的时间为零,那么进入该分支,也就是会执行tcp_disconnect函数。
/* Check zero linger _after_ checking for unread data. */
sk->sk_prot->disconnect(sk, 0);
NET_INC_STATS_USER(LINUX_MIB_TCPABORTONDATA);
} else if (tcp_close_state(sk)) {
……
tcp_send_fin(sk);
}
tcp的断开操作
int tcp_disconnect(struct sock *sk, int flags)
{
struct inet_sock *inet = inet_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
int err = 0;
int old_state = sk->sk_state;
if (old_state != TCP_CLOSE)
tcp_set_state(sk, TCP_CLOSE);
……
tcp_clear_xmit_timers(sk);
__skb_queue_purge(&sk->sk_receive_queue);
sk_stream_writequeue_purge(sk);
__skb_queue_purge(&tp->out_of_order_queue);
……
sk->sk_error_report(sk);
return err;
}
purge的操作也非常简单粗暴,只是简单的从接收队列中读取到每个报文,然后将这些报文逐个释放掉,回收缓冲区空间,这个操作的现象和意义就是所有推迟操作都不再生效,未发送出去的报文被直接丢弃,对于一些send和close时序上非常紧凑并且系统网卡复杂较重、或者报文数据量较大的应用来说,这可能会导致大量的数据丢失。
static inline void sk_stream_writequeue_purge(struct sock *sk)
{
struct sk_buff *skb;
while ((skb = __skb_dequeue(&sk->sk_write_queue)) != NULL)
sk_stream_free_skb(sk, skb);
sk_stream_mem_reclaim(sk);
}
五、FIN_WAIT1、FIN_WAIT2、TIME_WAIT
在tcp_close的时候,执行的状态转换图由一个数组标志
static const unsigned char new_state[16] = {
/* current state: new state: action: */
/* (Invalid) */ TCP_CLOSE,
/* TCP_ESTABLISHED */ TCP_FIN_WAIT1 | TCP_ACTION_FIN,
/* TCP_SYN_SENT */ TCP_CLOSE,
/* TCP_SYN_RECV */ TCP_FIN_WAIT1 | TCP_ACTION_FIN,
/* TCP_FIN_WAIT1 */ TCP_FIN_WAIT1,
/* TCP_FIN_WAIT2 */ TCP_FIN_WAIT2,
/* TCP_TIME_WAIT */ TCP_CLOSE,
/* TCP_CLOSE */ TCP_CLOSE,
/* TCP_CLOSE_WAIT */ TCP_LAST_ACK | TCP_ACTION_FIN,
/* TCP_LAST_ACK */ TCP_LAST_ACK,
/* TCP_LISTEN */ TCP_CLOSE,
/* TCP_CLOSING */ TCP_CLOSING,
};
其实SYN_RECV状态下对方可能已经完成了三次握手进入establish状态,所以此时进入FIN_WAIT1并发送分手报文FIN。在FIN_WAIT1状态下,此时发送方的发送缓冲中可能有也可能没有待发送内容,但是在从上面两个状态转换为FIN_WAIT1的时候给对方发送了一个FIN报文,这个报文对方是一定需要根据协议进行确认的,而从FIN1到FIN2的转换也就在对于网络报文的接收中完成了。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
……
case TCP_FIN_WAIT1:
if (tp->snd_una == tp->write_seq) {所有的写出数据都已经得到了确认则进入FIN2状态。
tcp_set_state(sk, TCP_FIN_WAIT2);
sk->sk_shutdown |= SEND_SHUTDOWN;
……
}
进入FIN2状态,则意味着FIN的一个步骤已经完成,因为握手时三次,而分手则是四次,四次是两个事物,分为两个FIN+ACK事务,当进入FIN2时,表示一个分手事务已经完成,此时等待对方的FIN发送过来,其实此时对方是出于close_wait状态,即等待应用层调用close来关闭本次连接。
当对方发送分手的FIN并且己方确认之后就进入到time_wait状态,此时的通讯已经无法再确认,所以可能重传过来的FIN报文可能误伤新创建的套接口内容,所以此时最后一个FIN的接收方要等待一定时间,来接收网络中可能重传的FIN报文,这个时间推荐是两个RTT。
从FIN2到time_wait的转换位于
static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
switch (sk->sk_state) {
……
case TCP_FIN_WAIT2:
/* Received a FIN -- send ACK and enter TIME_WAIT. */
tcp_send_ack(sk);
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
break;
也即当FIN2时受到对方的分手报文则进入到TIME_WAIT,从前面的new_state数组中也可以看到,对方在发送了fin之后同时进入last_ack状态。