深入探索 Linux listen() 函数 backlog 的含义

本文深入解析listen()函数的backlog参数含义,通过理论分析与实验验证,明确backlog与连接队列的关系及其在Linux系统中的具体表现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1:listen()回顾以及问题引入
2:正确的解释
3:实验验证

1:listen()回顾以及问题引入

listen()函数是网络编程中用来使服务器端开始监听端口的系统调用,首先来回顾下listen()函数的定义:

这里写图片描述

有关于第二个参数含义的问题网上有好几种说法,我总结了下主要有这么3种:

  1. Kernel会为LISTEN状态的socket维护一个队列,其中存放SYN RECEIVEDESTABLISHED状态的套接字,backlog就是这个队列的大小。
  2. Kernel会为LISTEN状态的socket维护两个队列,一个是SYN RECEIVED状态,另一个是ESTABLISHED状态,而backlog就是这两个队列的大小之和。
  3. 第三种和第二种模型一样,但是backlog是队列ESTABLISHED的长度。

有关上面说的两个状态SYN RECEIVED状态和ESTABLISHED状态,是TCP三次握手过程中的状态转化,具体可以参考下面的图(在新窗口打开图片):

这里写图片描述

2:正确的解释

那上面三种说法到底哪个是正确的呢?我下面的说法翻译自这个链接:

http://veithen.github.io/2014/01/01/how-tcp-backlog-works-in-linux.html

下面我翻译下作者的文章:

When an application puts a socket into LISTEN state using the listen syscall, it needs to specify a backlog for that socket. The backlog is usually described as the limit for the queue of incoming connections.

当一个应用使用listen系统调用让socket进入LISTEN状态时,它需要为该套接字指定一个backlogbacklog通常被描述为连接队列的限制。

Because of the 3-way handshake used by TCP, an incoming connection goes through an intermediate state SYN RECEIVED before it reaches the ESTABLISHED state and can be returned by the accept syscall to the application (see the part of the TCP state diagram reproduced above). This means that a TCP/IP stack has two options to implement the backlog queue for a socket in LISTEN state:

由于TCP使用的3次握手,连接在到达ESTABLISHED状态之前经历中间状态SYN RECEIVED,并且可以由accept系统调用返回到应用程序。这意味着TCP / IP堆栈有两个选择来为LISTEN状态的套接字实现backlog队列:

(备注:一种就是两种状态在一个队列,一种是分别在一个队列)

1 : The implementation uses a single queue, the size of which is determined by the backlog argument of the listen syscall. When a SYN packet is received, it sends back a SYN/ACK packet and adds the connection to the queue. When the corresponding ACK is received, the connection changes its state to ESTABLISHED and becomes eligible for handover to the application. This means that the queue can contain connections in two different state: SYN RECEIVED and ESTABLISHED. Only connections in the latter state can be returned to the application by the accept syscall.

1:使用单个队列实现,其大小由listen syscallbacklog参数确定。 当收到SYN数据包时,它发送回SYN/ACK数据包,并将连接添加到队列。 当接收到相应的ACK时,连接将其状态改变为已建立。 这意味着队列可以包含两种不同状态的连接:SYN RECEIVEDESTABLISHED。 只有处于后一状态的连接才能通过accept syscall返回给应用程序。

2 : The implementation uses two queues, a SYN queue (or incomplete connection queue) and an accept queue (or complete connection queue). Connections in state SYN RECEIVED are added to the SYN queue and later moved to the accept queue when their state changes to ESTABLISHED, i.e. when the ACK packet in the 3-way handshake is received. As the name implies, the accept call is then implemented simply to consume connections from the accept queue. In this case, the backlog argument of the listen syscall determines the size of the accept queue.

2 : 使用两个队列实现,一个SYN队列(或半连接队列)和一个accept队列(或完整的连接队列)。 处于SYN RECEIVED状态的连接被添加到SYN队列,并且当它们的状态改变为ESTABLISHED时,即当接收到3次握手中的ACK分组时,将它们移动到accept队列。 显而易见,accept系统调用只是简单地从完成队列中取出连接。 在这种情况下,listen syscall的backlog参数表示完成队列的大小。

Historically, BSD derived TCP implementations use the first approach. That choice implies that when the maximum backlog is reached, the system will no longer send back SYN/ACK packets in response to SYN packets. Usually the TCP implementation will simply drop the SYN packet (instead of responding with a RST packet) so that the client will retry.

历史上,BSD 派生系统实现的TCP使用第一种方法。 该选择意味着当达到最大backlog时,系统将不再响应于SYN分组发送回SYN/ACK分组。 通常,TCP的实现将简单地丢弃SYN分组,使得客户端重试。

On Linux, things are different, as mentioned in the man page of the listen syscall:
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. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog.

