TCP socket option

来源:互联网 发布:java 字符串转义函数 编辑:程序博客网 时间:2024/02/24 04:10

 

Author:http://lenky.info/

 

Last Modified Date: 2013-02-24

Last Modified Date: 2012-12-27

Created Date: 2012-10-21

 

TCP_NODELAY

在网络拥塞控制领域,我们知道有一个非常有名的算法叫做Nagle算法(Naglealgorithm),这是使用它的发明人JohnNagle的名字来命名的,JohnNagle在1984年首次用这个算法来尝试解决福特汽车公司的网络拥塞问题(RFC 896),该问题的具体描述是:如果我们的应用程序一次产生1个字节的数据,而这个1个字节数据又以网络数据包的形式发送到远端服务器,那么就很容易导致网络由于太多的数据包而过载。比如,当用户使用Telnet连接到远程服务器时,每一次击键操作就会产生1个字节数据,进而发送出去一个数据包,所以,在典型情况下,传送一个只拥有1个字节有效数据的数据包,却要发费40个字节长包头(即ip头20字节+tcp头20字节)的额外开销,这种有效载荷(payload)利用率极其低下的情况被统称之为愚蠢窗口症候群(Silly WindowSyndrome)。可以看到,这种情况对于轻负载的网络来说,可能还可以接受,但是对于重负载的网络而言,就极有可能承载不了而轻易的发生拥塞瘫痪。

针对上面提到的这个状况,Nagle算法的改进在于:如果发送端欲多次发送包含少量字符的数据包(一般情况下,后面统一称长度小于MSS的数据包为小包,与此相对,称长度等于MSS的数据包为大包,为了某些对比说明,还有中包,即长度比小包长,但又不足一个MSS的包;MSS,TCP最大分段大小,以太网下一般就是1460字节。),则发送端会先将第一个小包发送出去,而将后面到达的少量字符数据都缓存起来而不立即发送,直到收到接收端对前一个数据包报文段的ACK确认、或当前字符属于紧急数据,或者积攒到了一定数量的数据(比如缓存的字符数据已经达到数据包报文段的最大长度)等多种情况才将其组成一个较大的数据包发送出去,具体有哪些情况,我们来看看内核(以linux-3.4.4为例,后同)实现:

1383:      Filename: \linux-3.4.4\net\ipv4\tcp_output.c1384:      /*Return 0, if packet can be sent now without violation Nagle's rules:1385:      * 1.It is full sized.1386:      * 2.Or it contains FIN. (already checked by caller)1387:      * 3.Or TCP_CORK is not set, and TCP_NODELAY is set.1388:      * 4.Or TCP_CORK is not set, and all sent packets are ACKed.1389:      *    With Minshall's modification: allsent small packets are ACKed.1390:      */1391:      staticinline int tcp_nagle_check(const struct tcp_sock *tp,1392:                                    const struct sk_buff *skb,1393:                                    unsigned mss_now, int nonagle)1394:      {1395:             return skb->len < mss_now&&1396:                    ((nonagle & TCP_NAGLE_CORK) ||1397:                    (!nonagle &&tp->packets_out && tcp_minshall_check(tp)));1398:      }1399:     1400:      /*Return non-zero if the Nagle test allows this packet to be1401:      *sent now.1402:      */1403:      staticinline int tcp_nagle_test(const struct tcp_sock *tp, const struct sk_buff *skb,1404:                                  unsigned intcur_mss, int nonagle)1405:      {1406:             /* Nagle rule does not apply to frames,which sit in the middle of the1407:             * write_queue (they have no chances toget new data).1408:             *1409:             * This is implemented in the callers,where they modify the 'nonagle'1410:             * argument based upon the location ofSKB in the send queue.1411:             */1412:             if (nonagle & TCP_NAGLE_PUSH)1413:                    return 1;1414:     1415:             /* Don't use the nagle rule for urgentdata (or for the final FIN).1416:             * Nagle can be ignored during F-RTO too(see RFC4138).1417:             */1418:             if (tcp_urg_mode(tp) ||(tp->frto_counter == 2) ||1419:                (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN))1420:                    return 1;1421:     1422:             if (!tcp_nagle_check(tp, skb, cur_mss,nonagle))1423:                    return 1;1424:     1425:             return 0;1426:      }


这一段Linux内核代码非常容易看,因为注释代码足够的多。从函数tcp_nagle_test()看起,第1412行是直接进行参数判断,如果在外部(也就是调用者)主动设置了TCP_NAGLE_PUSH旗标,比如主动禁止Nagle算法或主动拔走塞子(下一节TCP_CORK内容)或明确是连接最后一个包(比如连接close()前发出的数据包),此时当然是返回1从而把数据包立即发送出去;第1418-1420行代码处理的是特殊包,也就是紧急数据包、带FIN旗标的结束包以及带F-RTO旗标的包;第1422行进入到tcp_nagle_check()函数进行判断,该函数的头注释有点混乱而不太清楚,我再逐句代码解释一下,首先要看明白如果该函数返回1,则表示该数据包不立即发送;再看具体实现就是:skb->len < mss_now为真表示如果包数据长度小于当前MSS;nonagle &TCP_NAGLE_CORK为真表示当前已主动加塞或明确标识立即还会有数据过来(内核表示为MSG_MORE);!nonagle为真表示启用Nagle算法;tp->packets_out为真表示存在有发出去的数据包没有被ACK确认;tcp_minshall_check(tp)是Nagle算法的改进,先直接认为它与前一个判断相同,具体后续再讲。把这些条件按与或组合起来就是:如果包数据长度小于当前MSS &&((加塞 || 有数据马上过来)||(启用Nagle算法 && 存在有发出去的数据包没有被ACK确认)),那么缓存数据而不立即发送:

