原文How TCP backlog works in Linux
水平有限,难免有错,欢迎指出!
以下为翻译:


当应用程序通过系统调用listen将一个套接字(socket)置为LISTEN状态时,需要为该套接字指定一个backlog参数,该参数通常被描述为用来限制进来的连接队列长度(queue of incoming connections)。

由于TCP协议的三次握手机制,一个进来的套接字连接在进入ESTABLISHED状态并且可以被accept调用返回给应用程序之前,会经历中间状态SYN RECEIVED(见上图)。这意味着TCP协议栈可以有两种方案来实现backlog队列:

  1. 只使用一个队列,队列大小由系统调用listen的backlog参数指定。服务端收到一个SYN数据包后将返回一个SYN/ACK数据包,同时将该连接放入到队列中。当服务端再次收到客户端返回的ACK时,该连接状态将变为ESTABLISHED,从而有资格被交给应用程序处理。
  2. 使用两个队列,一个SYN队列和一个accept队列。处于SYN RECEIVED状态的连接会被添加到SYN队列中,等到后续收到ACK变为ESTABLISHED状态后,该连接会被转移到accept队列中。顾名思义,在这种实现方式下,accept系统调用可以简单地实现为从accept队列中消费连接,此时listen调用的backlog参数决定的是accept队列的大小。

历史上,BCD派生的TCP实现采用第一种方案,这意味着当队列大小达到backlog最大值时,系统将不再发送用以响应SYN数据包的SYN/ACK数据包。通常,TCP实现将简单地丢弃收到的SYN数据包(而不是发送RST数据包)以便客户端重试。这也是W. Richard Stevens的经典教科书《TCP/IP详解 卷三》14.5节listen Backlog Queue中所描述的方案。

需要注意的是,W. Richard Stevens解释说BSD的实现实际上确实是使用两个单独的队列,但是它们表现为一个单个队列,其最大长度固定且由(但不是必须完全等于)backlog参数确定,即BSD逻辑上如方案1所述。

Linux上的情况有些不同,listen的man手册写到:

The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length forcompletely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog
TCP套接字上的backlog参数的行为随Linux 2.2而改变。 现在它指等待被接受(accept)的、完全建立的套接字的队列长度,而不是不完整的连接请求数。 不完整套接字队列的最大长度可以通过/proc/sys/net/ipv4/tcp_max_syn_backlog设置

这意味着当前Linux版本采用的是具有两个不同队列的方案二:一个SYN队列,大小由系统范围的设置指定;一个accept队列,大小由应用程序指定。 方案2一个有趣的问题是,如果当前accept队列已满,而一个连接需要从SYN队列中移到accept队列中,这个时候TCP实现将如何处理?这种情况由net/ipv4/tcp_minisocks.c中的tcp_check_req函数处理。 相关代码如下:

child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
if (child == NULL)
goto listen_overflow;

对于IPV4,第一行代码实际会调用net/ipv4/tcp_ipv4.c中的tcp_v4_syn_recv_sock,其中包含如下代码:

if (sk_acceptq_is_full(sk))
goto exit_overflow;

上面的代码中可以看到对accept队列的检查。exit_overflow标签之后的代码将执行一些清理工作,更新/proc/net/netstat中的ListenOverflows和ListenDrops统计信息,然后返回NULL,这将触发执行tcp_check_req中的listen_overflow代码:

listen_overflow:
if (!sysctl_tcp_abort_on_overflow) {
inet_rsk(req)->acked = 1;
return NULL;
}

这意味着除非/proc/sys/net/ipv4/tcp_abort_on_overflow设置为1(这种情况下将如代码所示发送RST数据包),否则TCP实现基本上不做任何事情!

总而言之,如果Linux中的(服务端)TCP实现接收到(客户端)三次握手的ACK数据包,并且accept队列已满,则(服务端)基本上将忽略该数据包。这种处理方式刚听起来可能有点奇怪,但是请记住,SYN RECEIVED状态有一个关联定时器:如果服务端没有收到ACK(或者像这里所说的被忽略),则TCP实现将重新发送 SYN/ACK数据包(重试次数由/proc/sys /net/ipv4/tcp_synack_retries指定,并使用指数退避算法)。

上述现象可以在以下数据包跟踪中看到,客户端尝试连接(并发送数据)到已达到其最大backlog的套接字:

0.000 127.0.0.1 -> 127.0.0.1 TCP 74 53302 > 9999 [SYN] Seq=0 Len=0
0.000 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
0.000 127.0.0.1 -> 127.0.0.1 TCP 66 53302 > 9999 [ACK] Seq=1 Ack=1 Len=0
0.000 127.0.0.1 -> 127.0.0.1 TCP 71 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
0.207 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
0.623 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
1.199 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
1.199 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 6#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
1.455 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
3.123 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
3.399 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
3.399 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 10#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
6.459 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
7.599 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
7.599 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 13#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
13.131 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
15.599 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
15.599 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 16#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
26.491 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
31.599 127.0.0.1 -> 127.0.0.1 TCP 74 9999 > 53302 [SYN, ACK] Seq=0 Ack=1 Len=0
31.599 127.0.0.1 -> 127.0.0.1 TCP 66 [TCP Dup ACK 19#1] 53302 > 9999 [ACK] Seq=6 Ack=1 Len=0
53.179 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
106.491 127.0.0.1 -> 127.0.0.1 TCP 71 [TCP Retransmission] 53302 > 9999 [PSH, ACK] Seq=1 Ack=1 Len=5
106.491 127.0.0.1 -> 127.0.0.1 TCP 54 9999 > 53302 [RST] Seq=1 Len=0

客户端TCP实现由于收到多个SYN/ACK数据包,会假定其发送的ACK数据包丢失,从而重新发送ACK(参见上述跟踪中的TCP Dup ACK行)。
如果服务器端的应用程序在达到SYN/ACK最大重试次数之前减少了backlog(即从accept队列中消耗了一个条目),则TCP实现最终将会处理一个客户端重复发送的ACK,将连接状态从SYN RECEIVED转换到ESTABLISHED,并将连接添加到accept队列。否则的话,客户端最终会收到一个RST数据包(如上图所示)。

数据包跟踪同时也展示了上述行为另一个有趣的一面。从客户端的角度来说,TCP连接将在收到第一个SYN/ACK数据包之后变为ESTABLISHED状态。如果客户端向服务端发送数据(不等待来自服务端的数据),则该数据也会被重传。幸运的是,TCP的慢启动可以限制重传阶段发送的数据段个数。

另一方面,如果客户端一直在等待来自服务端的数据,而服务端的backlog一直没有降低,则最终的结果是客户端的连接状态是ESTABLISHED,而服务端的连接状态则是SYN_RCVD(注:原文说的是CLOSED状态,应该是不对的),也就是处于一个半连接的状态!

还有另一个方面我们目前没有讨论。listen的man手册引用表明,除非SYN队列已满,否则每个SYN数据包都将导致TCP连接被添加到SYN队列中,这种说法和实际情况有所出入,原因在于net/ipv4/tcp_ipv4.c中的tcp_v4_conn_request函数存在以下一段代码:

/* Accept backlog is full. If we have already queued enough
* of warm entries in syn queue, drop request. It is better than
* clogging syn queue with openreqs with exponentially increasing
* timeout.
*/
if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) {
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}

上面的代码意味着,如果当前accept队列已满,内核会对接收SYN数据包的速率施加限制。如果收到太多SYN数据包其中一些将会被丢弃,这将导致客户端重试发送SYN数据包,从而最终得到与BSD派生实现中相同的行为。

最后看看为什么Linux的设计选择会优于传统的BSD实现。 Stevens提出了以下有趣的观点:

The backlog can be reached if the completed connection queue fills (i.e., the server process or the server host is so busy that the process cannot call accept fast enough to take the completed entries off the queue) or if the incomplete connection queue fills. The latter is the problem that HTTP servers face, when the round-trip time between the client and server is long, compared to the arrival rate of new connection requests, because a new SYN occupies an entry on this queue for one round-trip time. […]
The completed connection queue is almost always empty because when an entry is placed on this queue, the server’s call to accept returns, and the server takes the completed connection off the queue.

Stevens提出的解决方案只是增加backlog。这样做的问题在于它假定如果应用程序希望调整backlog,不仅要考虑如何处理新建立的传入连接,还有考虑诸如往返时间等流量特性。Linux中的实现有效地分离了这两个问题:应用程序只负责调整backlog,使其可以足够快地接收(accept)连接,避免填满accept队列; 系统管理员则可以根据流量特性调整/proc/sys/net/ipv4/tcp_max_syn_backlog

