Linux协议栈accept和syn队列问题

本文详细解析了Linux TCP协议栈中accept队列的工作原理,包括半连接队列与accept队列的区别,以及如何处理连接请求。同时,通过一个实际案例分析了当accept队列满时可能产生的问题及解决方案。

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



分类: LINUX

环境:

       Client 通过tcp 连接server,server端只是listen,但是不调用accept。通过netstat –ant查看两端的连接情况。

server端listen,不调用accept。

client一直去connect server。

问题:

运行一段时间后,为什么server端的ESTABLISHED连接的个数基本是固定的129个,但是client端的ESTABLISHED连接的个数却在不断增加?

分析

Linux内核协议栈为一个tcp连接管理使用两个队列,一个是半链接队列(用来保存处于SYN_SENT和SYN_RECV状态的请求),一个是accpetd队列(用来保存处于established状态,但是应用层没有调用accept取走的请求)。

第一个队列的长度是/proc/sys/net/ipv4/tcp_max_syn_backlog,默认是1024。如果开启了syncookies,那么基本上没有限制。

第二个队列的长度是/proc/sys/net/core/somaxconn,默认是128,表示最多有129个established链接等待accept。(为什么是129?详见下面的附录1)。

现在假设acceptd队列已经达到129的情况:

client发送syn到server。client(SYN_SENT),server(SYN_RECV)

server端处理流程:tcp_v4_do_rcv--->tcp_rcv_state_process--->tcp_v4_conn_request

      if(sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_yong(sk)>1)

              goto drop;

inet_csk_reqsk_queue_yong(sk)的含义是请求队列中有多少个握手过程中没有重传过的段。

在第一次的时候,之前的握手过程都没有重传过,所以这个syn包server端会直接drop掉,之后client会重传syn,当inet_csk_reqsk_queue_yong(sk) < 1,那么这个syn被server端接受。server会回复synack给client。这样一来两边的状态就变为client(ESTABLISHED), server(SYN_SENT)

Client收到synack后回复ack给server。