上左图(台式主机图样为发送端,又叫客户端,服务器主机图样为接收端,又叫服务器)是未开启Nagle算法的情况,此时客户端应用层下传的数据包被立即发送到网络上(暂不考虑发送窗口与接收窗口这些固有限制,下同),而不管该数据包的大小如何,因此在网络里就有可能同时存在该连接的多个小包;而如上右图所示上,在未收到服务器对第一个包的ACK确认之前,客户端应用层下传的数据包被缓存了起来,当收到ACK确认之后(图中给的情况是这种,当然还有其他情况,前面已经详细描述过)才发送出去,这样不仅总包数由原来的3个变为2个,网络负载降低,与此同时,客户端和服务器都只需处理两个包,消耗的CPU等资源也减少了。

Nagle算法在一些场景下的确能提高网络利用率、降低包处理(客户端或服务器)主机资源消耗并且工作得很好,但是在某些场景下却又弊大于利,要说清楚这个问题需要引入另一个概念,即延迟确认(Delayed ACK)。延迟确认是提高网络利用率的另一种优化,但它针对的是ACK确认包。我们知道,对于TCP协议而言,正常情况下,接收端会对它收到的每一个数据包向发送端发出一个ACK确认包(如前面图示那样);而一种相对的优化就是把ACK延后处理,即ACK与数据包或窗口更新通知包等一起发送(文档RFC 1122),当然这些数据包都是由接收端发送给发送端(接收端和发送端只是一个相对概念)的:

上左图是一般情况,上右图(这里只画出了ACK延迟确认机制中的两种情况:通过反向数据携带ACK和超时发送ACK)中,数据包A的ACK是通过接收端发回给发送端的数据包a携带一起过来的,而对应的数据包a的ACK是在等待超时之后再发送的。另外,虽然RFC 1122标准文档上,超时时间最大值是500毫秒,但在实际实现中最大超时时间一般为200毫秒(并不是指每一次超时都要等待200毫秒,因为在收到数据时,定时器可能已经经历一些时间了,在最坏情况的最大值也就是200毫秒,平均等待超时值为100毫秒),比如在linux3.4.4有个TCP_DELACK_MAX的宏标识该超时最大值:

115: Filename : \linux-3.4.4\include\net\tcp.h116: #defineTCP_DELACK_MAX      ((unsigned)(HZ/5)) /* maximal time to delay before sending an ACK*/

回过头来看Nagle算法与ACK延迟确认的相互作用,仍然举个例子来讲,如果发送端暂有一段数据要发送给接收端,这段数据的长度不到最大两个包,也就是说,根据Nagle算法,发送端发出去第一个数据包后,剩下的数据不足以组成一个可立即发送的数据包(即剩余数据长度没有大于等于MSS),因此发送端就会等待,直到收到接收端对第一个数据包的ACK确认或者应用层传下更多需要发送的数据等(这里暂只考虑第一个条件,即收到ACK);而在接收端,由于ACK延迟确认机制的作用,它不会立即发送ACK,而是等待,直到(具体情况请参考内核函数tcp_send_delayed_ack(),由于涉及到情况太过复杂,并且与当前内容关系不大,所以略过,我们仅根据RFC1122来看):1,收到发送端的第二个大数据包;2,等待超时(比如,200毫秒)。当然,如果本身有反向数据包要发送,那么可以携带ACK,但是在最糟的情况下,最终的结果就是发送端的第二个数据包需要等待200毫秒才能被发送到网络上。而在像HTTP这样的应用里,某一时刻的数据基本是单向的,所以出现最糟情况的概率非常的大,而且第二个数据包往往用于标识这一个请求或响应的成功结束,如果请求和响应都要超时等待的话,那么时延就得增大400毫秒。

针对在上面这种场景下Nagle算法缺点改进的详细情况描述在文档:http://tools.ietf.org/id/draft-minshall-nagle-01.txt里,在linux内核里也已经应用了这种改进,也就是前面未曾详细讲解的函数tcp_minshall_check():

1376:      Filename: \linux-3.4.4\net\ipv4\tcp_output.c1377:      /*Minshall's variant of the Nagle send check. */1378:      staticinline int tcp_minshall_check(const struct tcp_sock *tp)1379:      {1380:             return after(tp->snd_sml,tp->snd_una) &&1381:                    !after(tp->snd_sml,tp->snd_nxt);1382:      }

函数名是按改进提出者的姓名来命名的,这个函数的实现很简单,但要理解它必须先知道这些字段的含义(RFC 793、RFC 1122):tp->snd_nxt,下一个待发送的字节(序号,后同);tp->snd_una,下一个待确认的字节,如果它的值等于tp->snd_nxt,则表示所有已发数据都已经得到了确认;tp->snd_sml,已经发出去的最近的一个小包的最后一个字节(注意,不一定是已确认)。具体图示如下:

总结前面所有介绍的内容,Minshall对Nagle算法所做的改进简而言之就是一句话:在判断当前包是否可发送时,只需检查最近的一个小包是否已经确认(其它需要判断的条件,比如包长度是否大于MSS等这些没变,这里假定判断到最后,由此处决定是否发送),如果是,即前面提到的tcp_minshall_check(tp)函数返回值为假,从而函数tcp_nagle_check()返回0,那么表示可以发送(前面图示里的上图),否则延迟等待(前面图示里的下图)。基于的原理很简单,既然发送的小包都已经确认了,也就是说网络上没有当前连接的小包了,所以发送一个即便是比较小的数据包也无关大碍,同时更重要的是,这样做的话,缩短了延迟,提高了带宽利用率。