【翻译】TCP backlog在Linux中的工作原理的更多相关文章

  1. Linux中Postfix邮件原理介绍(一)

    邮件相关协议 SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议, 工作在TCP的25端口.它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式 ...

  2. php在web服务器中的工作原理

    1.web工作原理 我是学习PHP网站建设的,那么网站在客户端和服务端的运行是网站运行的根本所在,那个这个运行过程是怎样的呢?我们一探就将! Web:终端 服务器web:我们把提供(响应)服务的计算机 ...

  3. Spring MVC中DispatcherServlet工作原理探究

    转:http://blog.csdn.net/zhouyuqwert/article/details/6853730 下面类图将主要的类及方法抽离出来,以便查看方便,根据类的结构来说明整个请求是如何工 ...

  4. Java 8 中 ConcurrentHashMap工作原理的要点分析

    简介: 本文主要介绍Java8中的并发容器ConcurrentHashMap的工作原理,和其它文章不同的是,本文重点分析了对不同线程的各类并发操作如get,put,remove之间是如何同步的,以及这 ...

  5. Java8 中 ConcurrentHashMap工作原理的要点分析

    简介: 本文主要介绍Java8中的并发容器ConcurrentHashMap的工作原理,和其它文章不同的是,本文重点分析了不同线程的各类并发操作如get,put,remove之间是如何同步的,以及这些 ...

  6. linux中内存使用原理

    首先介绍一下linux中内存是如何使用的. 当有应用需要读写磁盘数据时,由系统把相关数据从磁盘读取到内存,如果物理内存不够,则把内存中的部分数据导入到磁盘,从而把磁盘的部分空间当作虚拟内存 来使用,也 ...

  7. HADOOP1.X中HDFS工作原理

    转载自:http://www.daniubiji.cn/archives/596 HDFS(Hadoop Distributed File System )Hadoop分布式文件系统.是根据googl ...

  8. Hadoop中HDFS工作原理

    转自:http://blog.csdn.net/sdlyjzh/article/details/28876385 Hadoop其实并不是一个产品,而是一些独立模块的组合.主要有分布式文件系统HDFS和 ...

  9. js中Ajax工作原理(转)

    在写这篇文章之前,曾经写过一篇关于AJAX技术的随笔,不过涉及到的方面很窄,对AJAX技术的背景.原理.优缺点等各个方面都很少涉及null.这次写这篇文章的背景是因为公司需要对内部程序员做一个培训.项 ...

随机推荐

  1. mybatis二级缓存应用及与ehcache整合

    mybaits的二级缓存是mapper范围级别,除了在SqlMapConfig.xml设置二级缓存的总开关,还要在具体的mapper.xml中开启二级缓存. 1.开启mybatis的二级缓存 在核心配 ...

  2. SPI、IIC、IIS、UART、CAN、SDIO、GPIO、USB总线协议

    SPI.IIC.IIS.UART.CAN.SDIO.GPIO总线协议 SPI(Serial Peripheral Interface:串行外设接口)SPI总线由三条信号线组成:串行时钟(SCLK).串 ...

  3. tomcat6和tomcat7管理用户manager配置

    tomcat用户登录文件配置 如果想要对部署在tomcat上的项目进行管理查看,需要在tomcat安装目录conf文件夹下的tomcat-user.xml里添加用户登录权限.具体添加的内容如下: To ...

  4. 【一通百通】c/php的printf总结

    程序语言都是触类旁通的,讲人话就是[一通百通].so今天说说工作中常用的printf的用法吧. 1.先说说PHP printf()函数: printf()函数的调用格式为: printf(" ...

  5. AS 中 Plugin for Gradle 和 Gradle 之间的版本对应关系

    Plugin for Gradle 和 Gradle 之间的版本对应关系 来源:https://developer.android.com/studio/releases/gradle-plugin. ...

  6. Oracle 网络配置与管理

    [学习目标] Oracle 监听器是一个服务器端程序,用于监听所有来自客户端的请求,并为其提供数 据库服务.因此对监听器的管理与维护相当重要.         本章主要内容是描述对Oracle 监听器 ...

  7. rownum和分析函数 over

    select rownum, t.* from qyuser.tr_apply_info t where rownum < 10; --rownum 对于满足 查询条件的结果,从1 开始,所以大 ...

  8. ssh批量执行命令-paramiko

    ---恢复内容开始--- # python3.5 + paramiko # pip 是python的包管理工具,在shell里执行如下命令安装paramoko模块 # pip install para ...

  9. 《剑指offer》-链表找环入口

    题目描述 一个链表中包含环,请找出该链表的环的入口结点. 初步想法是每个节点做几个标记,表示是否被访问过,那么遍历链表的时候就知道哪个被访问到了.但是不会实现. 另一个直觉是判断链表有环的算法中出现过 ...

  10. IEnumerable和IEnumerator接口

    我们先思考几个问题:1.为什么在foreach中不能修改item的值?(IEnumerator的Current为只读)2.要实现foreach需要满足什么条件?(实现IEnumerator接口来实现的 ...