Linux上,是和上面不同的。如在listen系统调用的手册中所提到的:
在Linux内核2.2之后,socket backlog参数的形为改变了,现在它指等待accept完全建立的套接字的队列长度,而不是不完全连接请求的数量。 不完全连接的长度可以使用/proc/sys/net/ipv4/tcp_max_syn_backlog设置。

This means that current Linux versions use the second option with two distinct queues: a SYN queue with a size specified by a system wide setting and an accept queue with a size specified by the application.

这意味着当前Linux版本使用上面第二种说法,有两个队列:具有由系统范围设置指定的大小的SYN队列 和 应用程序(也就是backlog参数)指定的accept队列。

OK,说到这里,相信backlog含义已经解释的非常清楚了,下面我们用实验验证下这种说法:

3:实验验证

验证环境:

RedHat 7
Linux version 3.10.0-514.el7.x86_64

验证思路:

1:客户端开多个线程分别创建socket去连接服务端。
2:服务端在listen之后,不去调用accept,也就是不会从已完成队列中取走socket连接。
3:观察结果,到底服务端会怎么样?处于ESTABLISHED状态的套接字个数是不是就是backlog参数指定的大小呢?

我们定义backlog的大小为5:

# define BACKLOG 5

看下我系统上默认的SYN队列大小:

这里写图片描述

也就是我现在两个队列的大小分别是 :

SYN队列大小:256
ACCEPT队列大小:5

看看我们的服务端程序 server.c :

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

#define PORT  8888    //端口号
#define BACKLOG 5     //BACKLOG大小

void my_err(const char* msg,int line) 
{
    fprintf(stderr,"line:%d",line);
    perror(msg);
}


