神奇的backlog参数对TCP链接创建的影响

曾经有人问我套接字编程中 listen的第二个参数 backlog是什么意思?多大的值合适?我不假思索地回答它表示服务器能够接受的并发请求的最大值。然而事实真的是这样的吗?

tcp-state-diagram.png

TCP经过三次握手创建链接的过程应该都不陌生了。从服务器的角度看,它分为如下几步html

  1. TCP状态设置为LISTEN状态,开启监听客户端的链接请求
  2. 收到客户端发送的SYN报文后,TCP状态切换为SYN RECEIVED,并发送SYN ACK报文
  3. 收到客户端发送的ACK报文后,TCP三次握手完成,状态切换为ESTABLISHED

Unix系统中,开启监听是经过listen完成。linux

int listen(int sockfd, int backlog)

listen有两个参数,第一个参数sockfd表示要设置的套接字,本文主要关注的是其第二个参数backloggit

<Unix 网络编程>将其描述为已完成的链接队列(ESTABLISHED)与未完成链接队列(SYN_RCVD)之和的上限。编程

通常咱们将ESTABLISHED状态的链接称为全链接,而将SYN_RCVD状态的链接称为半链接ubuntu

图片描述

当服务器收到一个SYN后,它建立一个子链接加入到SYN_RCVD队列。在收到ACK后,它将这个子链接移动到ESTABLISHED队列。最后当用户调用accept()时,会将链接从ESTABLISHED队列取出。小程序


是 Posix 不是 TCP

listen只是posix标准,不是TCP的标准!不是TCP标准就意味着不一样的内核能够有本身独立的实现服务器

POSIX是这么说的:cookie

The backlog argument provides a hint to the implementation which the implementation shall use to limit the number of outstanding connections in the socket's listen queue.

Linux是什么行为呢 ? 查看listenman page网络

The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests.

什么意思呢?就是说的在Linux 2.2之后, backlog只限制完成了三次握手,处于ESTABLISHED状态等待accept的子链接的数目了。并发

真的是这样吗?因而我决定抄一个小程序验证一下:

服务器监听50001端口,而且设置backlog = 4。注意,我为了将队列塞满,没有调用accept

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define BACKLOG 4

int main(int argc, char **argv)
{
    int listenfd;
    int connfd;
    struct sockaddr_in servaddr;

    listenfd = socket(PF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(50001);

    bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    listen(listenfd, BACKLOG);
    while(1)
    {
        sleep(1);
    }
    
    return 0;
}

客户端的代码

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    int sockfd;
    struct sockaddr_in servaddr;

    sockfd = socket(PF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(50001);
    servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");

    if (0 != connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)))
    {
         printf("connect failed!\n");
    }
    else
    {
         printf("connect succeed!\n");
    }

    sleep(30);

    return 1;
}

为了排除syncookie的干扰,我首先关闭了syncookie功能

echo 0 > /proc/sys/net/ipv4/tcp_syncookies

因为我设置的backlog = 4而且服务器始终不会accept。所以预期会创建 4 个全链接, 但实际倒是

root@ubuntu-1:/home/user1/workspace/client# ./client &
[1] 12798
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[2] 12799
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[3] 12800
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[4] 12801
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[5] 12802
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[6] 12803
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[7] 12804
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[8] 12805
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[9] 12806
root@ubuntu-1:/home/user1/workspace/client# connect succeed!
./client &
[10] 12807
root@ubuntu-1:/home/user1/workspace/client# connect failed!

看!客户器居然显示成功创建了 9 次链接!

netstat看看TCP链接状态

> netstat -t
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State
tcp        0      0 localhost:50001         localhost:55792         ESTABLISHED
tcp        0      0 localhost:55792         localhost:50001         ESTABLISHED
tcp        0      0 localhost:55798         localhost:50001         ESTABLISHED   
tcp        0      1 localhost:55806         localhost:50001         SYN_SENT
tcp        0      0 localhost:50001         localhost:55784         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55794         SYN_RECV
tcp        0      0 localhost:55786         localhost:50001         ESTABLISHED
tcp        0      0 localhost:55800         localhost:50001         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55786         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55800         SYN_RECV
tcp        0      0 localhost:55784         localhost:50001         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55796         SYN_RECV
tcp        0      0 localhost:50001         localhost:55788         ESTABLISHED
tcp        0      0 localhost:55794         localhost:50001         ESTABLISHED
tcp        0      0 localhost:55788         localhost:50001         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55790         ESTABLISHED
tcp        0      0 localhost:50001         localhost:55798         SYN_RECV
tcp        0      0 localhost:55790         localhost:50001         ESTABLISHED
tcp        0      0 localhost:55796         localhost:50001         ESTABLISHED

