从上一节的内容可以看到,选项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.c 1731: /* This routine writes packets to the network. It advances the 1732: * send_head. This happens as incoming acks open up the remote 1733: * window for us. 1734: … 1739: * Returns 1, if no segments are in flight and we have queued segments, but 1740: * cannot send anything now because of SWS or another problem. 1741: */ 1742: static int 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 to 1822: * TCP_CORK or attempt at coalescing tiny packets. 1823: * The socket 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.c 37: 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 header before a file */ 151: 152: if (c->tcp_nopush == NGX_TCP_NOPUSH_UNSET 153: && header.nelts != 0 154: && cl 155: && 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.c 2130: static int 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.c 536: static inline 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: }
Socket选项系列完整Word文档:tcp socket option.rar
转载请保留地址:http://www.lenky.info/archives/2013/02/2218 或 http://lenky.info/?p=2218
备注:如无特殊说明,文章内容均出自Lenky个人的真实理解而并非存心妄自揣测来故意愚人耳目。由于个人水平有限,虽力求内容正确无误,但仍然难免出错,请勿见怪,如果可以则请留言告之,并欢迎来信讨论。另外值得说明的是,Lenky的部分文章以及部分内容参考借鉴了网络上各位网友的热心分享,特别是一些带有完全参考的文章,其后附带的链接内容也许更直接、更丰富,而我只是做了一下归纳&转述,在此也一并表示感谢。关于本站的所有技术文章,欢迎转载,但请遵从CC创作共享协议,而一些私人性质较强的心情随笔,建议不要转载。
法律:根据最新颁布的《信息网络传播权保护条例》,如果您认为本文章的任何内容侵犯了您的权利,请以Email或书面等方式告知,本站将及时删除相关内容或链接。