server端处理流程: tcp_check_req--->syn_recv_sock-->tcp_v4_syn_recv_sock

        if(sk_acceptq_is_full(sk)

                 goto exit_overflow;

如果server端设置了sysctl_tcp_abort_on_overflow,那么server会发送rst给client,并删除掉这个链接;否则server端只是记录一下LINUX_MIB_LISTENOVERFLOWS(详见附录2),然后返回。默认情况下是不会设置的,server端只是标记连接请求块的acked标志,之后连接建立定时器,会遍历半连接表,重新发送synack,重复上面的过程(具体的函数是inet_csk_reqsk_queue_prune),如果重传次数超过synack重传的阀值(/proc/sys/net/ipv4/tcp_synack_retries),会把该连接从半连接链表中删除。

一次异常问题分析

Nginx通过FASTCGI协议连接cgi程序,出现cgi程序read读取socket内容的时候永远block。通过netstat查看,cgi程序所在的服务器上显示连接存在,但是nginx所在的服务器上显示不存在该连接。

下面是原始数据图:

我们从上面的数据流来分析一下:

出现问题的时候,cgi程序(tcp server端)处理非常慢,导致大量的连接请求放到accept队列,把accept队列阻塞。

148021 nginx(tcp client端) 连接cgi程序,发送syn

       此时server端accpet队列已满,并且inet_csk_reqsk_queue_yong(sk) > 1,server端直接丢弃该数据包

148840 client端等待3秒后,重传SYN

       此时server端状态与之前送变化,仍然丢弃该数据包

150163 client端又等待6秒后,重传SYN

                   此时server端accept队列仍然是满的,但是存在了重传握手的连接请求,server端接受连接请求,并发送synack给client端(150164)

150166 client端收到synack,标记本地连接为ESTABLISHED状态,给server端应答ack,connect系统调用完成。

                   Server收到ack后,尝试将连接放到accept队列,但是因为accept队列已满,所以只是标记连接为acked,并不会将连接移动到accept队列中,也不会为连接分配sendbuf和recvbuf等资源。

150167 client端的应用程序,检测到connect系统调用完成,开始向该连接发送数据。

                   Server端收到数据包,由于acept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

150225 client端由于没有收到刚才发送数据的ack,所以会重传刚才的数据包

150296 同上

150496 同上

150920 同上

151112 server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,重新发送synack给client端。

151113 client端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

151896 client端等待3秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

152579 server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synack给client端。

152581 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回

153455 client端等待3秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

155399  server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synack给client端。

155400 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

156468 client端等待几秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

161309  server端连接建立定时器生效,遍历半连接链表,发现刚才acked的连接,synack重传次数在阀值以内,重新发送synack给client端。

161310 cient端收到synack后,根据ack值,使用SACK算法,只重传最后一个ack内容。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

162884 client端等待几秒后,没有收到对应的ack,认为之前的数据包也丢失,所以重传之前的内容数据包。

                   Server端收到数据包,由于accept队列仍然是满的,所以server端处理也只是标记acked,然后返回。

164828 client端等待一段时间后,认为连接不可用,于是发送FIN、ACK给server端。Client端的状态变为FIN_WAIT1,等待一段时间后,client端将看不到该链接。

164829 server端收到ACK后,此时cgi程序处理完一个请求,从accept队列中取走一个连接,此时accept队列中有了空闲,server端将请求的连接放到accept队列中。

这样cgi所在的服务器上显示该链接是established的,但是nginx(client端)所在的服务器上已经没有该链接了。

之后,当cgi程序从accept队列中取到该连接后,调用read去读取sock中的内容,但是由于client端早就退出了,所以read就会block那里了。

问题解决

         或许你会认为在164829中,server端不应该建立连接,这是内核的bug。但是内核是按照RFC来实现的,在3次握手的过程中,是不会判断FIN标志位的,只会处理SYN、ACK、RST这三种标志位。

从应用层的角度来考虑解决问题的方法,那就是使用非阻塞的方式read,或者使用select超时方式read;亦或者nginx中关闭连接的时候使用RST方式,而不是FIN方式。

附录1

when I use linux TCP socket, and find there is a bug in function sk_acceptq_is_full():

When a new SYN comes, TCP module first checks its validation. If valid,send SYN,ACK to the client and add the sock 

to the syn hash table. 

Next time if received the valid ACK for SYN,ACK from the client. server will accept this connection and increase the

sk->sk_ack_backlog -- which is done in function tcp_check_req().

We check wether acceptq is full in function tcp_v4_syn_recv_sock().

Consider an example:

 After listen(sockfd, 1) system call, sk->sk_max_ack_backlog is set to

 As we know, sk->sk_ack_backlog is initialized to 0. Assuming accept() system call is not invoked now

1. 1st connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=0 sk->sk_max_ack_backlog=1, function return 0 accept this connection. Increase the sk->sk_ack_backlog

2. 2nd connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=1 sk->sk_max_ack_backlog=1, function return 0 accept this connection. Increase the sk->sk_ack_backlog
3. 3rd connection comes. invoke sk_acceptq_is_full(). sk->sk_ack_backlog=2 sk->sk_max_ack_backlog=1, function return 1. Refuse this connection.I think it has bugs. after listen system call. sk->sk_max_ack_backlog=1
but now it can accept 2 connections.

附录2

netstat -s

 cat /proc/net/netstat

最后感谢Tiger提供的测试数据

<think>嗯,用户这次的问题集中在lwIP协议中LISTEN状态PCB的存储机制,需要结合之前的讨论引用资料做系统性解释。用户似乎在进行嵌入式网络协议栈开发,可能正在调试服务端程序,对底层数据结构有浓厚兴趣。注意到用户引用的资料[1]明确划分了lwIP的四个PCB链表,其中tcp_listen_pcbs专门存放监听控制块。这个设计很精妙——比起Linux用完整PCB表示监听套接字,lwIP的tcp_pcb_listen结构体尺寸仅为标准PCB的1/5。让我算算具体数值:标准tcp_pcb约200字节(含窗口管理双缓冲池指针),而监听块仅保留local_port、backlog、accept回调等核心字段,按源码structtcp_pcb_listen确实在40字节左右。用户可能更关心实际工作流程。当应用调用tcp_listen()时[引用2],内核会完成三件事:将原始PCB转为轻量级监听块、加入全局链表、释放原PCB资源。此时若有SYN到达,tcp_input()会遍历tcp_listen_pcbs链表匹配目标端口,匹配成功则创建新标准PCB放入tcp_active_pcbs[引用1]。整个过程监听块本身恒定存在,就像个永不关闭的“注册入口”。特别要强调内存优化意义。假设某网关设备需同时监听10个端口,使用监听块可节省$$10\times(200-40)=1600$$字节内存,这对资源受限的嵌入式系统很关键。另外从算法复杂度看,查找监听块是$$O(n)$$操作,但因链表仅含监听项而非所有连接,实际效率远高于遍历活动连接池。用户或许会质疑backlog机制。资料[2]提到tcp_listen_with_backlog()的backlog参数在lwIP中实际控制半连接队列长度(SYN_RCVD状态数)。当新SYN到达时,内核会检查当前该监听端口下SYN_RCVD状态数是否超限,避免内存耗尽——这种设计比Linux更直接,但缺少SYNCookie等高级防护。最后生成的三个衍生问题都针对关键痛点:backlog机制差异涉及协议栈移植、REUSEPORT实现反映多进程服务需求、TIME_WAIT影响则直指高并发场景。特别是第五个裸机环境问题,用户若是开发RTOS设备,必须考虑中断安全——应建议用信号量保护链表操作。</think> 在lwIP协议中,处于`LISTEN`状态的PCB节点存储机制是实现TCP服务端的核心设计。以下是关键细节的系统性解析: --- ### 📌 **监听型PCB的存储位置与结构** 1. **全局链表定位** - 所有`LISTEN`状态的PCB集中存储在全局链表 **`tcp_listen_pcbs`** 中[^1] - 链表节点类型为精简的 **`struct tcp_pcb_listen`**(非标准`tcp_pcb`) 2. **链表节点轻量化设计** 监听专用PCB仅保留必要字段(尺寸约40字节): ```c struct tcp_pcb_listen { u16_t local_port; // 监听端口 u8_t backlog; // 半连接队列容量 tcp_accept_fn accept;// 连接建立回调 struct tcp_pcb *next;// 链表指针 }; ``` $$ \text{较标准PCB节省} \approx 80\% \text{内存} $$ 3. **状态机迁移** - 初始绑定:PCB加入`tcp_bound_pcbs`(`CLOSED`状态) - 开启监听:调用`tcp_listen()`将PCB从`tcp_bound_pcbs`移除,创建轻量监听块加入`tcp_listen_pcbs`[^2] $$ \text{tcp\_bound\_pcbs} \xrightarrow{\text{tcp\_listen()}} \text{tcp\_listen\_pcbs} + \text{内存回收} $$ --- ### ⚙️ **监听过程工作流** 1. **服务端初始化** ```c struct tcp_pcb *pcb = tcp_new(); // 创建标准PCB tcp_bind(pcb, IP_ADDR_ANY, 80); // 加入tcp_bound_pcbs struct tcp_pcb_listen *lpcb = tcp_listen(pcb, 5); // 转换并加入tcp_listen_pcbs ``` 2. **SYN处理** ```mermaid sequenceDiagram 客户端->>+tcp_input(): SYN报文 tcp_input() ->> tcp_listen_pcbs: 遍历匹配目标端口 alt 端口匹配成功 创建新tcp_pcb → 加入tcp_active_pcbs 发送SYN+ACK → 客户端 触发accept回调 → 应用层 else 无匹配 发送RST拒绝 end ``` 3. **连接队列管理** - `backlog`控制**半连接队列**(SYN_RCVD状态连接数)[^2] - 当活动连接中SYN_RCVD数量 ≥ backlog时,新SYN被丢弃 --- ### 🔍 **关键特性深度解析** 1. **并发处理能力** - 单个监听PCB可同时处理多个SYN请求 - 实际连接由独立的标准PCB(位于`tcp_active_pcbs`)管理 $$ \frac{\Delta\text{tcp\_active\_pcbs}}{\Delta t} = \text{SYN接收速率}, \quad \frac{\Delta\text{tcp\_listen\_pcbs}}{\Delta t} \equiv 0 $$ 2. **内存安全机制** | 风险场景 | 防护措施 | |-----------------|------------------------------| | SYN泛洪攻击 | 通过`backlog`限流半连接数量 | | 短连接风暴 | 调整`MEMP_NUM_TCP_PCB`池大小 | | 内存碎片 | 固定尺寸监听块避免碎片 | 3. **协议栈效率优化** - 精简监听块:降低端口扫描时遍历链表的CPU开销 - 状态隔离:避免活动连接的发送/接收缓冲(`unsent/unacked/ooseq`[^4])占用监听资源 --- ### ✅ **设计优势总结** 1. **资源隔离** 监听块无发送窗口、重传计时器等字段,与数据传输逻辑解耦 2. **恒定入口** 监听块生命周期与端口绑定一致,不因连接建立/销毁而变动 3. **时间复杂度** 端口匹配时间复杂度:$$ O(n) \text{(n=监听端口数)} $$ (远低于遍历活动连接的$$ O(m) \text{(m≫n)} $$) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值