那么对于前面那个例子,由于第一个数据包是大包,所以不管它所对应的ACK是否已经收到都不影响对是否发送第二个数据包所做的检查与判断,此时因为所有的小包都已经确认(其实是因为本身就没有发送过小包),所以第二个包可以直接发送而无需等待。

传统Nagle算法可以看出是一种包-停-等协议,它在未收到前一个包的确认前不会发送第二个包,除非是“迫不得已”,而改进的Nagle算法是一种折中处理,如果未确认的不是小包,那么第二个包可以发送出去,但是它能保证在同一个RTT内,网络上只有一个当前连接的小包(因为如果前一个小包未被确认,不会发出第二个小包);但是,改进的Nagle算法在某些特殊情况下反而会出现不利,比如下面这种情况(3个数据块相继到达,后面暂时也没有其他数据到达),传统Nagle算法只有一个小包,而改进的Nagle算法会产生2个小包(第二个小包是延迟等待超时产生),但这并没有特别大的影响(所以说是它一种折中处理):

TCP中的Nagle算法默认是启用的,但是它并不是适合任何情况,对于telnet或rlogin这样的远程登录应用的确比较适合(原本就是为此而设计),但是在某些应用场景下我们却又需要关闭它。在链接:http://www.isi.edu/lsam/publications/phttp_tcp_interactions/node2.html里提到Apache对HTTP持久连接(Keep-Alive,Prsistent-Connection)处理时凸现的奇数包&结束小包问题(The Odd/Short-Final-SegmentProblem),这是一个并的关系,即问题是由于已有奇数个包发出,并且还有一个结束小包(在这里,结束小包并不是指带FIN旗标的包,而是指一个HTTP请求或响应的结束包)等待发出而导致的。我们来看看具体的问题详情,以3个包+1个结束小包为例,下图是一种可能发生的发包情况:

最后一个小包包含了整个响应数据的最后一些数据,所以它是结束小包,如果当前HTTP是非持久连接,那么在连接关闭时,最后这个小包会立即发送出去,这不会出现问题;但是,如果当前HTTP是持久连接(非pipelining处理,pipelining仅HTTP 1.1支持,并且目前有相当一部分陈旧但仍在广泛使用中的浏览器版本尚不支持,nginx目前对pipelining的支持很弱,它必须是前一个请求完全处理完后才能处理后一个请求),即进行连续的Request/Response、Request/Response、…,处理,那么由于最后这个小包受到Nagle算法影响无法及时的发送出去(具体是由于客户端在未结束上一个请求前不会发出新的request数据,导致无法携带ACK而延迟确认,进而导致服务器没收到客户端对上一个小包的的确认导致最后一个小包无法发送出来),导致第n次请求/响应未能结束,从而客户端第n+1次的Request请求数据无法发出:

正是由于有这个问题,所以如果可能会遇到这种情况,nginx就事前主动关闭Nagle算法,我们来看nginx代码:

2436:      Filename: ngx_http_request.c2437:      staticvoid2438:      ngx_http_set_keepalive(ngx_http_request_t*r)2439:      {2440:      …2623:          if (tcp_nodelay2624:              && clcf->tcp_nodelay2625:              && c->tcp_nodelay== NGX_TCP_NODELAY_UNSET)2626:          {2627:              ngx_log_debug0(NGX_LOG_DEBUG_HTTP,c->log, 0, "tcp_nodelay");2628:     2629:              if (setsockopt(c->fd, IPPROTO_TCP,TCP_NODELAY,2630:                             (const void *)&tcp_nodelay, sizeof(int))2631:                  == -1)2632:              {2633:      …2646:              c->tcp_nodelay =NGX_TCP_NODELAY_SET;2647:          }

Nginx执行到这个函数内部,就说明当前连接是持久连接。第2623行的局部变量tcp_nodelay是用于标记TCP_CORK选项的,由配置指令tcp_nopush指定,默认情况下为off,在linux下,nginx把TCP_NODELAY和TCP_CORK这两个选项完全互斥使用(事实上,从内核版本2.5.71以后,它们就可以相互接合使用),禁用TCP_CORK选项时,局部变量tcp_nodelay值为1(从该变量可以看到,nginx对这两个选项的使用,TCP_CORK优先级别高于TCP_NODELAY);clcf->tcp_nodelay对应TCP_NODELAY选项的配置指令tcp_nodelay的配置值,默认情况下为1;c->tcp_nodelay用于标记当前是否已经对该套接口设置了TCP_NODELAY选项,第一次执行到这里时,值一般情况下也就是NGX_TCP_NODELAY_UNSET(除非不是IP协议等),因为只有此处一个地方设置TCP_NODELAY选项。所以,整体来看,如果此判断为真,于是第2629行对套接口设置TCP_NODELAY禁止Nagle算法(字段c->tcp_nodelay被赋值为NGX_TCP_NODELAY_SET,表示当前已经对该套接口设置了TCP_NODELAY选项),最后的响应数据会被立即发送出去,从而解决了前面提到的可能问题。

 

TCP_CORK

从上一节的内容可以看到,选项TCP_NODELAY是禁用Nagle算法,即数据包立即发送出去,而选项TCP_CORK与此相反,可以认为它是Nagle算法的进一步增强,即阻塞数据包发送,具体点说就是:TCP_CORK选项的功能类似于在发送数据管道出口处插入一个“塞子”,使得发送数据全部被阻塞,直到取消TCP_CORK选项(即拔去塞子)或被阻塞数据长度已超过MSS才将其发送出去。举个对比示例,比如收到接收端的ACK确认后,Nagle算法可以让当前待发送数据包发送出去,即便它的当前长度仍然不够一个MSS,但选项TCP_CORK则会要求继续等待,这在前面的tcp_nagle_check()函数分析时已提到这一点,即如果包数据长度小于当前MSS&&((加塞 || …)|| ...),那么缓存数据而不立即发送:

上右图显示的选项TCP_CORK的理论情况,但是在各个具体协议栈的实际实现中,有一些机制会打破选项TCP_CORK的这个“完全”堵塞(即数据包长度不到一个MSS则不允许发送)特性。以linux 3.4.4版本的内核代码实现为例,正常的tcp数据发送流程为(调用片段:仅从tcp层往ip层发送的函数调用关系):

tcp_push() -> __tcp_push_pending_frames() ->tcp_write_xmit()

如果函数tcp_write_xmit()有发送数据成功,不论发送了多少个数据包,它都将返回0;但如果一个数据包也未发送,比如可能受当前拥塞窗口和发送窗口的限制,也可能是受选项TCP_CORK(函数tcp_write_xmit()内会调用tcp_nagle_test()函数做数据包发送判断)等的影响,导致数据包暂不能发送,此时就可能返回1:

1730:      Filename: \linux-3.4.4\net\ipv4\tcp_output.c1731:      /*This routine writes packets to the network. It advances the1732:      *send_head.  This happens as incoming acksopen up the remote1733:      *window for us.1734:      …1739:      *Returns 1, if no segments are in flight and we have queued segments, but1740:      *cannot send anything now because of SWS or another problem.1741:      */1742:      staticint tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,1743:                             int push_one, gfp_t gfp)1744:      {1745:      …1814:             if (likely(sent_pkts)) {1815:                    tcp_cwnd_validate(sk);1816:                    return 0;1817:             }1818:             return !tp->packets_out &&tcp_send_head(sk);1819:      }1820:     1821:      /*Push out any pending frames which were held back due to1822:      *TCP_CORK or attempt at coalescing tiny packets.1823:      * Thesocket must be locked by the caller.1824:      */1825:      void__tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,1826:                                  int nonagle)1827:      {1828:      …1835:             if (tcp_write_xmit(sk, cur_mss, nonagle,0, GFP_ATOMIC))1836:                    tcp_check_probe_timer(sk);1837:      }