int main(int argc,char *argv[])
{
    int conn_len;
    int sock_fd,conn_fd;
    struct sockaddr_in serv_addr,conn_addr;


    if((sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1) { 
        my_err("socket",__LINE__); 
        exit(1);
    }

    memset(&serv_addr,0,sizeof(struct sockaddr_in));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);


    if(bind(sock_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) {
        my_err("bind",__LINE__);
        exit(1);
    }

    if(listen(sock_fd,BACKLOG) == -1) {
        my_err("sock",__LINE__);
        exit(1);
    }

    conn_len = sizeof(struct sockaddr_in);


    sleep(10);                  //sleep 10s之后接受一个连接
    printf("I will accept one\n");
    accept(sock_fd,(struct sockaddr *)&conn_addr,(socklen_t *)&conn_len);

    sleep(10);                  //同理,再接受一个
    printf("I will accept one\n");
    accept(sock_fd,(struct sockaddr *)&conn_addr,(socklen_t *)&conn_len);

    sleep(10);                  //同理,再次接受一个
    printf("I will accept one\n");
    accept(sock_fd,(struct sockaddr *)&conn_addr,(socklen_t *)&conn_len);


    while(1) {}  //之后进入while循环,不释放连接
    return 0;
}

客户端程序 client.c

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

#define PORT 8888
#define thread_num 10  //定义创建的线程数量

struct sockaddr_in serv_addr;

void *func() 
{
    int conn_fd;
    conn_fd = socket(AF_INET,SOCK_STREAM,0);
    printf("conn_fd : %d\n",conn_fd);

    if( connect(conn_fd,(struct sockaddr *)&serv_addr,sizeof(struct sockaddr_in)) == -1) {
        printf("connect error\n");
    }

    while(1) {}
}

int main(int argc,char *argv[])
{
    memset(&serv_addr,0,sizeof(struct sockaddr_in));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    inet_aton("192.168.30.155",(struct in_addr *)&serv_addr.sin_addr); //此IP是局域网中的另一台主机
    int retval;

    //创建线程并且等待线程完成
    pthread_t pid[thread_num];
    for(int i = 0 ; i < thread_num; ++i)
    {
        pthread_create(&pid[i],NULL,&func,NULL);

    }

    for(int i = 0 ; i < thread_num; ++i)
    {
        pthread_join(pid[i],(void*)&retval);
    }

    return 0;
}

编译运行程序,并用netstat命令监控服务端8888端口的情况:

$ gcc server.c -o server

$ gcc client.c -o client -lpthread -std=c99

# watch -n 1 “netstat -natp | grep 8888” //root执行
//watch -n 1 表示每秒显示一次引号中命令的结果
//netstat n: 以数字化显示 a:all t:tcp p:显示pid和进程名字 然后我们grep端口号8888就行了。

$ ./server

$ ./client

结果如下:

首先是watch的情况:

这里写图片描述

  • 因为我们客户端用10个线程去连接服务器,因此服务器上有10条连接。
  • 第一行的./server状态是LISTEN,这是服务器进程。
  • 倒数第三行的./server是服务器已经执行了一次accept
  • 6条ESTABLISHED状态比我们的BACKLOG参数5大1。
  • 剩余的SYN_RECV状态即使收到了客户端第三次握手回应的ACK也不能成为ESTABLISHED状态,因为BACKLOG队列中没有位置。

然后过了10s左右,等到服务器执行了第二个accept之后,服务器情况如下,它执行了第二个accept

这里写图片描述

此时watch监控的画面如下:

这里写图片描述

  • 和上面相比,服务器再次accept之后,多了一条./server的连接。
  • 有一条连接从SYN_RECV状态转换到了ESTABLISHED状态,原因是accept函数从BACKlOG完成的队列中取出了一个连接,接着有空间之后,SYN队列的一个链接就可以转换成ESTABLISHED状态然后放入BACKlOG完成队列了。

好了,分析到这里,有关BACKLOG的问题已经解决了,至于继续上面的实验将backlog的参数调大会怎么样呢?我试过了,就是ESTABLISHED状态的数量也会增大,值会是BACKLOG+1至于为什么是BACKLOG+1呢???我也没有搞懂。欢迎指教。

当然,还有别的有意思的问题是 : 如果ESTABLISHED队列满了,可是有连接需要从SYN队列转移过来时会发生什么?

一边在喊:让我过来!我满足条件了。
一边淡淡的说:过个毛啊,没看没地方‘ 住 ‘ 吗?~

改天再细说吧,欢迎评论交流~

<think>嗯,用户问的是listen函数中的backlog参数的含义。首先,我得回忆一下网络编程中的相关知识。记得listen函数是在服务器端调用的,用于让套接字进入监听状态,等待客户端的连接请求。而backlog参数应该和连接队列有关。 那backlog具体控制什么呢?可能需要详细解释一下TCP三次握手的过程。当客户端发起SYN包,服务器接收到后会将连接放入未完成队列(半连接队列),然后回复SYN-ACK。接着客户端发送ACK,完成三次握手,连接转入已完成队列(全连接队列)。backlog参数应该和这个已完成队列的长度有关。 不过不同操作系统可能有不同的实现方式。比如,Linux系统中,backlog指的是已完成队列的最大长度,而半连接队列的长度则由其他参数控制。而在一些旧系统里,可能两个队列的总和不能超过backlog。这点需要说明清楚,避免用户误解。 还要注意,如果已完成队列满了,服务器可能会忽略新的ACK,导致客户端重传,这会影响连接建立的效率。所以设置合适的backlog值对服务器性能很重要。通常的建议值比如Nginx使用511,可能和系统的最大限制有关,比如Linux中somaxconn默认是128,但可以调整。 另外,需要提醒用户,即使设置了较大的backlog,也要检查系统级别的参数,比如somaxconn,否则可能被截断。比如设置backlog为511,但系统限制是128,实际生效的是128。这时候需要修改系统参数才能生效。 总结一下,backlog参数的作用是定义已完成连接队列的最大长度,影响服务器处理并发连接的能力,但实际效果受操作系统实现和系统参数的限制。需要根据实际情况调整,并考虑系统配置。</think>在TCP网络编程中,`listen()`函数的`backlog`参数用于控制服务器端等待处理的连接队列长度。以下是逐步解析: 1. **基本定义** 调用`listen(int sockfd, int backlog)`后,套接字从CLOSED状态变为LISTEN状态。此时操作系统会维护两个队列: - **未完成连接队列(半连接队列)** 存储已完成SYN接收但未完成三次握手的连接(SYN_RCVD状态) - **已完成连接队列(全连接队列)** 存储已完成三次握手的连接(ESTABLISHED状态) 2. **backlog的具体作用** `backlog`参数**严格限制已完成连接队列的最大长度**(不同系统实现可能不同): - 当新连接完成三次握手时,会进入该队列 - 当队列已满时,新连接会被内核忽略,客户端可能重发ACK - 实际可用队列长度取`min(backlog, somaxconn)`,其中`somaxconn`是系统级参数 3. **操作系统差异示例** - **Linux** `backlog`仅控制已完成队列,半连接队列长度由`/proc/sys/net/ipv4/tcp_max_syn_backlog`控制 - **传统BSD实现** `backlog`表示两个队列的总和上限 4. **典型问题场景** - 若已完成队列满,客户端`connect()`可能返回`ECONNREFUSED`错误 - 高并发场景下需同时调整`backlog`和系统参数: ```bash # Linux修改已完成队列最大值 echo 1024 > /proc/sys/net/core/somaxconn ``` 5. **最佳实践建议** - Web服务器(如Nginx)通常设置`backlog=511` - 需配合负载均衡和连接复用机制 - 监控`ss -lnt`命令输出观察队列使用情况 关键公式:当瞬时并发连接数为$C$时,合理`backlog`应满足 $$ backlog \geq \frac{C \cdot RTT}{Timeout} $$ 其中$RTT$为往返时间,$Timeout$是连接超时阈值。
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

杨博东的博客

请我喝瓶可乐鼓励下~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值