tcp连接关闭时,被动关闭一方,tcp连接终止过程
本文是对《TCP连接的终止----主动关闭》的补充,重点介绍被动封闭连接一端的状态迁移和内核中的处理。被动连接的状态迁移可以参考《TCP连接的终止----主动关闭》中的状态迁移图,这里就不画了。
被动连接在收到FIN时关闭。如果TCP处于ESTABLISHED状态(我们的讨论假设连接处于这种状态),这个FIN包将在tcp_rcv_established()中进行处理。tcp_rcv_state_process()中的处理分为快速路径和慢速路径。如果TCP报头中的第四个32位字与除保留位之外的预测标志一致,skb包的序列号等于sock结构中下一个要接收的序列号,并且skb包中的确认序列号有效,则skb包将在快速路径中处理。判断sock实例预测标记的位图分布时,通常如下图所示:
其中s对应tcp报头中的doff成员(TCP报头的长度为4个字节),通常0和1的比特对应ACK标志,snd_wnd是本地发送窗口的大小。
从上面预测标志的分布来看,如果设置了FIN标志,那么预测标志的检查将失败,因此FIN包将在慢速路径中处理。
在慢速路径处理中,将首先检查skb包的校验和以及该包是否有效。检查通过后,将调用tcp_ack()来处理ack。在接下来的处理中,真正与FIN相关的操作是在被调用的tcp_data_queue()中处理的,它是通过调用tcp_fin()函数和接收来自初始tcp层的函数tcp_v4_rcv()来处理的。
在tcp_fin()中,首先调用inet_csk_schedule_ack()设置相关成员,表示需要发送ack;因为收到对端发送的FIN包,表示对端不再发送数据包(可以发送ACK),所以接收通道关闭;还要修改sock结构的标志,以表示连接将要结束;然后会根据sock实例的状态跳转到不同的分支进行处理。如果建立,内核将调用tcp_set_state()修改sock实例的状态,并设置延迟发送ACK的标志,如下所示:
静态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 _ shut down =RCV _关机;
sock_set_flag(sk,SOCK _ DONE);
开关(sk- sk_state) {
案例TCP_SYN_RECV:
案例TCP _已建立:
/*移至CLOSE_WAIT */
tcp_set_state(sk,TCP _ CLOSE _ WAIT);
inet _ csk(sk)-icsk _ ack . ping pong=1;
打破;
.
}
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 _ DELLED _ ACK()延迟发送。代码如下:
静态void _ _ TCP _ ack _ snd _ check(struct sock * sk,int ofo_possible)
struct TCP _ sock * TP=TCP _ sk(sk);
/*接收到一个以上的完整帧.*/
if((TP-rcv _ NXT-TP-rcv _ wup)inet _ csk(sk)-icsk _ ack . rcv _ MSS
/* .并且窗口的右边缘前进得足够远。
*(否则tcp_recvmsg()将发送ACK)。或者.
_ _ TCP _ select _ window(sk)=TP-rcv _ wnd)
/*我们确认每个帧,或者.*/
tcp_in_quickack_mode(sk)
/*我们有无序的数据。*/
(ofo _ possible skb _ peek(TP-out _ of _ order _ queue))){
/*那现在就确认*/
TCP _ send _ ack(sk);
}否则{
/*否则,发送延迟ack。*/
TCP _ send _ delayed _ ack(sk);
}
判断条件是,只要满足以下条件,就会立即发送ACK:
1.接收窗口中有许多未确认的全尺寸段。
2.目前处于快速确认模式。
3.当无序队列被启用时,无序队列中有段。
这三个条件中,我们能确定的是第二个条件。先来看tcp_in_quickack_mod()函数的实现,这个函数是判断是否处于快速确认模式的函数,如下图:
静态内联int TCP _ in _ quick ack _ mode(const struct sock * sk)
const struct inet _ connection _ sock * icsk=inet _ csk(sk);
return icsk- icsk_ack.quick!icsk-icsk _ ack . ping pong;
}
在tcp_FIN()中,如果FIN是在ESTABLISHED状态下接收的,那么pingpong的值会被设置为1,所以可以确定此时不处于快速确认模式。是否立即发送ACK取决于另外两个判断条件。
我们的下一个讨论是在确认收到的鳍之后开始的。此时TCP连接的关闭已经进行到一半,下一步就是等待本地终端的上层应用调用close()来执行本地终端的关闭操作。这个操作在《TCP连接的终止----主动关闭》中提到过,是由tcp_close()完成的。所以我们还是看tcp_close(),只是这次sock实例的状态不同,此时的状态应该是CLOSE_WAIT。
在tcp_close()中,我们这次只关注一些状态相关的处理,其他的队列清理,内存回收等。就不介绍了,所以我们只关注下面部分代码:
void tcp_close(struct sock *sk,长超时)
struct sk _ buff * skb
int data _ was _ unread=0;
int状态;
.
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判断条件为真,那么会调用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,无符号len)
struct TCP _ sock * TP=TCP _ sk(sk);
struct inet _ connection _ sock * icsk=inet _ csk(sk);
int queued=0;
int res
.
/*步骤5:检查ACK字段*/
if (th- ack) {
int acceptable=tcp_ack(sk,skb,FLAG_SLOWPATH)
开关(sk- sk_state) {
.
案例TCP_LAST_ACK:
if (tp- snd_una==tp- write_seq) {
TCP _ update _ metrics(sk);
TCP _ done(sk);
转到丢弃;
打破;
}否则
转到丢弃;
.
开关(sk- sk_state) {
案例TCP_CLOSE_WAIT:
案例TCP_CLOSING:
案例TCP_LAST_ACK:
如果(!之前(TCP_SKB_CB(skb)- seq,tp- rcv_nxt))
打破;
案例TCP_FIN_WAIT1:
案例TCP_FIN_WAIT2:
/* RFC 793要求在这些状态下对数据进行排队,
* RFC 1122说我们必须发送一个复位。
* BSD 4.4也会重置。
if(sk-sk _ shut down RCV _关机){
if (TCP_SKB_CB(skb)- end_seq!=TCP_SKB_CB(skb)- seq
(TCP_SKB_CB(skb)- end_seq - th- fin,tp- rcv_nxt)之后){
NET_INC_STATS_BH(sock_net(sk),LINUX _ MIB _ TCPABORTONDATA);
TCP _ reset(sk);
返回1;
/*失败*/
案例TCP _已建立:
tcp_data_queue(sk,skb);
排队=1;
打破;
/* tcp_data可以将套接字移动到TIME-WAIT */
if (sk- sk_state!=TCP_CLOSE) {
TCP _ data _ snd _ check(sk);
TCP _ ack _ snd _ check(sk);
如果(!排队){
丢弃:
_ _ kfree _ skb(skb);
返回0;
如果它只是预期的ACK包,它将在第19-23行被处理。调用tcp_done()将socket状态设置为TCP_CLOSE,调用inet_csk_destroy_sock()释放sock实例占用的资源,调用sock_put()释放传输控制块(真正的调用sk_free()通常不在这里)。我们知道一般情况下,描述TIME_WAIT状态的sock结构会被timer释放或者放入twcal_row队列等待释放。这些释放方法是显而易见的。还有一种,就是通过inet_csk_destroy_sock()间接完成释放。
有时可能会收到其他数据包。如果它是一个包含数据的数据包,RST将在44-51过程中被发送给对等体。如果是简单的ACK包,但是确认的序列号是错误的,那么会在tcp_data_queue()中释放。
至此,TCP连接被动关闭方的处理完成。