首页 > *nix技术, TCP/IP, 内核技术, 网络协议 > Socket选项系列之SO_LINGER

Socket选项系列之SO_LINGER

2013年2月24日 发表评论 阅读评论 18,156 次浏览

SO_LINGER是nginx里用到的另外一个重要套接口选项(因为它涉及到的问题很重要),虽然它不是特定于TCP套接口的,但针对的仍然是面向连接的协议,因此就TCP而言,自然也是可以使用它,这里就统一以TCP为例(即下面所提到的套接口仍然都还是TCP套接口)来进行阐述。

当应用程序在调用close()函数关闭TCP连接时,Linux内核的默认行为是将套接口发送队列里的原有数据(比如之前残留的数据)以及新加入的数据(比如函数close()产生的FIN标记,如果发送队列没有残留之前的数据,那么这个FIN标记将单独产生一个新数据包)发送出去并且销毁套接口(并非把相关资源全部释放,比如只是把内核对象sock标记为dead状态等,这样当函数close()返回后,TCP发送队列的数据包仍然可以继续由内核协议栈发送,但是一些相关操作就会受到影响和限制,比如对数据包发送失败后的重传次数)后立即返回。这需要知道两点:第一,当应用程序获得close()函数的返回值时,待发送的数据可能还处在Linux内核的TCP发送队列里,因为当我们调用write()函数成功写出数据时,仅表示这些数据被Linux内核接收放入到发送队列,如果此时立即调用close()函数返回后,那么刚才write()的数据限于TCP本身的拥塞控制机制(比如发送窗口、接收窗口等等),完全有可能还呆在TCP发送队列里而未被发送出去;当然也有可能发送出去一些,毕竟在调用函数close()时,进入到Linux内核后有一次数据包的主动发送机会,即:
tcp_close() -> tcp_send_fin() -> __tcp_push_pending_frames() -> tcp_write_xmit()

第二,所有这些数据的发送,最终却并不一定能全部被对端确认(即数据包到了对端TCP协议栈的接收队列),只能做到TCP协议本身提供的一定程度的保证,比如TCP协议的重传机制(并且受close()函数影响,重传机制弱化,也就是如果出现类似系统资源不足这样的问题,调用过close()函数进行关闭的套接口所对应的这些数据会优先丢弃)等,因为如果网络不好可能导致TCP协议放弃继续重传或在意外收到对端发送过来的数据时连接被重置导致未成功发送的数据全部丢失(后面会看到这种情况)。

针对如此,Linux提供了一个套接口选项SO_LINGER,可以改变在套接口上执行close()函数时的默认行为。选项SO_LINGER用到的相关参数主要是一个linger结构体:

50:	Filename : \linux-3.4.4\include\linux\socket.h
51:	struct linger {
52:		int		l_onoff;	/* Linger active		*/
53:		int		l_linger;	/* How long to linger for	*/
54:	};

注释很清楚,字段l_onoff标记是否启用Linger特性,非0为启用,0为禁用(即内核对close()函数采取默认行为);字段l_onoff为非0的情况下,字段l_linger生效,如果它的值为0,则导致所有数据丢失且连接立即中止;如果字段l_linger的值为非0(假定为t秒),那么此时函数close()将被阻塞(假定为阻塞模式)直到:
1) 待发送的数据全部得到了对端确认,返回值为0;
2) 超时返回,返回值为-1,errno被设置为EWOULDBLOCK。
上面两点是很多介绍TCP/IP协议的经典书(比如Richard Steven的《Unix网络编程》)上所描述的,但是却并不适合Linux系统上的实现(《Unix网络编程》应该是根据BSD上的实现来讲的,所以有些结论不适合Linux系统上的实现,这很正常)。在Linux系统上,应该是函数close()将被阻塞(假定为阻塞模式)直到:
1) 待发送的数据全部得到了对端确认,返回值为0;
2) 发生信号中断或异常(比如意外收到对端发送过来的数据)或超时,返回值为0;

也就是说,在Linux系统上,针对SO_LINGER选项而言,不论哪种情况,函数close()总是返回0(注意我所针对的情况,我并没有说在Linux系统上,函数close()就总是返回0,如果你关闭一个无效的描述符,它同样也会返回-EBADF的错误),并且对于情况2),Linux内核不会清空缓存区,更加不会向对端发送RST数据包,即执行close()函数的后半部分代码时不会因此发送任何特别的流程变化(当然,因为close()函数阻塞了一段时间,在这段时间内,套接口相关字段可能被TCP协议栈修改过了,所以导致相关判断结果发生变化,但这并不是由于情况2)直接导致)。你可以说这是Linux内核实现的BUG,但从Linux 2.2+ 开始,它就一直存在,但从未被修复,个人猜测原因有二:第一,基本不会有“通过检测close()返回值来判断待发送数据是否发送成功”这种需求,检测close()返回值更多的是用来判断当前关闭的描述符是否有效等;第二,即便判断出数据没有发送成功,此时套接口的相关资源已经释放(当然,也可以实现对资源先不释放,但如果这样完全保留,那么将导致系统不必要的资源浪费),应用程序也无法做出更多补救措施,除了打印一条错误日志以外。更重要的是,实现“判断待发送数据是否成功发送”的需求有更好的不深度依赖Linux内核的应用层实现方式,即后面将提到的shutdown()函数,至于close()函数,做好套接口关闭这一单独的功能就好。所以,即便Linux内核对启用SO_LINGER选项的套接口调用函数close()的各种情况统一返回0也并无特别严重之处。

那么,在Linux系统上,选项SO_LINGER是否就没有什么实用的价值了?当然不是,首先,它完全实现了l_onof非0而l_linger为0情况下的逻辑;其次,它的确阻塞了close()函数,直到待发送的数据全部得到了对端确认或信号中断、异常、超时返回;在阻塞的这一段时间内,套接口尚且还处在正常状态,即此时还没有打上SOCK_DEAD的标记,因此TCP重传等各种机制还能平等使用,保证待发送数据发送成功的概率更大。

