网络协议栈(17)对端套接口关闭后的SIGPIPE信号
一、tcp关闭
tcp的关闭在实际应用中的重要性可能会高于通常教科书中描述的三次握手。在三次握手发生时,此时的语义和动作都是确定的,server在侦听,而client去主动连接,此时连着的角色在连接开始之前就已经明确。对于TCP的断开来说,此时整个协议没有办法确定到底是谁来先断开,任何一方都可以在任何时候断开连接,这个连接可能是主动的、也可能是被动的(例如网络链路不通、系统崩溃),此时的断开对于双方来说要做到的是尽量优雅的断开连接,让此次的断开对于双方和网络来说都是一个可控的良好行为。
二、shutdown操作
当一次连接关闭时,总有一方会主动执行关闭操作,这个操作可能是由shutdown来完成,也有可能有close完成。但是事实上shutdown并没有完成socket资源的释放,而只是修改了链路的逻辑状态;放过来说,close在多进程中只是说自己已经完成了对链路的使用,这个资源可以被系统回收了。这种说法还是有些绕,所以我们只能看代码,至于代码如何应用,那就要看场景了。
1、shutdown rcv
①、主动方读取数据时
一个连接可以通过shutdown来执行读关闭或者是写关闭。当执行读关闭的时候,此时表示应用层(如果一个套接口在多个进程中共享,则所有进程可见)从该套接口中读取数据时返回值为零,也就是无法在读取数据出来。
sys_shutdown--->>>inet_shutdown--->>>tcp_shutdown-->>
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;
/* 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层不会向对方发送任何数据,而只是在上层的inet_shutdown函数中设置了套接口的状态
int inet_shutdown(struct socket *sock, int how)
default:
sk->sk_shutdown |= how;
if (sk->sk_prot->shutdown)
sk->sk_prot->shutdown(sk, how);
break;
当应用层通过该套接口读取数据时,此时的读取操作为
int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t len, int nonblock, int flags, int *addr_len)
{
int copied = 0;
err = -ENOTCONN;
……
if (copied) {
if (sk->sk_err ||
sk->sk_state == TCP_CLOSE ||
(sk->sk_shutdown & RCV_SHUTDOWN) ||
!timeo ||
signal_pending(current) ||
(flags & MSG_PEEK))
break;
} else {
if (sock_flag(sk, SOCK_DONE))
break;
if (sk->sk_err) {
copied = sock_error(sk);
break;
}
if (sk->sk_shutdown & RCV_SHUTDOWN)
break;
……
此时可以看到,当执行读取操作时返回值始终为零,并且没有错误码(即用户态通过errno返回值依然为零)。
②、主动方接收报文时
当该套接口接收到数据时
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
struct tcphdr *th, unsigned len)
……
/* step 7: process the segment text */
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(LINUX_MIB_TCPABORTONDATA);
tcp_reset(sk);
return 1;
}
}
/* Fall through */
如果是对方执行了写操作关闭,或者自己执行了读操作关闭,如果再有报文发送过来,此时向对方发送reset报文,该reset报文的处理将会在后面继续说明。
2、shutdown write
①、主动方写入时
此时执行tcp_shutdown中的后继操作
/* 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_ESTABLISHED转换为TCP_FIN_WAIT1状态,并向对方发送fin报文,表示自己不再发送报文。此时要通知对方,因为对方可能此时正在执行read等待,此时一方不再发送报文告诉对方自己不再发送,那么对方可能会一直阻塞在这个没有意义的read接口中。
②、对方读取时
当对端接收到fin信号之后调用tcp_fin函数处理,
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;
收到fin的一方会设置自己不再接收,这也意味着当上层再次执行read的时候返回值为零。
而对于写操作关闭的主动方,当它执行套接字写操作时,此时执行
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t size)
err = -EPIPE;
if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
goto do_error;
……
do_error:
if (copied)
goto out;
out_err:
err = sk_stream_error(sk, flags, err);
TCP_CHECK_TIMER(sk);
release_sock(sk);
return err;
}
int sk_stream_error(struct sock *sk, int flags, int err)
{
if (err == -EPIPE)
err = sock_error(sk) ? : -EPIPE;
if (err == -EPIPE && !(flags & MSG_NOSIGNAL))
send_sig(SIGPIPE, current, 0);
return err;
}
此时应用程序会收到SIGPIPE信号。
三、close执行
close是一个可能执行的资源释放操作,资源的释放此时已经开始超越协议层了。此时的底层的基础设施即将关闭,上层的所有操作都应该结束,系统不会为了连接的优雅关闭而不去释放这个资源。
tcp_close
else if (tcp_close_state(sk)) {
……
tcp_send_fin(sk);
}
通常一个已经建立的连接在close之后进入fin_wat1,当收到这个fin的确认报文之后进入fin_wait2,并且套接口转换为time_wait套接口。
time_wait对于报文的处理
tcp_v4_rcv--->>tcp_timewait_state_process
if (tw->tw_substate == TCP_FIN_WAIT2) {
……
/* New data or FIN. If new data arrive after half-duplex close,
* reset.
*/
if (!th->fin ||
TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1) {
kill_with_rst:
inet_twsk_deschedule(tw, &tcp_death_row);
inet_twsk_put(tw);
return TCP_TW_RST;
}
上层函数对于该返回值会向对方发送reset报文。
四、reset报文的处理
/* When we get a reset we do this. */
static void tcp_reset(struct sock *sk)
{
/* We want the right error as BSD sees it (and indeed as we do). */
switch (sk->sk_state) {
case TCP_SYN_SENT:
sk->sk_err = ECONNREFUSED;
break;
case TCP_CLOSE_WAIT:
sk->sk_err = EPIPE;
break;
case TCP_CLOSE:
return;
default:
sk->sk_err = ECONNRESET;
}
if (!sock_flag(sk, SOCK_DEAD))
sk->sk_error_report(sk);
tcp_done(sk);
}
其中的close_wait也就是一侧接收到fin报文之后进入的状态。这也意味着当一方close之后,如果对方再次发送数据过来,此时将会收到对方的reset报文,此时设置自己的套接口错误。
在之前的tcp写入操作时,此时操作为
int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
size_t size)
err = -EPIPE;
if (sk->sk_err || (sk->sk_shutdown & SEND_SHUTDOWN))
goto do_error;
下次写入套接口时如果之前收到过reset,则应用程序会收到SIGPIPE信号。