TCP连接的终止----主动关闭

  在正常情况下,TCP连接的关闭需要连接的两端进行四次分组交换,具体过程是:执行主动关闭的一端(A端)会首先发送FIN包给对端(B端),B端收到FIN包后会发送一个ACK包给A段;B段执行关闭操作,发送FIN给A端,A端发送一个ACK给B端,连接彻底关闭。分组交换和状态迁移如下图所示:

  

  通常情况下,只有执行主动关闭的一端会进入TIME_WAIT状态,还有一种情况会导致连接的两端都进入TIME_WAIT状态。当TCP的两端同时给对端发送FIN包,两端的TCP状态均从ESTABLISHED变为FIN_WAIT_1,在FIN_WAIT_1状态下接收到FIN包后,状态会由FIN_WAIT_1迁移到CLOSING,并发送最后的ACK。收到最后的ACK后,状态变迁为TIME_WAIT状态,如下图所示:


  上述的两种情况只是TCP连接关闭情况的一部分,在其他情况下,内核可能给对端发送的不是FIN包,而是RST包。在这种情况下,有可能是应用层序的问题,也可能是内核资源短缺造成的,如何来查找和确定这些异常情况的原因,需要对内核的实现有一个比较全面的了解。

  在应用层要关闭一个连接非常简单,只需要指定要关闭的连接对应的套接字即可。内核中处理TCP连接关闭的系统调用是sys_close(),该函数做的事情不多,主要的关闭操作是由tcp_close()函数来完成的。tcp_close()函数的定义中比sys_close()多了一个timeout参数,不难看出这个timeout肯定是一个超时时间,第一次看到这个参数也是比较疑惑。在调用close()的时候并没有指定超时参数,那这个timeout的值怎么来的呢?这个值是在tcp_close()的上层函数inet_release()中计算出来的,其计算方式如下所示:

int inet_release(struct socket *sock)
{
         ......
		timeout = 0;
		if (sock_flag(sk, SOCK_LINGER) &&
		    !(current->flags & PF_EXITING))
			timeout = sk->sk_lingertime;
        ......
}
  我们看到timeout的值和SOCK_LINGER选项有关,这个选项可以通过setsockopt()来设置,该选项表示在关闭连接时需要等待的时间,设置的事件值保存在sk_lingertime中,这个值可以为负值。这个选项通常不会设置,所以在我们的讨论上不关注这个选选项,认为timeout的值为0.

  接下来我们从tcp_close()开始,看内核中如何来执行应用层发出的关闭连接请求。

  tcp_close()中首先调用lock_sock()来获取访问sock实例的互斥锁,获取锁后将sk_shutdown设置为SHUTDOWN_MASK。sk_shutdown可以设置的值有RCV_SHUTDOWN(值为1)、SEND_SHUTDOWN(值为2)、SHUTDOWN_MASK(值为3),分别代表关闭接收通道、关闭发送通道、完全关闭,所以这里是要同时关闭发送和接收通道。接下来是检查套接的状态是否是LISTEN状态,我们这里要看的ESTABLISHED状态下套接字的关闭,所以这个部分直接跳过。

  如果此时接收队列中不为空,即接收到的数据没有被上层读取,这时需要将接受队列中的SKB包全都释放掉,释放的数据长度存储在局部变量data_was_unread(它的初始值为0)中,如下所示:

        while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {
		u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq -
			  tcp_hdr(skb)->fin;
		data_was_unread += len;
		__kfree_skb(skb);
	}
  局部变量data_was_unread中存储的值并不是内核关心的,内核关心只是是不是有数据未被读取。如果套接字的接收队列中有数据未被读取,这时内核发送的不是FIN包给对端,而是RST,如下所示:

    if (data_was_unread) {
		/* Unread data was tossed, zap the connection. */
		NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
		tcp_set_state(sk, TCP_CLOSE);
		tcp_send_active_reset(sk, sk->sk_allocation);
	}
  在发送RST包给对端前会调用tcp_set_state()将套接字的状态设置为TCP_CLOSE,这时没有TIME_WAIT状态,没有FIN_WAIT_1状态。所以在编写应用程序时,在关闭连接前,一定要保证所有接收到的数据被读取,否则连接会不正常关闭。

  假设套接字的接收队列为空,接着会调用tcp_close_state()函数,这个函数根据sock实例的当前状态找出执行关闭时下一个对应的状态,其返回值要么为0,要么为TCP_ACTION_FIN。返回值为TCP_ACTION_FIN时,表示要发送FIN包;为0时,表示不需要发送FIN包。如果连接是在ESTABLISHED状态执行关闭操作,其对应的下一个状态为FIN_WAIT_1,tcp_close_state()的返回值为TCP_ACTION_FIN,所以接下来会调用tcp_send_fin()给对端发送FIN包。在tcp_send_fin()中,如果sock实例的发送队列不为空,则将FIN标志添加到发送队列中最后一个要发送的SKB包中;如果发送队列为空,则会分配一个新的SKB包,添加上FIN标志后加入到发送队列中。FIN标志处理后,内核会调用__tcp_push_pending_frames()将发送队列的skb包发送出去。tcp_send_fin()的代码如下:

/* Send a fin.  The caller locks the socket for us.  This cannot be
 * allowed to fail queueing a FIN frame under any circumstances.
 */
void tcp_send_fin(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb = tcp_write_queue_tail(sk);
	int mss_now;

	/* Optimization, tack on the FIN if we have a queue of
	 * unsent frames.  But be careful about outgoing SACKS
	 * and IP options.
	 */
	mss_now = tcp_current_mss(sk);

	if (tcp_send_head(sk) != NULL) {
		TCP_SKB_CB(skb)->flags |= TCPCB_FLAG_FIN;
		TCP_SKB_CB(skb)->end_seq++;
		tp->write_seq++;
	} else {
		/* Socket is locked, keep trying until memory is available. */
		for (;;) {
			skb = alloc_skb_fclone(MAX_TCP_HEADER,
					       sk->sk_allocation);
			if (skb)
				break;
			yield();
		}

		/* Reserve space for headers and prepare control bits. */
		skb_reserve(skb, MAX_TCP_HEADER);
		/* FIN eats a sequence byte, write_seq advanced by tcp_queue_skb(). */
		tcp_init_nondata_skb(skb, tp->write_seq,
				     TCPCB_FLAG_ACK | TCPCB_FLAG_FIN);
		tcp_queue_skb(sk, skb);
	}

	__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_OFF);
}
  在tcp_send_fin()中分配用于发送FIN的SKB包时,如果分配失败,内存会一直循环,知道分配成功。不太理解内核为什么为了发送一个FIN包,而这么的“执着”,知道的朋友可以说一下,拜谢!

  在发送完FIN包后,会将sock实例当前的状态存储在局部变量state中,增加对sock实例的引用,然后调用sock_orphan()将sock实例从socket结构中分离,并且将套接字标志设置为SOCK_DEAD,表示套接字即将关闭。在分离的过程中,有一项是分离等待队列,内核只是简单地将sk_sleep成员设置为NULL。这就有一个疑惑,sk_sleep是一个指针,直接设置为NULL,指向的内存怎么释放?等待的进程怎么处理?其实sk_sleep成员指向的是socket实例的wait成员,所以等待队列的释放会在释放socket结构时释放。sk_sleep的值的设置是在sock_init_data()中进行的。接着调用release_sock()释放sock实例后备队列上的SKB包,如果此时后备队列上的数据包不是刚好确认到FIN所在的序列好,在tcp_rcv_state_process()中处理时也有可能发送RST给对端,这个和tcp_close()中对接收队列的处理类似。这个过程可以在后面看FIN_WAIT_1状态下接收到ACK时的处理时会看到。此时,sock实例要等待释放,所以此时待销毁的sock实例个数加1。

  为了完整的看到TCP连接的整个过程,在我们的讨论中认为在tcp_close()中处理后sock的状态为FIN_WAIT_1,在这个假设前提下,我们会进入到下面这段代码的处理中:

    if (sk->sk_state != TCP_CLOSE) {
		int orphan_count = percpu_counter_read_positive(
						sk->sk_prot->orphan_count);

		sk_mem_reclaim(sk);
		if (tcp_too_many_orphans(sk, orphan_count)) {
			if (net_ratelimit())
				printk(KERN_INFO "TCP: too many of orphaned "
				       "sockets\n");
			tcp_set_state(sk, TCP_CLOSE);
			tcp_send_active_reset(sk, GFP_ATOMIC);
			NET_INC_STATS_BH(sock_net(sk),
					LINUX_MIB_TCPABORTONMEMORY);
		}
	}