整理一下就是下面这样
图片描述

从上面能够看出,一共有5条链接对是ESTABLISHED<->ESTABLISHED链接, 但还有4条链接对是SYN_RECV<->ESTABLISHED链接, 这表示对客户端三次握手已经完成了,但对服务器尚未! 回顾一下TCP三次握手的过程,形成这种链接对缘由只有多是服务器客户端最后发送的握手ACK被丢弃了!

还有一个问题,我明明设置的backlog的值是 4,可为何还能创建5个链接 ?!


去内核找缘由

我实验用的机器内核是 4.4.0

前面提到过已完成链接队列未完成链接队列这两个概念, Linux有这两个队列吗 ? Linux 既有又没有! 说有是由于内核中能够获得两种链接各自的长度; 说没有是由于 Linux只有已完成链接队列实际存在, 而未完成链接队列只有长度的记录!

每个LISTEN状态的套接字都有一个struct inet_connection_sock结构, 其中的accept_queue从名字上也能够看出就是已完成三次握手的子链接队列.只是这个结构里还记录了半链接请求的长度!

struct inet_connection_sock {    
    // code omitted 
    struct request_sock_queue icsk_accept_queue;
    // code omitted
}

struct request_sock_queue {
    // code omitted
    atomic_t        qlen;                // 半链接的长度
    atomic_t        young;               // 通常状况, 这个值 = qlen

    struct request_sock    *rskq_accept_head;  // 已完成链接的队列头
    struct request_sock    *rskq_accept_tail;  // 已完成链接的队列尾
    // code omitted
};

因此通常状况下链接创建时,服务端的变化过程是这样的:

  1. 收到SYN报文, qlen++,young++
  2. 收到ACK报文, 三次握手完成,将链接加入accept队列,qlen--,young--
  3. 用户使用accept,将链接从accept取出.

再来看内核收到SYN握手报文时的处理, 因为我关闭了syncookie,因此一旦知足了下面代码中的两个条件之一就会丢弃报文

int tcp_conn_request(struct request_sock_ops *rsk_ops, 
             const struct tcp_request_sock_ops *af_ops,
             struct sock *sk, struct sk_buff *skb) 

    if ((net->ipv4.sysctl_tcp_syncookies == 2 ||
         inet_csk_reqsk_queue_is_full(sk)) && !isn) {   // 条件1: 半链接 >= backlog
        want_cookie = tcp_syn_flood_action(sk, skb, rsk_ops->slab_name);
        if (!want_cookie)
            goto drop;
    } 

    if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) { // 条件2: 全链接 > backlog 而且 半链接 > 1
        NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
        goto drop;
    } 

    // code omitted

下面是收到ACK握手报文时的处理

struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb,
                  struct request_sock *req,
                  struct dst_entry *dst,
                  struct request_sock *req_unhash,
                  bool *own_req) 
{
     // code omitted
     if (sk_acceptq_is_full(sk))           //  全链接 > backlog, 就丢弃
         goto exit_overflow;

     newsk = tcp_create_openreq_child(sk, req, skb); // 建立子套接字了
     // code omitted
}

因此这样就能够解释实验现象了!

  1. 4个链接请求均可以顺利建立子链接, 全链接队列长度 = backlog = 4, 半链接数目 = 0
  2. 5个链接请求, 因为sk_acceptq_is_full的判断条件是>而不是>=,因此依然能够创建全链接
  3. 6-9个链接请求到来时,因为半链接的数目尚未超过backlog,因此仍是能够继续回复SYNACK,但收到ACK后已经不能再建立子套接字了,因此TCP状态依然为SYN_RECV.同时半链接的数目也增长到backlog.而对于客户端,它既然能收到SYNACK握手报文,所以它能够将TCP状态变为ESTABLISHED,
  4. 10个请求到来时, 因为半链接的数目已经达到backlog,所以,这个SYN报文会被丢弃.

内核的问题

从以上的现象和分析中,我认为内核存在如下问题

  1. accept队列是否满的判断用>=>更合适, 这样才能体现backlog的做用
  2. accept队列满了,就应该拒绝半链接了,由于即便半链接握手完成,也没法加入accept队列,不然就会出现SYN_RECV--ESTABLISHED这样状态的链接对!这样的链接是不能进行数据传输的!

问题2在16年的补丁中已经修改了! 因此若是你在更新版本的内核中进行相同的实验, 会发现客户端只能链接成功5次了,固然这也要先关闭syncookie

但问题1尚未修改! 若是之后修改了,我也不会意外

(完)

REF

how-tcp-backlog-works-in-linux

相关文章
相关标签/搜索