下面来看Linux内核代码的相关具体实现,照样关注我们的重点,首先仍然是设置SO_LINGER选项处:

667:	Filename : \linux-3.4.4\net\core\sock.c
668:		case SO_LINGER:
669:	…
677:			if (!ling.l_onoff)
678:				sock_reset_flag(sk, SOCK_LINGER);
679:			else {
680:	…
685:					sk->sk_lingertime = (unsigned int)ling.l_linger * HZ;
686:				sock_set_flag(sk, SOCK_LINGER);
687:			}

这很容易理解,将应用程序传进入的值设置到套接口变量sk上,注意到第685行代码可知l_linger字段是以秒为单位。
当应用程序调用函数close()关闭套接口时,与此相关的函数调用路径如下:
sys_close() -> filp_close() -> fput() -> __fput() -> sock_close() -> sock_release() -> inet_release() -> tcp_close()
直接看后面两个函数,函数inet_release()的相关代码如下:

417:	Filename : \linux-3.4.4\net\ipv4\af_inet.c
418:	int inet_release(struct socket *sock)
419:	{
420:		struct sock *sk = sock->sk;
421:	…
437:			timeout = 0;
438:			if (sock_flag(sk, SOCK_LINGER) &&
439:			    !(current->flags & PF_EXITING))
440:				timeout = sk->sk_lingertime;
441:			sock->sk = NULL;
442:			sk->sk_prot->close(sk, timeout);
443:		}
444:		return 0;

需说明两点:首先,函数inet_release()有返回值,但永远返回0(只有一处返回代码,在第444行);其次,在有设置SO_LINGER选项的情况下(如果程序当前正在退出就不做linger操作),修改timeout变量(初始值为0)的值为sk->sk_lingertime(代码第440行),而对于这个变量,前面刚看到过,应该还未忘记它的值来源何处。
在代码第442行,即调入到函数tcp_close()内,这是核心函数,注意它的返回值类型为void,配合前面的inet_release()函数,可以看到它们至少不会直接反馈错误到更上一层(即系统调用,这进一步说了前面所描述的函数close()总是返回0的结论),再看其它关键代码:

1894:	Filename : \linux-3.4.4\net\ipv4\tcp.c
1895:	void tcp_close(struct sock *sk, long timeout)
1896:	{
1897:		struct sk_buff *skb;
1898:		int data_was_unread = 0;
1899:	….
1902:		sk->sk_shutdown = SHUTDOWN_MASK;
1903:	…
1917:		while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) {
1918:			u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq -
1919:				  tcp_hdr(skb)->fin;
1920:			data_was_unread += len;
1921:			__kfree_skb(skb);
1922:		}
1923:	…
1937:		if (data_was_unread) {
1938:			/* Unread data was tossed, zap the connection. */
1939:			NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
1940:			tcp_set_state(sk, TCP_CLOSE);
1941:			tcp_send_active_reset(sk, sk->sk_allocation);
1942:		} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {

先注意到代码第1902行,把套接口标记为不可再读也不可再写(宏SHUTDOWN_MASK值为3),在后面会看到这个标记的使用。根据文档RFC 2525,当一个套接口正在或已经被关闭,如果在其接收队列有未读数据(不管是在关闭前就已收到的,或者还是在关闭后新到达的),那么此时就需给对端发送一个RST数据包;而上面这些代码实现的就是在关闭时对接收对应是否存在有未读数据的检测(代码第1917-1922行、第1937行)和处理(代码第1940-1941行),如果检测到有,那么直接将套接口状态设置为TCP_CLOSE,并发送一个RST数据包(代码第1941行,通过tcp_send_active_reset() -> tcp_transmit_skb()直接发送),此时也就没有常规的四次挥手过程。继续看接下来的代码:

1941:	Filename : \linux-3.4.4\net\ipv4\tcp.c
1942:		} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
1943:			/* Check zero linger _after_ checking for unread data. */
1944:			sk->sk_prot->disconnect(sk, 0);
1945:			NET_INC_STATS_USER(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
1946:		} else if (tcp_close_state(sk)) {
1947:	…
1972:			tcp_send_fin(sk);
1973:		}
1974:	
1975:		sk_stream_wait_close(sk, timeout);

第1942行是linger结构体的字段l_onoff为1而l_linger为0的情况,此时调用sk->sk_prot->disconnect(sk, 0) -> tcp_disconnect()函数丢失所有接收数据并且直接断开连接,具体也就是发送RST数据包,清空相关接收队列:

2061:	Filename : \linux-3.4.4\net\ipv4\tcp.c
2062:	int tcp_disconnect(struct sock *sk, int flags)
2063:	{
2064:	…
2068:		int old_state = sk->sk_state;
2069:	
2070:		if (old_state != TCP_CLOSE)
2071:			tcp_set_state(sk, TCP_CLOSE);
2072:	…
2087:		tcp_clear_xmit_timers(sk);
2088:		__skb_queue_purge(&sk->sk_receive_queue);
2089:		tcp_write_queue_purge(sk);
2090:		__skb_queue_purge(&tp->out_of_order_queue);

第1946-1972行代码属于正常的结束流程,即四次挥手,此时需先调用函数tcp_close_state()切换状态,并判断是否需要发送FIN数据包(比如,如果当前还处于TCP_SYN_SENT状态,连接尚未完全建立,自然就不用发送FIN数据包),如果需要发送FIN数据包则调用tcp_send_fin()函数:

2328:	Filename : \linux-3.4.4\net\ipv4\tcp_output.c
2329:	void tcp_send_fin(struct sock *sk)
2330:	{
2331:		struct tcp_sock *tp = tcp_sk(sk);
2332:		struct sk_buff *skb = tcp_write_queue_tail(sk);
2333:	…
2341:		if (tcp_send_head(sk) != NULL) {
2342:			TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_FIN;
2343:	…
2345:		} else {
2346:			/* Socket is locked, keep trying until memory is available. */
2347:			for (;;) {
2348:				skb = alloc_skb_fclone(MAX_TCP_HEADER,
2349:						       sk->sk_allocation);
2350:	…
2358:			tcp_init_nondata_skb(skb, tp->write_seq,
2359:					     TCPHDR_ACK | TCPHDR_FIN);
2360:			tcp_queue_skb(sk, skb);
2361:		}
2362:		__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_OFF);

如果发送队列存在待发送数据包,那么直接把FIN标记打在队末数据包上即可(代码第2331-2342);否则就新创建一个无实际数据内容的数据包并加入到发送队列(代码第2347-2360);最后调用函数__tcp_push_pending_frames() -> tcp_write_xmit()发送数据包。
与SO_LINGER选项相关的代码在第1975行,这是一个阻塞等待函数,参数timeout指示了等待的时间(单位为时钟滴答),既然sk_stream_wait_close()函数是实现SO_LINGER选项阻塞特性的关键,那么有必要看一下它的全景:

88:	Filename : \linux-3.4.4\net\core\stream.c
89:	static inline int sk_stream_closing(struct sock *sk)
90:	{
91:		return (1 << sk->sk_state) &
92:		       (TCPF_FIN_WAIT1 | TCPF_CLOSING | TCPF_LAST_ACK);
93:	}
94:	
95:	void sk_stream_wait_close(struct sock *sk, long timeout)
96:	{
97:		if (timeout) {
98:			DEFINE_WAIT(wait);
99:	
100:			do {
101:				prepare_to_wait(sk_sleep(sk), &wait,
102:						TASK_INTERRUPTIBLE);
103:				if (sk_wait_event(sk, &timeout, !sk_stream_closing(sk)))
104:					break;
105:			} while (!signal_pending(current) && timeout);
106:	
107:			finish_wait(sk_sleep(sk), &wait);
108:		}
109:	}

一眼就能看得出核心代码在第100-105行,while循环的退出点有两处,首先很直白的退出条件是当前进程收到信号或时间超时,而另一处退出点在第103行的if判断里,sk_wait_event()是一个宏,展开形式如下:

767:	Filename : \linux-3.4.4\include\net\sock.h
768:	#define sk_wait_event(__sk, __timeo, __condition)			\
769:		({	int __rc;						\
770:			release_sock(__sk);					\
771:			__rc = __condition;					\
772:			if (!__rc) {						\
773:				*(__timeo) = schedule_timeout(*(__timeo));	\
774:			}							\
775:			lock_sock(__sk);					\
776:			__rc = __condition;					\
777:			__rc;							\
778:		})

变量__timeo会被修改(代码773行),也就是对应的timeout会被修改,直到0导致函数sk_stream_wait_close()退出;另一方面,代码776行也就是执行函数sk_stream_closing(sk),该函数判断的是套接口当前状态,如果处于TCPF_FIN_WAIT1(也就是FIN_WAIT1状态,TCPF_FIN_WAIT1只是为了代码逻辑实现方面而设置的另一种表示,其它类同)或TCPF_CLOSING或TCPF_LAST_ACK则返回1,而整个被({与})包含起来的代码片段769-778(这属于GCC的扩展功能:语句表达式)的值也就是最末一条语句,即第777行代码__rc的值,也就是函数sk_stream_closing(sk)的返回结果,最终作为前面提到的while循环的第二个退出条件。

需补充说明两点:第一,为什么如果套接口当前状态处于TCPF_FIN_WAIT1或TCPF_CLOSING或TCPF_LAST_ACK则返回1,也就是此时不退出(注意函数调用前的取非符号)?在经过前面的tcp_close_state(sk)函数调用后(具体代码实现很简单,就是一个转换跳转表),套接口的当前状态只可能处于TCP_CLOSE、TCP_FIN_WAIT1、TCP_FIN_WAIT2、TCP_LAST_ACK、TCP_CLOSING(已经在关闭了,所以不会出现CLOSE_WAIT状态,而TIME_WAIT状态很特殊,到了这种状态,也就是弥留阶段,在这之前基本不用考虑它)这五种状态中的一种,而根据TCP状态迁移图(如下,仅画出结束部分):

在这五个状态中只有FIN_WAIT1、CLOSING、LAST_ACK这三种状态表示发送的数据(至少有FIN数据包)未被确认,所以需继续阻塞,如果是另外两种状态,CLOSE不用说,而对于FIN_WAIT2表示从FIN_WAIT1迁移过来,而迁移的条件为收到ACK,也就是FIN数据被确认。
第二,在这个阻塞等待的过程中,如果对端发送了数据包过来,根据文档RFC 2525,这属于异常数据包,因为此时的套接口已经处于关闭状态,到达的数据包无法递交给上层应用程序,所以遇到这种情况就需要发送RST数据包并且所有待发送数据丢失,可能发生的调用路径如下:
sk_stream_wait_close() -> release_sock() -> __release_sock() -> sk_backlog_rcv() -> tcp_v4_do_rcv() -> tcp_rcv_state_process() -> tcp_reset()

最后两个函数的相关代码如下:

5842:	Filename : \linux-3.4.4\net\ipv4\tcp_input.c
5843:	int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
5844:				  const struct tcphdr *th, unsigned int len)
5845:	{
5846:	…
6020:		/* step 7: process the segment text */
6021:		switch (sk->sk_state) {
6022:		case TCP_CLOSE_WAIT:
6023:		case TCP_CLOSING:
6024:		case TCP_LAST_ACK:
6025:			if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt))
6026:				break;
6027:		case TCP_FIN_WAIT1:
6028:		case TCP_FIN_WAIT2:
6029:			/* RFC 793 says to queue data in these states,
6030:			 * RFC 1122 says we MUST send a reset.
6031:			 * BSD 4.4 also does reset.
6032:			 */
6033:			if (sk->sk_shutdown & RCV_SHUTDOWN) {
6034:				if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
6035:				    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
6036:	…
6037:					tcp_reset(sk);
6038:					return 1;
6039:				}
6040:			}
6041:			/* Fall through */
6042:		case TCP_ESTABLISHED:
6043:			tcp_data_queue(sk, skb);

