这篇文章是对 《TCP连接的终止----主动关闭》的补充,重点关注的是被动关闭连接一端的状态迁移及内核中的 处理,被动连接的状态迁移可以参见 《TCP连接的终止----主动关闭》中的状态迁移图,这里就不再画了。
被动连接的关闭是从接收到FIN开始的,如果TCP是处于ESTABLISHED状态(我们的讨论假设连接处于此状态),这个FIN包会在tcp_rcv_established()中处理。在tcp_rcv_state_process()中的处理分为快速路径和慢速路径。如果TCP首部中第4个32位字除去保留的bit位和预测标志一致,skb包的序列号和sock结构下一个要接收到序号相等,并且skb包中的确认序列号是有效的,此时skb包会在快速路径中处理。判断时sock实例的预测标志的位图分布通常如下图所示:
其中S对应于TCP首部(struct tcphdr)中的doff成员(tcp首部的长度,以4字节为单位),?通常为0,为1的bit位对应的是ACK标志,snd_wnd则是本端发送窗口的大小。
从上面的预测标志的分布来看,如果设置了FIN标志的话,则检查预测标志时失败,所以会在慢速路径中处理FIN包。
在慢速路径处理中,会首先检查skb包的校验和及包是否是有效的,检查通过后会调用tcp_ack()处理ack的情况,接下来的处理中真正和FIN相关的操作是在调用的tcp_data_queue()中处理的,这个处理是通过调用tcp_fin()函数来处理的,从最初的TCP层接收函数tcp_v4_rcv()到tcp_fin()的处理的代码流程图如下所示:
tcp_fin()中首先调用inet_csk_schedule_ack()设置相关成员,表明需要发送ACK;因为接收了对端发送的FIN包,表明对端已经不会再发送数据包(可以发送ACK),因此关闭接收通道;还要修改sock结构的标志,表示连接将要结束;然后根据sock实例的状态会跳转到不同的分支进行处理,如果是ESTABLISHED状态下,内核会调用tcp_set_state()修改sock实例的状态,并且设置延迟发送ACK的标志,如下所示:
static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th) { struct tcp_sock *tp = tcp_sk(sk); inet_csk_schedule_ack(sk); sk->sk_shutdown |= RCV_SHUTDOWN; sock_set_flag(sk, SOCK_DONE); switch (sk->sk_state) { case TCP_SYN_RECV: case TCP_ESTABLISHED: /* Move to CLOSE_WAIT */ tcp_set_state(sk, TCP_CLOSE_WAIT); inet_csk(sk)->icsk_ack.pingpong = 1; break; ...... } }
tcp_fin()后面的清理乱序队列、状态更改时可能要唤醒相关进程这些操作不是我们关心的,就不作过多说明了。
在tcp_fin()中虽然设置了发送ACK的相关标志,但是要有一个引发ACK发送的操作,或者是给内核发送ACK的一个提示。这个操作是在上层函数tcp_rcv_established()函数中进行的,通过间接调用__tcp_ack_snd_check()中完成。__tcp_ack_snd_check()中会判断当前发送ACK是要立即发送还是延迟发送,如果立即发送则调用tcp_send_ack()来发送ACK,否则调用tcp_send_delayed_ack()延迟发送,代码如下所示:
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible) { struct tcp_sock *tp = tcp_sk(sk); /* More than one full frame received... */ if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss /* ... and right edge of window advances far enough. * (tcp_recvmsg() will send ACK otherwise). Or... */ && __tcp_select_window(sk) >= tp->rcv_wnd) || /* We ACK each frame or... */ tcp_in_quickack_mode(sk) || /* We have out of order data. */ (ofo_possible && skb_peek(&tp->out_of_order_queue))) { /* Then ack it now */ tcp_send_ack(sk); } else { /* Else, send delayed ack. */ tcp_send_delayed_ack(sk); } }
判断的条件是这样的,只要满足以下条件就会立即发送ACK:
1、接收窗口中有多个全尺寸段还未确认
2、当前处于快速确认模式下
3、在启用判断乱序队列的情况下,乱序队列中存在段
这三个条件中,我们可以确定的是第2个条件,我们首先来看判断是否处于快速确认模式的函数tcp_in_quickack_mod()函数的实现,如下所示:
static inline int tcp_in_quickack_mode(const struct sock *sk) { const struct inet_connection_sock *icsk = inet_csk(sk); return icsk->icsk_ack.quick && !icsk->icsk_ack.pingpong; }
在tcp_fin()中如果是ESTABLISHED状态下接收到FIN,会设置pingpong的值为1,所以可以肯定此时不处于快速确认模式下,至于是否要立即发送ACK取决于另外两个判断条件了。
我们接下来的讨论是在从对接收到的FIN确认后开始的。这时TCP连接的关闭已经进行一半了,接下来就是等待本端的上层应用调用close()来执行本端的关闭连接操作,这个操作我们在 《TCP连接的终止----主动关闭》中讲到过,是由tcp_close()来完成的。所以我们还是来看tcp_close(),只是这次sock实例的状态不一样,这时的状态应该为CLOSE_WAIT。
在tcp_close()中我们这次只关注一些和状态相关的一些处理,其他的队列清理、内存回收等就不再介绍了,所以我们只关注下面这部分代码:
void tcp_close(struct sock *sk, long timeout) { struct sk_buff *skb; int data_was_unread = 0; int state; ...... if (data_was_unread) { ...... } else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) { ...... } else if (tcp_close_state(sk)) { tcp_send_fin(sk); } ...... }
在tcp_close_state()中sock实例的状态会由CLOSE_WAIT迁移到LAST_ACK状态,返回值为TCP_ACTION_FIN,表示要发送FIN。因此,在第三个if判断中条件为true,所以会调用tcp_send_fin()给对端发送FIN。
当本端接收到TCP连接关闭的最后一个ACK时,由tcp_rcv_state_process()函数来处理,相关的代码如下所示:
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; ...... /* step 5: check the ACK field */ if (th->ack) { int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH) > 0; switch (sk->sk_state) { ...... case TCP_LAST_ACK: if (tp->snd_una == tp->write_seq) { tcp_update_metrics(sk); tcp_done(sk); goto discard; } break; } } else goto discard; ...... switch (sk->sk_state) { case TCP_CLOSE_WAIT: case TCP_CLOSING: case TCP_LAST_ACK: if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) break; 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包,则会在第19-23行代码中处理,调用tcp_done()将套接字状态设置为TCP_CLOSE,并且调用inet_csk_destroy_sock()释放sock实例占用的资源,并且调用sock_put()释放传输控制块(真正的调用sk_free()一般情况下不会是这里,但是这里较少引用计数后,上层再调用sock_put()时就会触发sk_free()操作)。我们知道在主动关闭一端正常情况下会通过定时器来释放描述TIME_WAIT状态的sock结构或者放在twcal_row队列中等待释放,这些释放方式比较明显。还有一个种就是这里看到的通过inet_csk_destroy_sock()来间接完成释放。
有时也可能接收到其他包,如果是包含数据的包,在44-51的处理中会发送RST给对端,如果只是单纯的ACK包,但是确认的序列号不对,则会在tcp_data_queue()中释放掉。
至此,TCP连接被动关闭一方的处理完成了。