从上面代码的第1818行看到了函数tcp_write_xmit()返回1的条件:如果所有发出去的数据包都已经确认并且有数据包等待发送。如果这两个条件成立,导致第1836行的函数调用tcp_check_probe_timer()被执行。这个函数会启动一个零窗口探测定时器,最短超时时间为200毫秒(取动态值RTO,所以会根据网络环境变动),反正不管怎样,这在一段时间后,定时器对应的回调函数tcp_probe_timer()就将被执行而发送零窗口探测包。

为什么要在这里插入描述一大堆看似无关而又“高深”的Linux内核协议栈实现,原因在于函数tcp_probe_timer()发送的零窗口探测包会破坏前面提到的选项TCP_CORK的“完全”堵塞特性。如果当前有数据等待发送并且接收端又有一定的接收缓存空间(即待发送数据的起始序号在接收端通告窗口的允许范围之内),那么函数tcp_probe_timer()就会根据接收端的可用缓存区情况,创建并发送一个适量长度的负载有等待发送数据的数据包,函数调用关系为:

tcp_probe_timer() -> tcp_send_probe0() ->tcp_write_wakeup() -> tcp_transmit_skb()

在这之间不会调用到tcp_nagle_test()函数做数据包发送判断,所以数据包能得以发送出去,即便当前处于TCP_CORK选项“堵塞”情况。这种策略其实很好理解,TCP_CORK选项“堵塞”特性的最终目的无法是为了提高网络利用率,既然反正是要发一个数据包(零窗口探测包),如果有实际数据等待发送,那么干脆就直接发送一个负载等待发送数据的数据包岂不是更好?关于这些具体细节,描述得比较简单,因为它们不是本节的相关重点,主要是为了说明一点:真实的情况与理论的描述也许会有些差别,也给喜欢追根究底的人一个粗浅的解释,另外,在链接http://lenky.info/?p=1892有对上面这些结论的实验验证。

我们已经知道,TCP_CORK选项的作用主要是阻塞小数据发送,所以在nginx内的用处就在对响应头的发送处理上。一般而言,处理一个客户端请求之后的响应数据包括有响应头和响应体两部分,那么利用TCP_CORK选项就能让这两部分数据一起发送:

36:   Filename : ngx_linux_sendfile_chain.c37:   ngx_chain_t*38:   ngx_linux_sendfile_chain(ngx_connection_t*c, ngx_chain_t *in, off_t limit)39:   {40:   ...150:         /* set TCP_CORK if there is a headerbefore a file */151:152:         if (c->tcp_nopush ==NGX_TCP_NOPUSH_UNSET153:             && header.nelts != 0154:             && cl155:             && cl->buf->in_file)156:         {157: ...189:             if (c->tcp_nodelay ==NGX_TCP_NODELAY_UNSET) {190:191:                 if (ngx_tcp_nopush(c->fd) ==NGX_ERROR) {192:                     err = ngx_errno;


由于TCP_CORK选项是Linux特有的,在其他比如BSD平台上,与此对应的是TCP_NOPUSH选项,所以在nginx内部通过函数ngx_tcp_nopush()/ngx_tcp_push()来分别对应启用/禁用TCP_CORK选项,达到各个平台的一致性封装。

第152行的if判断给出了需设置TCP_CORK选项的前提条件,变量c->tcp_nopush的值为NGX_TCP_NOPUSH_UNSET则表示TCP_CORK选项当前处于禁用状态,所以才需要进入到if块内去执行函数ngx_tcp_nopush()启用TCP_CORK选项;值得注意的是,禁用状态并不是表示nginx不使用TCP_CORK选项,如果设置为不使用该选项,那么对应该变量的值则为NGX_TCP_NOPUSH_DISABLED。第153-155的判断为真则表示响应头和响应体同时存在,并且响应体在文件内;为什么要把“响应体在文件内”作为一个是否启用TCP_CORK选项的条件,原因在后面《数据读/写传输方式》一节对系统函数writev()进行描述时有讲到,在这里简单的说一句就是:如果待发送数据全部都在内存缓冲区,那么使用系统函数writev()可达到更好的效果,从而无需使用TCP_CORK选项。另外,由于nginx对选项TCP_CORK和TCP_NODELAY是互斥使用,所以有底189行的if判断。开启TCP_CORK选项发送完响应数据后,在连接结束的其中一个处理函数,也就是ngx_http_set_keepalive()内又将禁用TCP_CORK选项,即拔掉塞子,让阻塞的数据可以发送出去,但是否立即发送出去还需由选项TCP_NODELAY以及Nagle算法决定。

对于选项TCP_CORK和TCP_NODELAY,除了前面提到的这些使用逻辑之外,在nginx的upstream模块也有对应的使用,不过都比较简单而不多累述,但要注意的就是,nginx始终是在互斥使用这两个选项,也正因为如此,为了避免错误的认为这两个选项必须互斥使用,下面就介绍一下这两个选项的混合使用情况。

对于一个套接口描述符,选项TCP_NODELAY和TCP_CORK可以同时存在,这是无容置疑的。看一下内核里设置两个选项时所对应的操作:

2129:      Filename: \linux-3.4.4\net\ipv4\tcp.c2130:      staticint do_tcp_setsockopt(struct sock *sk, int level,2131:                    int optname, char __user *optval,unsigned int optlen)2132:      {2133:      …2268:             case TCP_NODELAY:2269:                    if (val) {2270:      …2278:                           tp->nonagle |=TCP_NAGLE_OFF|TCP_NAGLE_PUSH;2279:                           tcp_push_pending_frames(sk);2280:                    } else {2281:                           tp->nonagle &=~TCP_NAGLE_OFF;2282:                    }2283:                    break;2284:      …2299:             case TCP_CORK:2300:      …2311:                    if (val) {2312:                           tp->nonagle |=TCP_NAGLE_CORK;2313:                    } else {2314:                           tp->nonagle &=~TCP_NAGLE_CORK;2315:                           if(tp->nonagle&TCP_NAGLE_OFF)2316:                                  tp->nonagle |=TCP_NAGLE_PUSH;2317:                           tcp_push_pending_frames(sk);2318:                    }2319:                    break;


前面已经提到过,如果选项TCP_CORK存在,那么选项TCP_NODELAY的作用将被弱化,这在函数tcp_nagle_check()里能看到这一点,而对应的TCP_NAGLE_CORK旗标正好在第2312行里设置,也就是启用TCP_CORK选项时打上该标记;从第2315-2317代码可以看出,只有当选项TCP_CORK被清除后,选项TCP_NODELAY的作用才会体现出来,但是存在一个特别的时间点,也就是在开启选项TCP_NODELAY时,如第2278-2279行所示,此时会设置TCP_NAGLE_PUSH旗标,而后调用tcp_push_pending_frames()函数,把当前发送队列的数据包强制发送(即PUSH)出去,即便当前设置有选项TCP_CORK,在前面提到的函数tcp_nagle_test()里对TCP_NAGLE_PUSH旗标的特殊处理论证了这一点。当有新的数据包被加入到发送队列时,会调用函数skb_entail()清除TCP_NAGLE_PUSH标记,对于此时的这些数据包(新加进来的数据包以及在这个新数据包加进来之前还没发送完的旧数据包),选项TCP_CORK才又占主导地位:

535: Filename : \linux-3.4.4\net\ipv4\tcp.c536: staticinline void skb_entail(struct sock *sk, struct sk_buff *skb)537: {538: …549:        if (tp->nonagle & TCP_NAGLE_PUSH)550:               tp->nonagle &=~TCP_NAGLE_PUSH;551: }


 

TCP_DEFER_ACCEPT

学过计算机网络课程的人,应该都知道TCP协议有个连接状态转换图,也许对其整体详细不甚清楚,但至少对TCP握手协议有些印象。标准的TCP三次握手(本节仅考虑这种情况,对于四次握手等其他情况,可以参考RFC793和http://lenky.info/?p=1921)的一般图示如下:

上面图示中的服务器端并不那么容易让人理解,因为我们知道监听套接口仅做请求接收操作,其创建后就永远都处在LISTEN状态(除非服务进程关闭),因此转换为SYN_RCVD、ESTABLISHED状态的必定是服务端根据客户端请求而新建立的连接套接口:

上面的图示只是一种理想状况,而Linux TCP协议栈的实际实现却并不一定就是这样,性能优化是一方面,比如Linux在收到客户端的SYN包时,并不创建实际的连接套接口,而只是创建一个request_sock结构体来代表这个连接套接口,但很显然,相比完全的套接口sock结构体,request_sock结构体要小得多,相关图示如下(为了后面对比,加上accept()的调用时机):

而另一方面则是因为在现实网络中,有一种专门针对TCP协议的攻击:TCP半连接攻击。这种攻击的原理很容易理解,既然服务器要在收到客户端的请求(即SYN数据包到达)后创建相关结构体等而需要消耗系统资源(比如内存,即便是像Linux这种做了优化处理,一次消耗的资源比较少),因此,如果客户端持续发送大量的SYN包,那么就会不断的消耗服务器端的可用资源,如果针对服务器发回的SYN+ACK包,客户端也不做出ACK回复,从而使得服务器反复发送SYN+ACK包导致情况进一步恶化,每个请求连接都持续如此直到连接的最终超时,而在这个持续的过程中,可能有些正常的客户端请求,服务器端因为系统资源不足而无法创建连接、提供服务。

Syn_cookies是针对TCP半连接攻击的重要防范方法之一,原理也很简单,既然TCP半连接攻击的基本原理是服务器端会在收到客户端的SYN包时消耗资源,那么最简单的防范也就是服务器端在收到客户端的SYN包时尽量不做资源分配,把这个动作延后处理(可以看到,连接套接口处于SYN_RCVD状态的时间非常的短):

而必要的信息(也就是客户端的验证信息)保存在网络上。“把信息保存在网络上”,这听起来有点不可思议,但它的依据却也十分简单,因为在正常的情况下,对于服务器发送的SYN+ACK包,假定其SEQ为N,正常客户端回复的ACK包的ACK值将是N+1,所以,服务器端只需把客户端的相关信息转换为一个数值N(也就是cookie值)并发送给客户端,而本身无需保存任何信息,当在收到客户端的回复ACK包时,再根据客户端的相关信息得到一个值M,比较M和ACK包里的ACK-1值,在正常的情况下,它们必定匹配(匹配并不是指两个值完全相等,如何判断是否匹配与具体的转换算法相关)。当然,这只是保证了不消耗内存等存储资源,对于服务器端网络带宽、CPU计算资源(如果计算cookie值的算法已经复杂,可能反而消耗更多)等依旧无法避免消耗,所以Syn_cookies也并非无懈可击。另外,值得说明一点的是,并非开启Linux系统的Syn_cookies选项(即:echo 1 > /proc/sys/net/ipv4/tcp_syncookies)就会立即让Linux TCP协议栈对每一个客户端请求都走发送Syn Cookies的流程,而只有在系统判断当前请求达到一定数量且满足相关条件时才会这么做:

1258:      Filename: \linux-3.4.4\net\ipv4\tcp_ipv4.c1259:      inttcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)1260:      {1261:      …1281:             if (inet_csk_reqsk_queue_is_full(sk)&& !isn) {1282:                    want_cookie =tcp_syn_flood_action(sk, skb, "TCP");1283:                    if (!want_cookie)1284:                           goto drop;1285:             }


上面所有描述的这些与本节将介绍的TCP_DEFER_ACCEPT选项有何关系?这在于选项TCP_DEFER_ACCEPT本身的目标功能:它将服务器端连接套接口转变成ESTABLISHED状态的时机后移。具体来讲,就是只有当服务器端收到客户端发送的实际请求数据后,才建立其对应的连接套接口(如果同时开启Syn_cookies,那么对于需要走发送Syn Cookies流程的客户端请求就按照前面介绍的Syn_cookies图示,即此时Syn_cookies起主导作用,但这并不是说Syn_cookies与选项TCP_DEFER_ACCEPT不能共用,前面说过,即便是启动Syn_cookies,也并不是所有的客户端请求都会走发送Syn Cookies的流程,所以对于那些不走Syn Cookies流程并且开启选项TCP_DEFER_ACCEPT的监听套接口,就是下面图示这样):

从上图中看到,连接套接口处于SYN_RCVD状态的时间大大的延长,所以要模拟出SYN_RCVD状态的连接套接口非常的容易,对nginx的监听套接口开启选项TCP_DEFER_ACCEPT选项,比如配置为这样:“listen 80 deferred;”,再写一个测试程序(前面有类似的完整代码,这里仅给出关键部分):

46:   Filename : tcp_defer_test.c47:       if (connect(sockfd, (struct sockaddr*)(&server_addr),48:               sizeof(struct sockaddr)) == -1) {49:   …68:       getchar();69:  70:       write(sockfd, req_header,strlen(req_header));


编译并执行该程序,它会立即connect()到nginx服务程序,但是如果在write()实际数据前让它主动停顿(即getchar();)下来,那么此时利用netstat查看系统的套接口状态(因为都在同一台机器上,三个套接口都显示出来了):

[root@localhost ~]# netstat -natp | grep 80tcp 0 0 0.0.0.0:80     0.0.0.0:*      LISTEN      21922/nginx        tcp 0 0 10.0.0.1:80    10.0.0.1:56073SYN_RECV    -                  tcp 0 0 10.0.0.1:56073 10.0.0.1:80    ESTABLISHED 3537/./tcp_defer_te

服务器端nginx的监听套接口处于LISTEN状态,其针对客户端请求而建立的连接套接口处于SYN_RECV状态,而客户端tcp_defer_test进程的套接口处于ESTABLISHED状态。

从这个示例可以看到,在客户端发送实际的请求数据前,服务器端的对应连接套接口一直(理论情况,在大多数情况下,Linux内核里的等待时间段都不可能无限的长,因为系统的定时器和超时机制无处不在,我说“一直”一般也就是指直到超时或连接断开,比如收到rst包等,所以请理解字词句里要表达的主要意思,而不要抠字眼,否则夹杂各种例外情况的说明反而忽略的重点,当有必要说明时,比如后续分析到选项TCP_DEFER_ACCEPT在Linux内核里实现时,再根据实际源码把握细节)都处于SYN_RECV状态,即便TCP三次握手已经成功完成。

前面讲过,Syn_cookies使得服务器对系统资源的分配做了一次延后处理,也就是拖延了Linux TCP协议栈(内核态)对资源进行分配的时机,从而在一定程度上防御TCP半连接攻击;但如果客户端发起的是TCP全连接攻击,即客户端发出正常的TCP三次握手流程把连接建立起来(此种情况,Syn_cookies当然是通过的),但不发送任何实际请求数据(因为一旦发送实际的请求数据,服务器端就能进行下一步处理,可能提前判断出请求有错或处理完成而结束,从而达不到让服务器长时间等待消耗的目的),达到消耗服务器应用层(用户态)资源的目的。以nginx为例,当其accept()接受客户端连接请求后,就会创建相关数据结构,比如ngx_connection_t,而我们知道,整个nginx服务进程能分配的ngx_connection_t是非常有限的,如果这个请求迟迟不结束,数据结构ngx_connection_t不能及时释放,后续再来正常的客户端请求,nginx就很有可能提供正常服务:

46:   Filename : ngx_event_accept.c47:           c = ngx_get_connection(s, ev->log);48:  49:           if (c == NULL) {50:               if (ngx_close_socket(s) == -1) {51:                   ngx_log_error(NGX_LOG_ALERT,ev->log, ngx_socket_errno,52:                                ngx_close_socket_n " failed");


针对如此,根据选项TCP_DEFER_ACCEPT的特性,它在一定程度上缓解了TCP全连接攻击;相比在应用层处理这些非法的客户端请求(即TCP全连接攻击)所带来的消耗,把它们直接交给内核去做处理所消耗的系统资源会更少一点。

下面开始看Linux内核里对选项TCP_DEFER_ACCEPT的具体实现(下面的分析都是针对选项TCP_DEFER_ACCEPT的相关流程),在应用层给套接口设置该选项的常规用法是这样:

46:   Filename : ngx_connection.c47:               if (setsockopt(ls[i].fd,IPPROTO_TCP, TCP_DEFER_ACCEPT,48:                              &timeout,sizeof(int))49:                   == -1)


有一个参数timeout很重要,很明显,它指定的是等待客户端发送实际数据的超时时间(nginx默认的就是60秒);这个系统调用对应到内核里就是执行这些代码:

109: Filename : \linux-3.4.4\net\ipv4\tcp.c110:        case TCP_DEFER_ACCEPT:111:               /* Translate value in seconds tonumber of retransmits */112:               icsk->icsk_accept_queue.rskq_defer_accept=113:                      secs_to_retrans(val,TCP_TIMEOUT_INIT / HZ,114:                                    TCP_RTO_MAX /HZ);115:               break;

通过函数secs_to_retrans()将应用层传下来的超时时间转换为了对应的重传次数,即服务器是通过统计重传次数(具体重传次数由三个值共同决定:TCP_DEFER_ACCEPT选项、TCP_SYNCNT选项、proc文件系统的tcp_synack_retrie)来判断是否超时的。具体来看,在服务器收到三次握手流程里的第三个数据包(即ACK数据包)时,关键代码在函数tcp_check_req()内:

567: Filename : \linux-3.4.4\net\ipv4\tcp_minisocks.c568: struct sock*tcp_check_req(struct sock *sk, struct sk_buff *skb,569:                         struct request_sock *req,570:                         struct request_sock **prev)571: {572: …720:        /* While TCP_DEFER_ACCEPT is active, dropbare ACK. */721:        if (req->retrans <inet_csk(sk)->icsk_accept_queue.rskq_defer_accept &&722:           TCP_SKB_CB(skb)->end_seq == tcp_rsk(req)->rcv_isn + 1) {723:               inet_rsk(req)->acked = 1;724:               NET_INC_STATS_BH(sock_net(sk),LINUX_MIB_TCPDEFERACCEPTDROP);725:               return NULL;726:        }

第721行的前半句判断,意味着如果服务器恰好在发送完最后一个(此时TCP_DEFER_ACCEPT选项占主导,那此时发送完后,重传次数刚好超限)或几个(此时是TCP_SYNCNT选项或proc文件系统设置占主导,对应的设置值比TCP_DEFER_ACCEPT选项设置值要大,如果只大1,那么也就是最后一个包)重传SYN+ACK包时收到客户端的ACK数据包,那么不走TCP_DEFER_ACCEPT流程,从而连接将建立并通知应用层的accept()函数。

这就是TCP_DEFER_ACCEPT选项的一种例外情况,虽然客户端还没有发送实际数据,但最终连接也建立起来了,为什么要这样做?这是因为要考虑,有可能客户端待发送的实际数据本身就没准备好而需要一定的等待时间(因而持续回复ACK),也有可能网络环境导致SYN+ACK包与ACK包的确发生了丢失;反正不管怎样,对于出现这样的例外情况时,Linux系统就认为它属于正常的客户端请求,需建立最终连接。

第721行的后半句判断不难理解,它判断当前收到的ACK包是否携带有实际数据,如果是,那么就无需走TCP_DEFER_ACCEPT流程;对于走TCP_DEFER_ACCEPT流程的ACK数据包会被直接丢掉,MiniSocket仍然是Mini Socket(即没有转换为Connect Socket),但其状态被更改为已确认(即inet_rsk(req)->acked= 1;);第724行是系统做信息统计,与此处逻辑无关。

如果客户端一直没有发送实际的请求数据,那么服务器就会反复的重传三次握手流程里的第二个数据包(即SYN+ACK数据包),重传的次数由三个数值决定:1,sysctl_tcp_synack_retries,它可以通过proc文件系统的/proc/sys/net/ipv4/tcp_synack_retries节点来控制;2,icsk_syn_retries,它可以通过TCP_SYNCNT选项来设置;3,rskq_defer_accept,它可以通过TCP_DEFER_ACCEPT选项来设置。关于SYN+ACK定时器的启用与取消不多详说,我们直接看其对应的主要执行函数inet_csk_reqsk_queue_prune()。首先从下面代码可以看到重传计数取值的优先顺序为TCP_DEFER_ACCEPT选项 > TCP_SYNCNT选项 > proc文件系统设置(注意变量max_retries和thresh的取值,后面会谈到):

655: Filename : \linux-3.4.4\net\ipv4\inet_connection_sock.c656: voidinet_csk_reqsk_queue_prune(struct sock *parent,657: …655:        int max_retries =icsk->icsk_syn_retries ? : sysctl_tcp_synack_retries;656:        int thresh = max_retries;657: …655:        if (queue->rskq_defer_accept)656:               max_retries =queue->rskq_defer_accept;

接着是对Mini Socket重传YN+ACK数据包的超时判断,逻辑很简单,如果已超时,那么将丢弃该请求(虽然当前还只是半连接,代码575-576行);如果需要重传SYN+ACK数据包,那么就将执行rtx_syn_ack()回调函数(对应函数tcp_v4_rtx_synack()):

2365:      Filename: \linux-3.4.4\net\ipv4\inet_connection_sock.c2366:                                  syn_ack_recalc(req,thresh, max_retries,2367:                                                queue->rskq_defer_accept,2368:                                                &expire, &resend);2369:      …2365:                                  if (!expire&&2366:                                      (!resend ||2367:                                       !req->rsk_ops->rtx_syn_ack(parent,req, NULL) ||2368:                                       inet_rsk(req)->acked)) {2369:      …2365:                                         continue;2366:                                  }2367:     2368:                                  /* Drop this request*/2369:                                  inet_csk_reqsk_queue_unlink(parent,req, reqp);2370:                                  reqsk_queue_removed(queue,req);

重要的是第555-557行代码,其计算当前Mini Socket是否已经超时(参数expire)以及是否需要再重传SYN+ACK数据包(参数resend);除了前面提到的重传SYN+ACK数据包的最大次数,还要结合其它条件才能判断当前Mini Socket是否超时,比如,如果当前Accept半连接队列里存在太多的陈旧连接(即有重传过SYN+ACK数据包的连接),那么就要将超时的阈值降低(参数thresh值更小),让这些陈旧连接提前结束以腾出空间接收后续新来的连接请求。看函数syn_ack_recalc()相关代码:

567: Filename : \linux-3.4.4\net\ipv4\inet_connection_sock.c568:        *expire = req->retrans >= thresh&&569:                (!inet_rsk(req)->acked || req->retrans >= max_retries);570:        /*571:        * Do not resend while waiting for dataafter ACK,572:        * start to resend on end of deferringperiod to give573:        * last chance for data or ACK to createestablished socket.574:        */575:        *resend = !inet_rsk(req)->acked ||576:                req->retrans >= rskq_defer_accept - 1;

代码487-488行做超时判断,条件有多个,可以看到如果thresh(依赖TCP_SYNCNT选项和proc文件系统设置)大于max_retries(依赖TCP_DEFER_ACCEPT选项),那么即便是重传次数大于TCP_DEFER_ACCEPT选项设定值,仍可能不被判断为超时。再结合代码494-495行来看,此时也可能需要重传SYN+ACK数据包,也就是说,最终起决定作用的反而是TCP_SYNCNT选项和proc文件系统设置值,所以总体来讲,重传次数基本等于TCP_DEFER_ACCEPT选项(如果未设置就当0看待)和TCP_SYNCNT选项(如果未设置就取proc文件系统设置值,但还受当前网络环境影响,比如前面提到的陈旧连接太多)设置的最大值。

 

SO_LINGER

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.h51:   structlinger {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协议的经典书(比如RichardSteven的《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.c668:        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.c418: intinet_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.c1895:      voidtcp_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 theconnection. */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.c1942:             } 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.c2062:      inttcp_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.c2329:      voidtcp_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 tryinguntil 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.c89:   staticinline 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:   voidsk_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.h768: #definesk_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.c5843:      inttcp_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 inthese states,6030:                    * RFC 1122 says we MUST send areset.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.c3203:      voidtcp_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.c1595:      inttcp_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.c1975:             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.h1581:      staticinline 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 <sys/socket.h>

int shutdown(int sockfd, inthow);

其中只有参数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.c785: intinet_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 otherlisteners, who can poll for812:                 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.c1861:      voidtcp_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 aclosed 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 completedpackets.  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.c1895:      voidtcp_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.c2987:      staticvoid2988:      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.c935:             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.c2193:          if (!ngx_terminate2194:               && !ngx_exiting2195:               && r->keepalive2196:               && clcf->keepalive_timeout> 0)2197:          {2198:              ngx_http_set_keepalive(r);2199:              return;2200:          }2201:     2202:          if (clcf->lingering_close ==NGX_HTTP_LINGERING_ALWAYS2203:              || (clcf->lingering_close ==NGX_HTTP_LINGERING_ON2204:                  && (r->lingering_close2205:                      || r->header_in->pos <r->header_in->last2206:                      ||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.c820: void821: 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.c438: ngx_int_t439: 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.c496: void497: 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.c2771:      staticvoid2772:      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.c2816:      staticvoid2817:      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对延迟关闭的实现与前面所设计的延迟关闭类似,只是在实际处理上有一点差别。

 PS

我只想说,我的路还太长太长……

原创粉丝点击