前面曾看到过在tcp_close()函数进入后就设置了不可读/写,即sk->sk_shutdown = SHUTDOWN_MASK;,但是后面将提到shutdown()函数却可以设置不可写但可读,所以在处在这些套接口关闭状态而又收到对端数据包时,需要先判断套接口的读写标记(代码第6033行),其中宏RCV_SHUTDOWN值为1,有此标记则表示套接口不可读,如果此时收到的数据包内包含有实际有效数据(代码第6034-6035行,其中第6034行判断为假则表示这是一个没有负载实际数据的数据包,而第6035行判断为假则表示它负载的实际数据是之前的重复数据比如可能是网络原因导致的对端重传等),那么执行tcp_reset()函数(代码第6037行)。
补充一点,对于代码6025-6026行,为什么在CLOSE_WAIT、CLOSING、LAST_ACK这三种状态下,收到对端的实际有效数据反而break掉而不发送RST包呢?因为处于这三种状态都是由于收到对端的FIN包导致,即对端已经处于关闭状态,自然也就没有必要再发送RST包去扰乱对端,而又因为要Fall through到第6043行保存数据,所以如上这样实现,这不多说。
接着前面的,在函数tcp_reset()内会调用tcp_done()函数,强制的修改套接口状态为CLOSE(即当回退到函数sk_stream_wait_close()内时,将导致其内的while循环退出,也就是close()函数提前异常返回)并且清除所有发送定时器(所有待发送数据丢失):

3202:	Filename : \linux-3.4.4\net\ipv4\tcp.c
3203:	void tcp_done(struct sock *sk)
3204:	{
3205:	…
3208:		tcp_set_state(sk, TCP_CLOSE);
3209:		tcp_clear_xmit_timers(sk);

在回退到函数tcp_v4_do_rcv()时再给对端发送一个rst数据包:

1594:	Filename : \linux-3.4.4\net\ipv4\tcp_ipv4.c
1595:	int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
1596:	{
1597:	…
1643:	reset:
1644:		tcp_v4_send_reset(rsk, skb);

函数tcp_v4_send_reset()和前面看到的tcp_send_active_reset()不同,因为此时的套接口在系统里可能不再存在(已经关闭),所以它必须通过收到的对端数据包的相关信息来构造这个RST包,这实现在调用函数tcp_v4_send_reset() -> ip_send_reply()内。
不管怎样情况,当经过sk_stream_wait_close()函数调用后,进行的就是套接口相关资源消耗操作:

1974:	Filename : \linux-3.4.4\net\ipv4\tcp.c
1975:		sk_stream_wait_close(sk, timeout);
1976:	
1977:	adjudge_to_death:
1978:		state = sk->sk_state;
1979:		sock_hold(sk);
1980:		sock_orphan(sk);
1981:	…

只看函数sock_orphan()内容,在代码第1584行,给套接口内核对象sk打上SOCK_DEAD的标记:

1580:	Filename : \linux-3.4.4\include\net\sock.h
1581:	static inline void sock_orphan(struct sock *sk)
1582:	{
1583:		write_lock_bh(&sk->sk_callback_lock);
1584:		sock_set_flag(sk, SOCK_DEAD);
1585:		sk_set_socket(sk, NULL);
1586:		sk->sk_wq  = NULL;
1587:		write_unlock_bh(&sk->sk_callback_lock);
1588:	}

因为有了这个标记,在TCP协议栈的很多其它地方,比如重传定时函数tcp_retransmit_timer()里,对该套接口的处理将被“优先”考虑,比如减少必要的重传次数,资源不足时提前对其进行回收等。过多内容就不再展开讨论,从上面所有这些分析来看,通过SO_LINGER选项来保证数据正确到达对端可靠吗?明显不太可靠,因为SO_LINGER选项仅仅只是延迟对套接口打上SOCK_DEAD死亡的标记,让系统更平等一点对待它。

那么,编写TCP网络程序涉及到的一个重要问题凸显出来了,即:如何尽全力(不可能做到百分之百保证,比如如果网络断了,那自然没法)保证write()写出的数据正确的到达对端TCP协议栈的接收队列而又不被其意外丢弃?如果要求正确到达对端应用层的对应程序,那么自然就需要在应用程序内做相互确认,而这只适应我们对客户端和服务器端都可控的情况;对于nginx而言,我们可控的只有服务器端,所以这里不讨论这种需求情况。

在Linux系统上,前面已经说明了单独的选项SO_LINGER对此是无能为力的,所以需要结合选项SO_LINGER、函数close()、函数shutdown()、函数read()做配合设计:
1) 设置SO_LINGER选项参数l_onof非0而l_linger为0;
2) 调用函数shutdown(sock_fd, SHUT_WR);
3) 设置超时定时器,假定为t秒;
4) 调用函数read(sock_fd)阻塞等待,直到读到EOF或被定时器超时中断。
5) 执行函数close(sock_fd)或者调用exit(0)进程退出。

这个设计较好的解决了前面讨论的使用SO_LINGER选项与close()函数的两个问题,第一:调用close()关闭套接口时或后,如果接收队列里存在有对端发送过来的数据,那么根据文档RFC 2525,此时需给对端发送一个RST数据包;假定有这样一种场景(以HTTP的pipelining情况为例,HTTP协议有点特殊,它基本是request/response的单向数据发送形式,如果是其它交互同时性更强的应用,出现问题的概念更大,但因为我们对HTTP应用比较熟悉,所以就用它为例以更容易理解):
1) 客户端应用程序在同一条TCP连接里连续向服务器端发送120个请求(访问很多门户网站的首页时,请求的文件可能还不止这个数目)。
2) 客户端的所有请求数据顺序到达服务器端,服务器端应用程序即开始逐个从内核里读取请求数据处理并把响应数据通过网络发回给客户端。
3) 服务器端应用程序(假定为nginx)限制了在一条连接上只能处理100个请求,因此在处理完第100个请求后结束,调用close()函数关闭连接。
4) 服务器端内核执行对应的tcp_close()函数时发现接收队列还有请求数据(即请求101-120)因此发送一个RST数据包到客户端。
5) 客户端应用程序依次从内核TCP接收队列读取服务器端发回的响应数据,但恰好正在读取第85个请求的响应数据时,客户端内核收到服务器端的RST数据包,因而丢掉所有接收内容,这包括已被服务器端正常处理了的请求86-100的响应数据。

