TCP backlog在Linux下是如何工作的

原文链接: http://veithen.io/2014/01/01/how-tcp-backlog-works-in-linux.html

当一个应用使用listen syscall将一个socket变成LISTEN状态时,它需要为该socket指定一个backlog值。这个backlog值通常被认为是接收传入连接的队列的长度限制。

TCP state diagram

 

因为TCP采用三次握手,一个连接在到达可以被accept syscall返回的ESTABLISHED状态之前,还会经过一个中间状态,称为SYN RECEIVED(见上图)。所以TCP/IP协议栈可以有2种方式去实现LISTEN状态的socket的backlog队列。

  1. 一种实现是使用单独的队列,队列的长度由listen syscall的backlog参数决定。当收到一个SYN包的时候,TCP返回一个SYN/ACK包,并将连接放入队列中。当收到对应的ACK包时,连接状态改为ESTABLISHED,并可以转交给应用使用。这意味着这个队列中可能包含两种状态的连接:SYN RECEIVED和ESTABLISHED。只有处于后一种状态的连接才能被accept syscall返回给应用。

  2. 另一种实现是使用两个队列,一个SYN队列(或者叫未完成连接队列)和一个accept队列(或者叫已完成连接队列)。处于SYN RECEIVED状态的连接会被加入到SYN队列,并在他们的状态变为ESTABLISHED时(即收到三次握手中的最后一个ACK包的时候)移动到accept队列中。就像系统调用的名称所表示的那样,accept调用只需简单的消费accept队列中的连接就行了。在这种实现中,listen syscall的backlog参数决定了accept队列的大小。

历史上,BSD系的TCP实现使用了第一种方式。这种方式隐含的意义是当backlog满了之后,系统不会再回应SYN/ACK包给SYN了。通常情况下TCP的实现会简单得丢弃SYN包(而不是回复一个RST包),这样客户端就会进行重试。就如W.Richard Stevens的经典著作《TCP/IP详解》14.5节 listen Backlog里面写的一样。

值得注意的是Stevens解释了BSD的实现实际上是用了两个队列的,但他们表现得像一个由backlog参数决定的(但不一定严格相等)具有固定大小的单一队列。也就是说BSD实现逻辑上采用了第一种方式:

队列的长度限制为未完成连接队列的条目数加上已完成连接队列的条目数的和

在Linux中,情况就不一样了,就如在listen syscall的man page中提到的:

TCP socket的backlog参数的行为在Linux2.2中有所改变。现在它指的是已完成连接队列的长度,而不再是未完成连接队列的长度了。未完成连接队列的长度可以用/proc/sys/net/ipv4/tcp_max_syn_backlog指定

这意味着目前的Linux版本采用了两个单独队列的方式:一个长度由系统级别设置指定的SYN队列和一个长度由应用指定的accept队列。

一个有意思的问题是,当accept队列满了,如果有连接需要从SYN队列移到accept队列(也就是当收到三次握手的最后一个ACK包的时候),这种实现方式会怎么做呢? Linux代码中,这类情况由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;

在这里我们可以看到对已接受队列的检查,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包),这种实现基本上。。。啥都没做!

总结一下,如果Linux中的TCP实现收到了ACK包,但accept队列已经满了的话,它会简单的忽略这个包。这种做法初看会觉得有点奇怪,但记住SYN RECEIVED状态是有一个定时器的:如果ACK包没有收到(或者像这种场景下被忽略了),TCP实现会重发SYN/ACK包(重试次数由/proc/sys/net/ipv4/tcp_synack_retries决定,并且使用指数退避算法)。

下面是一个客户端尝试连接(并发送数据)到一个已经达到最大backlog值的socket的时候的包跟踪日志,它可以证实上述的行为:

  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队列的大小(消费了已完成连接队列中的一个或多个连接),TCP最终能处理某一个重发的ACK,把连接从SYN RECEIVED变为ESTABLISHED状态,并移动到accept队列。否则的话,客户端最终会发送一个RST包(如上例所示)。

上面的日志还显示了另外一个有趣的行为。从客户端的角度来看,当收到第一个SYN/ACK时,连接会处于ESTABLISHED状态。如果它发送数据的话(不需要先等待服务端的数据的时候),数据也会被重传。好消息是TCP 慢开始 特性会限制在这个阶段传输的数据段的数量。

另一方面来说,如果客户端需要先等待服务端发来的数据而服务端又无法减少backlog,那最终的结果就是客户端连接处于ESTABLISHED状态,但在服务端认为连接处于CLOSED状态。这使我们得到了一个半开的连接

这里还有一点我们没有讨论到的。listen的man page中说每个SYN包会产生SYN队列中的一个新的连接(除非队列已经满了)。但其实并不完全是这样。原因在net/ipv4/tcp_ipv4.c的tcp_v4_conn_request函数中(该函数用来处理SYN包),代码如下:

        /* 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提出了以下有趣的观点:

到达backlog的设定值既有可能因为已完成连接队列的累积(也就是说服务端进程或者主机太忙了,导致他们调用accept的速度赶不上连接进入accept队列的速度),也有可能是因为未完成连接队列的累积导致。后一种情况在HTTP服务器中经常会遇到,因为每次客户端的调用会产生一个的SYN,进而占用未完成连接队列中的一个条目,当服务端处理与响应的速度比请求达到的速度慢的时候,未连接队列就会累积起来了。

已完成连接队列基本总是空的,因为当一个条目放到这个队列的时候,服务器对accept的调用就返回了,然后服务器就会从队列中拿走这个连接

Stevens的建议是简单地增加backlog的值来解决这个场景。但问题是这样做需要应用在调整backlog时不仅仅考虑他如何处理新建的连接,还要把诸如往返时间之类的数据流量的特性考虑进去。Linux的实现有效的隔离了这两个关注点:应用只需要负责调整backlog参数以保证它能足够快速的调用accept(避免已完成连接队列的累积)。而操作系统管理员可以根据流量特性调整/proc/sys/net/ipv4/tcp_max_syn_backlog参数。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值