如果tcp_too_many_orphans()返回true时,这里我们看到内核会发送RST给对端。在以下两种情况下tcp_too_many_orphans()会返回true:

  1、当前待销毁的套接字数量大于系统配置sysctl_tcp_max_orphans变量

   2、如果当前sock实例发送队列中所有报文数据的总长度大于SOCK_MIN_SNDBUF(值为2048),并且TCP层分配内存的状态处于pressure状态

  到这里,tcp_close()的处理基本上结束了,在上面我们已经说过,认为处理后sock实例处于FIN_WAIT_1状态。

  在FIN_WAIT_1状态下,接收到SKB包时会在tcp_rcv_state_process()函数中处理,函数中涉及FIN_WAIT_1状态的代码如下所示:

/*
 *	This function implements the receiving procedure of RFC 793 for
 *	all states except ESTABLISHED and TIME_WAIT.
 *	It's called from both tcp_v4_rcv and tcp_v6_rcv and should be
 *	address independent.
 */
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
			  struct tcphdr *th, unsigned len)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	int queued = 0;
	int res;

	tp->rx_opt.saw_tstamp = 0;
	
	......

	res = tcp_validate_incoming(sk, skb, th, 0);
	if (res <= 0)
		return -res;

	/* step 5: check the ACK field */
	if (th->ack) {
		int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH) > 0;

		switch (sk->sk_state) {
		......

		case TCP_FIN_WAIT1:
			if (tp->snd_una == tp->write_seq) {
				tcp_set_state(sk, TCP_FIN_WAIT2);
				sk->sk_shutdown |= SEND_SHUTDOWN;
				dst_confirm(sk->sk_dst_cache);

				if (!sock_flag(sk, SOCK_DEAD))
					/* Wake up lingering close() */
					sk->sk_state_change(sk);
				else {
					int tmo;
					if (tp->linger2 < 0 ||
					    (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
					     after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt))) {
						tcp_done(sk);
						NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
						return 1;
					}

					tmo = tcp_fin_time(sk);
					if (tmo > TCP_TIMEWAIT_LEN) {
						inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
					} else if (th->fin || sock_owned_by_user(sk)) {
						/* Bad case. We could lose such FIN otherwise.
						 * It is not a big problem, but it looks confusing
						 * and not so rare event. We still can lose it now,
						 * if it spins in bh_lock_sock(), but it is really
						 * marginal case.
						 */
						inet_csk_reset_keepalive_timer(sk, tmo);
					} else {
						tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
						goto discard;
					}
				}
			}
			break;
			
			......
		}
	} else
		goto discard;

	/* step 6: check the URG bit */
	tcp_urg(sk, skb, th);

	/* step 7: process the segment text */
	switch (sk->sk_state) {
	......
	
	case TCP_FIN_WAIT1:
	case TCP_FIN_WAIT2:
		/* RFC 793 says to queue data in these states,
		 * RFC 1122 says we MUST send a reset.
		 * BSD 4.4 also does reset.
		 */
		if (sk->sk_shutdown & RCV_SHUTDOWN) {
			if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
			    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
				NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
				tcp_reset(sk);
				return 1;
			}
		}
		/* Fall through */
	case TCP_ESTABLISHED:
		tcp_data_queue(sk, skb);
		queued = 1;
		break;
	}

	/* tcp_data could move socket to TIME-WAIT */
	if (sk->sk_state != TCP_CLOSE) {
		tcp_data_snd_check(sk);
		tcp_ack_snd_check(sk);
	}

	if (!queued) {
discard:
		__kfree_skb(skb);
	}
	return 0;
}
  如果数据包中带ACK标志,则会调用tcp_ack()函数来处理对端的确认,处理后会跳转到第30行处的FIN_WAIT_1分支继续处理,这个处理是否进行取决于第31行的判断,如下所示:

        if (tp->snd_una == tp->write_seq) {
  其中tp->snd_una存储的是输出的段中最早一个未确认段的序号,tp->write_seq存储的加入到发送队列中最后一个字节的序号加1,如果这两个成员相等,则表示所有发出去的段都已确认。我们知道在tcp_close()中最后加入到发送队列的是FIN标志(可能是重新创建一个包,或者是加入到已有包中),所以说明在发送完FIN后,接收到了对端的确认,此时可以从FIN_WAIT_1状态迁移到FIN_WAIT_2状态,状态的更改是通过调用tcp_set_state()函数来完成的。接下来根据内核中设置要求套接字保持在FIN_WAIT_2状态的时间来选择是通过定时器等待对端发送FIN包,还是立即迁移到TIME_WAIT状态。如果是第二种的话,虽然此时状态是TIME_WAIT状态,但还有一个子状态的概念,目的就是要区分当前是FIN_WAIT_2状态还是真正的TIME_WAIT状态,这样做的话会使用专门描述TIME_WAIT状态的sock结构,这样会节省内存,还会提升系统的性能。因为大多数传输控制块主动关闭时都会经历TIME_WAIT状态,如果每个控制块都使用一个定时器来处理,在系统中存在很多短连接的情况下(比如Web服务器),2MSL等待超时时间确实准确了,但由于使用了很多定时器,会严重影响系统的性能。如果TIME_WAIT的等待时间小于2MSL,会将sock结构添加到twcal_row散列表中,有一个定时器会定时清理这个散列表上的timewait控制块。
  接着我们跳转到第80行的FIN_WAIT_1分支的处理。在这里内核会检查sk_shutdown中是否设置了关闭接收通道,在tcp_close()中我们看到设置的是SHUTDOWN_MASK,所以这个的判断是成立的。我们来看这个判断:

if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt))
第一个条件在SKB的TCP层数据不为空时为true,第二个条件是在SKB包中的数据除了FIN外,还有数据时为true,简单来说就是在SKB包中除了FIN标志外,还有数据,此时这个判断为真,接下来就要对sock结构执行reset操作,并且返回1。如果返回1,在调用tcp_rcv_state_process()的上层函数tcp_v4_do_rcv()中会发送RST给对端,关键代码如下所示:

/* The socket must have it's spinlock held when we get
 * here.
 *
 * We have a potential double-lock case here, so even when
 * doing backlog processing we use the BH locking scheme.
 * This is because we cannot sleep with the original spinlock
 * held.
 */
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
    ......
    
	if (tcp_rcv_state_process(sk, skb, tcp_hdr(skb), skb->len)) {
		rsk = sk;
		goto reset;
	}
	TCP_CHECK_TIMER(sk);
	return 0;

reset:
	tcp_v4_send_reset(rsk, skb);
discard:
	kfree_skb(skb);
	/* Be careful here. If this function gets more complicated and
	 * gcc suffers from register pressure on the x86, sk (in %ebx)
	 * might be destroyed here. This current version compiles correctly,
	 * but you have been warned.
	 */
	return 0;
	
	......
}
  如果此时的数据包中恰好是对端发送过来的FIN包,那这个FIN会在第96行调用的tcp_data_queue()中处理。在tcp_data_queue()中会,会首先检查这个包是否是一个合法的,并且是可以接收的SKB包。通过检查后如果skb包中包含FIN标志,则调用tcp_fin()来处理,我们来看tcp_fin()中如何处理这个FIN包,相关的代码如下所示:

/*
 * 	Process the FIN bit. This now behaves as it is supposed to work
 *	and the FIN takes effect when it is validly part of sequence
 *	space. Not before when we get holes.
 *
 *	If we are ESTABLISHED, a received fin moves us to CLOSE-WAIT
 *	(and thence onto LAST-ACK and finally, CLOSE, we never enter
 *	TIME-WAIT)
 *
 *	If we are in FINWAIT-1, a received FIN indicates simultaneous
 *	close and we go into CLOSING (and later onto TIME-WAIT)
 *
 *	If we are in FINWAIT-2, a received FIN moves us to TIME-WAIT.
 */
static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
{
	struct tcp_sock *tp = tcp_sk(sk);

	sk->sk_shutdown |= RCV_SHUTDOWN;
	sock_set_flag(sk, SOCK_DONE);

	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;
	
	......
	}

	/* It _is_ possible, that we have something out-of-order _after_ FIN.
	 * Probably, we should reset in this case. For now drop them.
	 */
	__skb_queue_purge(&tp->out_of_order_queue);
	if (tcp_is_sack(tp))
		tcp_sack_reset(&tp->rx_opt);
	sk_mem_reclaim(sk);

	......
}
  在这里有一个地方需要注意下,在接收到FIN后,会调用tcp_set_flag()设置sock的flag为SOCK_DONE,与tcp_close()中设置的SOCK_DEAD是不一样的,注意这里的标志发生了变化,只是提醒下,后面的处理不作过多的说明。接下来就和明显了,调用tcp_send_ack()给对端发送ACK,表示FIN包已收到,接着调用tcp_time_wait()将套接字状态迁移到TIME_WAIT状态了。

  至此,TCP连接终于关闭了。

  当然后续在TIME_WAIT状态下接收到对端的数据包也会做一些处理,这些处理不是本文关注的了,后面会写一篇关于TIME_WAIT状态下的内核处理的文章。

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值