也就是,上面这种场景下,服务器端write()写出的数据已经正确的到达对端TCP协议栈的接收队列,但却因为服务器端的原因而导致其被意外丢弃。设置SO_LINGER选项是徒劳的,因为在这种情况下,服务器端照样会发送RST数据包到。
调用函数shutdown(fd, SHUT_WR)是解决第一个问题的关键,它仅设置套接口不可写,即向对端发送一个FIN数据包,表示本端没有数据需要继续发送,但是还可以接收数据,所以此时的套接口对应接收队列里有数据或后续收到对端发送过来的数据都不会导致服务器端发送RST数据包,避免了客户端丢弃已正确收到的响应数据。我们先看一下shutdown()函数的API描述:
#include
int shutdown(int sockfd, int how);
其中只有参数how需要说明一下,它可取值SHUT_RD或SHUT_WR或SHUT_RDWR,分别表示关闭读、关闭写、关闭读写,这也就比close()函数只能进行关闭读写来得更灵活一点。该函数可能的返回值有0表示正常,-1表示出错,对应的errno被设置为EBADF(无效描述符)、ENOTSOCK(描述符不是套接口)、ENOTCONN(套接口未连接)。
再来看shutdown()函数在内核里的具体实现,当应用程序调用函数shutdown()关闭套接口时,与此相关的函数调用路径如下:
sys_shutdown() -> inet_shutdown() -> tcp_shutdown()
当一条TCP连接被多个进程共享时,如果其中一个进程调用close()函数关闭其对应的套接口时,调用到内核里仅仅只是减少对应的引用计数,而不会对TCP连接做任何关闭操作(即在前面路径就已经返回了);只有当最后一个进程进行close()关闭时,引用计数变为0时才进行真正的套接口释放操作(也即此时才会深调到tcp_close()函数内)。而函数shutdown()不一样,它是套接口类型描述符所特有的操作,直接作用于套接口连接,根本就没有考虑引用计数的影响,这从它的调用路径就可以基本看出这一点。

其实函数shutdown()在内核里做的工作非常的少,SO_LINGER选项对它也不会产生任何影响,看代码:

784:	Filename : \linux-3.4.4\net\ipv4\af_inet.c
785:	int inet_shutdown(struct socket *sock, int how)
786:	{
787:	…
808:		switch (sk->sk_state) {
809:		case TCP_CLOSE:
810:			err = -ENOTCONN;
811:			/* Hack to wake up other listeners, who can poll for
812:			   POLLHUP, even on eg. unconnected UDP sockets -- RR */
813:		default:
814:			sk->sk_shutdown |= how;
815:			if (sk->sk_prot->shutdown)
816:				sk->sk_prot->shutdown(sk, how);
817:			break;
818:	…
823:		case TCP_LISTEN:
824:			if (!(how & RCV_SHUTDOWN))
825:				break;
826:			/* Fall through */
827:		case TCP_SYN_SENT:
828:			err = sk->sk_prot->disconnect(sk, O_NONBLOCK);

如果套接口处于TCP_LISTEN或TCP_SYN_SENT状态,那么都将直接disconnect()断开;关注我们的重点,其它所有状态都将设置套接口的读写标记(代码第814行)并且调入到函数tcp_shutdown()内(代码第816行,第815行的判断是因为有些连接协议可能没有提供进一步的shutdown()函数):

1860:	Filename : \linux-3.4.4\net\ipv4\tcp.c
1861:	void tcp_shutdown(struct sock *sk, int how)
1862:	{
1863:	…
1867:		if (!(how & SEND_SHUTDOWN))
1868:			return;
1869:	
1870:		/* If we've already sent a FIN, or it's a closed state, skip this. */
1871:		if ((1 << sk->sk_state) &
1872:		    (TCPF_ESTABLISHED | TCPF_SYN_SENT |
1873:		     TCPF_SYN_RECV | TCPF_CLOSE_WAIT)) {
1874:			/* Clear out any half completed packets.  FIN if needed. */
1875:			if (tcp_close_state(sk))
1876:				tcp_send_fin(sk);
1877:		}
1878:	}

只有在关闭可写的情况下才有必要发送FIN包通告对端,所以在第1867行先做了一个判断。剩下的逻辑也就是判断是否有必要发送FIN包,结合代码里的注释以及前面所讲解的内容,这里就不多累述。
回过头来看函数shutdown(fd, SHUT_WR) 调用,它只标记套接口不可写,所以完美的解决了前面所提到的第一个问题。接着的第二个问题就是:对数据发送是否成功的判断以及如何对超时连接进行及时释放?前面阐述了利用close()函数无法达到这个目的,即便辅助使用SO_LINGER选项。在这里,我们设计等待让对端先关闭,当然,这个等待是有时限的,所以需设置一个定时器,然后阻塞read(),如果读到EOF,也就是对端进行了主动关闭,发送了FIN数据包过来,那么意味着我们发送的数据已经被对端成功接收,此时执行close()函数将会直接关闭:

1894:	Filename : \linux-3.4.4\net\ipv4\tcp.c
1895:	void tcp_close(struct sock *sk, long timeout)
1896:	{
1897:	…
1927:		if (sk->sk_state == TCP_CLOSE)
1928:			goto adjudge_to_death;

如果定时器超时(如果是信号中断,可继续阻塞read(),直到超时为止),那么说明数据多半没有发送成功(因为在正常情况下,一旦对端收到我们发送过去的FIN数据包,即便多做了一些其它处理,它也应该会很快的执行close()进行套接口关闭),在这种情况下,我们执行函数close()进行套接口关闭,由于SO_LINGER选项设置的影响(参数l_onof非0而l_linger为0),此时将直接发送RST包强行中断,因为此时的连接已经超时异常,没必要再做常规的四次挥手流程,把资源及时释放更好。
最后,我们来看在nginx里是如何对SO_LINGER选项的应用以及相关问题的处理,首先是SO_LINGER选项的全部相关代码(对,这就是全部):

2986:	Filename : ngx_http_request.c
2987:	static void
2988:	ngx_http_free_request(ngx_http_request_t *r, ngx_int_t rc)
2989:	{
2990:	…
3033:	    if (r->connection->timedout) {
3034:	        clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
3035:	
3036:	        if (clcf->reset_timedout_connection) {
3037:	            linger.l_onoff = 1;
3038:	            linger.l_linger = 0;
3039:	
3040:	            if (setsockopt(r->connection->fd, SOL_SOCKET, SO_LINGER,
3041:	                          (const void *) &linger, sizeof(struct linger)) == -1)

注意两点:第一,进入设置SO_LINGER选项的if判断,此时连接已经超时并且nginx用户配置了超时重置。第二,linger结构体的字段l_onoff为1而l_linger为0,也就是close()套接口时直接发送RST数据包。初步看来,对于连接的关闭,nginx采用的是我们刚才提到的那种方案,但是它更为直接,对明确判断已经超时的连接都做RST重置处理,下面更具体来看。
可以看到在nginx内部有很多代码处有对ngx_http_close_request()函数的调用,而这都是在处理出现异常的情况下触发,一般也就是在执行某个功能函数时没有正常返回,此时就会调用函数ngx_http_close_request()来关闭请求。比如:

934:	Filename : ngx_http_request.c
935:	            rv = ngx_http_alloc_large_header_buffer(r, 1);
936:	
937:	            if (rv == NGX_ERROR) {
938:	                ngx_http_close_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR);
939:	                return;
940:	            }
941:	…
976:	    if (rev->timedout) {
977:	        ngx_log_error(NGX_LOG_INFO, c->log, NGX_ETIMEDOUT, "client timed out");
978:	        c->timedout = 1;
979:	        ngx_http_close_request(r, NGX_HTTP_REQUEST_TIME_OUT);
980:	        return;
981:	    }

上面仅例举了两处,在第937行判断的是上一个ngx_http_alloc_large_header_buffer()函数执行出错的情况(返回值为NGX_ERROR),此时调用ngx_http_close_request()函数以结束对请求的继续处理;代码第976行判断是当前客户端已超时,所以同样也调用ngx_http_close_request()函数关闭处理;
在函数ngx_http_close_request()末尾处依次调用了两个函数:
ngx_http_close_request() -> ngx_http_free_request(r, rc)
ngx_http_close_request() -> ngx_http_close_connection(c)
前面已经看到在函数ngx_http_free_request()内,会对已经超时的连接设置SO_LINGER选项;而在函数ngx_http_close_connection()函数内最终将调用close()函数关闭连接套接口:
ngx_http_close_connection() -> ngx_close_connection(c) -> close(fd)
那么此时,已经超时的连接就直接发送RST包而强行中断;未超时的连接就调用close()函数进行常规的四次挥手流程,这里也没有使用shutdown()函数,为什么?其实是因为对于这种本就异常结束的连接,nginx就不再多发心思而直接close()掉,否则的话,延迟关闭的异常连接过多反而影响其它正常请求处理的性能,nginx把关注重点放在那些正常结束的连接上,只有它们才会走到延迟关闭的流程上来。下面就具体来看。

在前面章节已经介绍过,函数ngx_http_finalize_connection()是客户端请求被正常处理后的关闭函数,在资源真正释放之前需判断keepalive机制或延迟关闭机制是否启用。当然,keepalive机制优先,因为它暂不关闭连接,而延迟关闭机制到底只是延迟一下。如果走keepalive机制的流程,那么nginx就和延迟关闭机制没有任何关系,从各自进入的条件判断以及先后顺序来看,一般情况下,nginx的延迟关闭机制并不会用得太多:

2192:	Filename : ngx_http_request.c
2193:	    if (!ngx_terminate
2194:	         && !ngx_exiting
2195:	         && r->keepalive
2196:	         && clcf->keepalive_timeout > 0)
2197:	    {
2198:	        ngx_http_set_keepalive(r);
2199:	        return;
2200:	    }
2201:	
2202:	    if (clcf->lingering_close == NGX_HTTP_LINGERING_ALWAYS
2203:	        || (clcf->lingering_close == NGX_HTTP_LINGERING_ON
2204:	            && (r->lingering_close
2205:	                || r->header_in->pos < r->header_in->last
2206:	                || r->connection->read->ready)))
2207:	    {
2208:	        ngx_http_set_lingering_close(r);
2209:	        return;
2210:	    }

进程退出的时机比较少,对于HTTP 1.1协议keepalive默认启用,而keepalive_timeout默认值为75秒,所以综合来看一般会调用第2198行的ngx_http_set_keepalive()函数走keepalive流程。
如果不走keepalive流程,那么对于是否走延迟关闭流程仍需要做一些判断,因为延迟关闭就意味着资源不能及时释放,所以如要这么做则需要满足一定的条件。逐一来看,第2202行表示用户在配置文件里主动设置了lingering_close选项为always,所以必须延迟关闭。第2203-2206行则是在用户设置lingering_close选项为on的情况下所做的判断,因为在某些情况下,即便用户做了这样的设置,但因为没有必要则也不进行延迟关闭。有哪些情况不必要体现在字段r->lingering_close内,我们看几处示例:

819:	Filename : ngx_http_request.c
820:	void
821:	ngx_http_handler(ngx_http_request_t *r)
822:	{
823:	…
844:	        r->lingering_close = (r->headers_in.content_length_n > 0);

如果客户端发送的请求没有请求体,那么第844行就将设置r->lingering_close为0;另一处代码:

437:	Filename : ngx_http_request_body.c
438:	ngx_int_t
439:	ngx_http_discard_request_body(ngx_http_request_t *r)
440:	{
441:	…
484:	    if (ngx_http_read_discarded_request_body(r) == NGX_OK) {
485:	        r->lingering_close = 0;

代码第484行判断为真则表示成功全部丢弃客户端发送的请求体数据。再看一处:

495:	Filename : ngx_http_request.c
496:	void
497:	ngx_http_discarded_request_body_handler(ngx_http_request_t *r)
498:	{
499:	…
515:	    if (r->lingering_time) {
516:	        timer = (ngx_msec_t) (r->lingering_time - ngx_time());
517:	
518:	        if (timer <= 0) {
519:	            r->discard_body = 0;
520:	            r->lingering_close = 0;

代码第518行判断为真则表示已经延迟超时。所以,可以看到在某些情况下,即客户端明确不会发送数据过来或已经超时,就没有必要进行延迟关闭了。而与此相对,如果客户端有很大可能会发送数据过来,那么就需进行延迟关闭,前面的代码第2205-2206行就属于这种情况,此时缓存区里有数据(第2205行)或明确可读(第2206行)。总之,我们需知道延迟关闭所要避免的就是在close()掉套接口时或之后却由于接收缓冲区有对端的数据或收到对端的数据包而导致发送RST包异常终止连接所带来的负面影响(比如导致之前发送给客户端的正常响应数据丢失等)。
不管怎样,一旦对套接口进行延迟关闭,那也就是调用函数ngx_http_set_lingering_close(),看一下这个函数的基本逻辑:

2770:	Filename : ngx_http_request.c
2771:	static void
2772:	ngx_http_set_lingering_close(ngx_http_request_t *r)
2773:	{
2774:	…
2782:	    rev = c->read;
2783:	    rev->handler = ngx_http_lingering_close_handler;
2784:	
2785:	    r->lingering_time = ngx_time() + (time_t) (clcf->lingering_time / 1000);
2786:	    ngx_add_timer(rev, clcf->lingering_timeout);
2787:	
2788:	    if (ngx_handle_read_event(rev, 0) != NGX_OK) {
2789:	…
2803:	    if (ngx_shutdown_socket(c->fd, NGX_WRITE_SHUTDOWN) == -1) {
2804:	…
2810:	    if (rev->ready) {
2811:	        ngx_http_lingering_close_handler(rev);
2812:	    }
2813:	}

代码第2782-2788行设置事件对象rev的超时定时器、监控其可读事件,这样后续不管超时还是发生可读事件,执行的都是回调函数ngx_http_lingering_close_handler();代码第2803行执行shutdown()函数关闭可写(宏ngx_shutdown_socket为shutdown,宏NGX_WRITE_SHUTDOWN为SHUT_WR),也就是向对端发送一个FIN包;代码2810行,如果此时已经可读,那么直接执行函数ngx_http_lingering_close_handler(),下面就来看该函数:

2815:	Filename : ngx_http_request.c
2816:	static void
2817:	ngx_http_lingering_close_handler(ngx_event_t *rev)
2818:	{
2819:	…
2832:	    if (rev->timedout) {
2833:	        ngx_http_close_request(r, 0);
2834:	        return;
2835:	    }
2836:	
2837:	    timer = (ngx_msec_t) (r->lingering_time - ngx_time());
2838:	    if (timer <= 0) {
2839:	        ngx_http_close_request(r, 0);
2840:	        return;
2841:	    }
2842:	…
2843:	    do {
2844:	        n = c->recv(c, buffer, NGX_HTTP_LINGERING_BUFFER_SIZE);
2845:	…
2848:	        if (n == NGX_ERROR || n == 0) {
2849:	            ngx_http_close_request(r, 0);
2850:	            return;
2851:	        }
2852:	
2853:	    } while (rev->ready);
2854:	…
2862:	    timer *= 1000;
2863:	…
2868:	    ngx_add_timer(rev, timer);
2869:	}

代码第2832-2841是判断超时,不管是读超时还是延迟关闭超时,此时都执行函数ngx_http_close_request()进行套接口关闭,此时受SO_LINGER选项影响将直接发送RST包。代码第2843-2853是进行读操作,如果读错(可能是网络断开等)或读的数据长度为0(表示收到对端发送的FIN包)则也调用函数ngx_http_close_request()进行套接口关闭,此时如果对端已经通过发送FIN包进行了关闭,那么这里close()调入到内核也就不会发送RST包,而只是简单关闭回收套接口了(这在前面已经详细描述过)。进入到最后的几行代码,表示需继续等待,所以重新启动定时器,当然,此时的超时时间timer已经变小了(代码第2837行)。
最后,总体来看,nginx对延迟关闭的实现与前面所设计的延迟关闭类似,只是在实际处理上有一点差别。

Socket选项系列完整Word文档:tcp socket option.rar

转载请保留地址:http://www.lenky.info/archives/2013/02/2220http://lenky.info/?p=2220


备注:如无特殊说明,文章内容均出自Lenky个人的真实理解而并非存心妄自揣测来故意愚人耳目。由于个人水平有限,虽力求内容正确无误,但仍然难免出错,请勿见怪,如果可以则请留言告之,并欢迎来讨论。另外值得说明的是,Lenky的部分文章以及部分内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容也许更直接、更丰富,而我只是做了一下归纳&转述,在此也一并表示感谢。关于本站的所有技术文章,欢迎转载,但请遵从CC创作共享协议,而一些私人性质较强的心情随笔,建议不要转载。

法律:根据最新颁布的《信息网络传播权保护条例》,如果您认为本文章的任何内容侵犯了您的权利,请以Email或书面等方式告知,本站将及时删除相关内容或链接。

  1. AAA
    2013年9月19日16:45 | #1

    @AAA
    是接收缓冲区,不是发送缓冲区

  2. AAA
    2013年9月19日16:43 | #2

    博主您好,我还有一个问题,您在文章开头,讲解close的默认行为时:

    “当应用程序在调用close()函数关闭TCP连接时,Linux内核的默认行为是将套接口发送队列里的原有数据(比如之前残留的数据)以及新加入的数据(比如函数close()产生的FIN标记,如果发送队列没有残留之前的数据,那么这个FIN标记将单独产生一个新数据包)发送出去并且销毁套接口(并非把相关资源全部释放,比如只是把内核对象sock标记为dead状态等,这样当函数close()返回后,TCP发送队列的数据包仍然可以继续由内核协议栈发送,但是一些相关操作就会受到影响和限制,比如对数据包发送失败后的重传次数)后立即返回。这需要知道两点:第一,当应用程序获得close()函数的返回值时,待发送的数据可能还处在Linux内核的TCP发送队列里,因为当我们调用write()函数成功写出数据时,仅表示这些数据被Linux内核接收放入到发送队列,如果此时立即调用close()函数返回后,那么刚才write()的数据限于TCP本身的拥塞控制机制(比如发送窗口、接收窗口等等),完全有可能还呆在TCP发送队列里而未被发送出去;当然也有可能发送出去一些,毕竟在调用函数close()时,进入到Linux内核后有一次数据包的主动发送机会,即:
    tcp_close() -> tcp_send_fin() -> __tcp_push_pending_frames() -> tcp_write_xmit()

    第二,所有这些数据的发送,最终却并不一定能全部被对端确认(即数据包到了对端TCP协议栈的接收队列),只能做到TCP协议本身提供的一定程度的保证,比如TCP协议的重传机制(并且受close()函数影响,重传机制弱化,也就是如果出现类似系统资源不足这样的问题,调用过close()函数进行关闭的套接口所对应的这些数据会优先丢弃)等,因为如果网络不好可能导致TCP协议放弃继续重传或在意外收到对端发送过来的数据时连接被重置导致未成功发送的数据全部丢失(后面会看到这种情况)。”

    如果就用默认的close行为,如果此时发送缓冲区里还有应用程序没有处理的数据的吧。
    tcp链接也应该直接发送rst吧。。

    如果是这样的话,so_linger在linux中就只用两个作用了:

    1、l_onoff为1、l_linger为0时,发送rst,快速清空tcp的发送和接收缓冲区
    2、l_onoff为1、l_linger为非0时,对close的行为进行延迟

    我理解的对吗?
    您怎么看?

  3. AAA
    2013年9月19日16:11 | #3

    博主你好,还有一个问题:

    “第1942行是linger结构体的字段l_onoff为1而l_linger为0的情况,此时调用sk->sk_prot->disconnect(sk, 0) -> tcp_disconnect()函数丢失所有接收数据并且直接断开连接,具体也就是发送RST数据包,清空相关接收队列:”

    您的意思是,发送缓冲去的数据不被清空吗,还可以继续发送???

    我的理解是,一旦发送了rst数据包(无论什么情况发送的rst包),不论接受缓冲区还是发送缓冲区里的数据都将瞬间被清空吧???

  4. lenky
    2013年9月19日07:48 | #4

    @ddd
    嗯,谢谢你看得很认真,也自己在思考,佩服一下。

    我没说清楚,其实是这样:
    “Linux内核不会清空缓存区,更加不会向对端发送RST数据包”针对的是close()时发送队列有数据的情况,“更加不会向对端发送RST数据包”是紧接着“Linux内核不会清空缓存区(这个缓存区是指发送缓存区)”而言的。
    后一种针对的是close()时或close()后,接收队列有新数据或到达的情况。

    tcp连接可以半开半闭,描述符close()掉后,缓存的数据是可以继续发送出去的,因此“Linux内核不会清空缓存区,更加不会向对端发送RST数据包”,但对应的描述符不能收数据了(因为close()掉了),所以,如果此时对方发了数据过来(我的那个关闭前与关闭后收到是指close()时刻的瞬间前后,毕竟应用层与内核协议栈是异步的,),比如发送RST以通知对方,如果要发数据过来,必须重置连接。

  5. ddd
    2013年9月18日14:34 | #5

    博主你好,有个问题像你请教一下:
    你在,该段中说
    1) 待发送的数据全部得到了对端确认,返回值为0;
    2) 发生信号中断或异常(比如意外收到对端发送过来的数据)或超时,返回值为0;

    也就是说,在Linux系统上,针对SO_LINGER选项而言,不论哪种情况,函数close()总是返回0(注意我所针对的情况,我并没有说在Linux系统上,函数close()就总是返回0,如果你关闭一个无效的描述符,它同样也会返回-EBADF的错误),并且对于情况2),Linux内核不会清空缓存区,更加不会向对端发送RST数据包,即执行close()函数的后半部分代码时不会因此发送任何特别的流程变化(当然,因为close()函数阻塞了一段时间,在这段时间内,套接口相关字段可能被TCP协议栈修改过了,所以导致相关判断结果发生变化,但这并不是由于情况2)直接导致)。你可以说这是Linux内核实现的BUG,但从Linux 2.2+ 开始,它就一直存在,但从未被修复,个人猜测原因有二:第一,基本不会有“通过检测close()返回值来判断待发送数据是否发送成功”这种需求,检测close()返回值更多的是用来判断当前关闭的描述符是否有效等;第二,即便判断出数据没有发送成功,此时套接口的相关资源已经释放(当然,也可以实现对资源先不释放,但如果这样完全保留,那么将导致系统不必要的资源浪费),应用程序也无法做出更多补救措施,除了打印一条错误日志以外。更重要的是,实现“判断待发送数据是否成功发送”的需求有更好的不深度依赖Linux内核的应用层实现方式,即后面将提到的shutdown()函数,至于close()函数,做好套接口关闭这一单独的功能就好。所以,即便Linux内核对启用SO_LINGER选项的套接口调用函数close()的各种情况统一返回0也并无特别严重之处。

    Linux内核不会清空缓存区,更加不会向对端发送RST数据包

    但是在后面的代码讲解中又说“当一个套接口正在或已经被关闭,如果在其接收队列有未读数据(不管是在关闭前就已收到的,或者还是在关闭后新到达的),那么此时就需给对端发送一个RST数据包”

    两者之间不矛盾吗?
    还是我哪里理解的不对,希望你给解释一下

  6. 江南
    2013年8月1日20:01 | #6

    额,你的代码是不是有乱码~~~

    • lenky
      2013年8月2日14:12 | #7

      额,一些与HTML标记有冲突的字符被反解析了
      你直接下载附件看吧

  1. 本文目前尚无任何 trackbacks 和 pingbacks.