欢迎光临散文网 会员登陆 & 注册

Linux-服务器的全连接队列(Accept队列)

2022-07-28 17:14 作者:补给站Linux内核  | 我要投稿

上篇文章:一文讲解从Linux内核角度分析服务器Listen细节分析了服务器Listen的底层细节,其中也分析了Listen系统调用的backlog参数,其决定了服务器Listen过程中全连接队列(Accept队列)的最大长度。本文将更进一步分析全连接队列(Accept队列)以及backlog参数是如何影响中全连接队列(Accept队列)的,并通过小实验直观了解backlog参数对全连接队列(Accept队列)的影响。

全连接队列是什么?

全连接队列存储3次握手成功并已建立的连接,将其称为全连接队列,也可称为接收队列(Accept队列),本文中的描述将称为Accept队列或全连接队列。如下红框中所示,全连已成功建立三次握手,当前的TCP状态为ESTABLISHED,但是服务端还未Accept的队列。

那么这全连接队列(Accept队列)** 在Linux内核中用什么数据结构进行表示?**

连接请求块- 存储队列

在介绍Accept队列前先看一下连接请求块:存储相关连接请求的队列的结构体

连接请求块的存储队列是对SYN同步队列(半连接)队列(服务端收到客户端SYN请求并回复SYN+ACK的队列)、接收(全连接)队列的描述。在Linux内核中使用request_sock_queue 进行表示,如下结构体所示:

该结构体描述了两种队列的相关信息,第一个是半连接队列的长度,使用atomic_t qlen来表示,第二个是Accept队列链表,使用struct request_sock *rskq_accept_head;来表示 Accept队列链表的头部,struct request_sock *rskq_accept_tail;表示Accept队列链表的尾部。


【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)        

由连接请求块-存储队列的结构体可以看到全连接队列-Accept队列由struct request_sock结构体进行表示,如下所示,服务器端收到SYN请求之后,内核会建立连接请求块(req)

结构体成员变量 request_sock *dl_next 指向队列中下一个Accept队列节点,Accept队列与存储队列直接的关系如下图所示:

在上一篇文章中:Linux内核角度分析服务器Listen细节分析服务器listen函数调用时,发现到listen()将调用inet_csk_listen_start(),后者将调用reqsk_queue_alloc()创建struct request_sock queue icsk_accept_queue,即创建存储队列的结构体。然后进行一些队列长度相关参数的设定。

在分析长度相关参数的设置代码之前,回顾一下用户传入的backlog参数在内核中最终如何取值的,如下代码所示,内核变量backlog的最终取值为 backlog = Min(用户传入的backlog值,somaxconn),其中somaxconn的值是Linux系统的默认值:128,该值可以通过 /proc/sys/net/core/somaxconn进行设置  。经常会有一个问题:Listen时backlog参数越大,Accept队列会越大吗?

从上面的分析也可以看出来答案,内核中backlog变量的最终取值是Listen系统调用传入的backlog与系统默认值两者之间的最小值, 所以在Listen时backlog的需求超过系统默认值128时,需要修改系统默认值以满足更大的需求。

下面分析长度相关参数的设置,如下代码节选自inet_csk_listen_start函数,sk_max_ack_backlog是对Accept队列最大长度进行限制,sk_ack_backlog是对当前Accept队列的长度进行计数,最开始初始化为0,也就是计数从0开始。

下面举例分析一下Accept队列并分析sk_ack_backlog如何对Accept队列进行计数、sk_max_ack_backlog = backlog如何对Accept接收队列长度的限制:

1、服务器收到客户端三次握手最后一个ACK时:

收到客户端最后一个ACK后·,服务器调用tcp_v4_rcv->tcp_v4_syn_rcv_sock,然后通过tcp_check_req函数进行检查,如果一切检查正常的话,使用回调syn_recv_sock处理去创建子套接口(child),之后由函数inet_csk_complete_hashdance中设置req->sk = child,然后将req放入全连接队列icsk_accept_queue里面。

如下是tcp_check_req函数:

syn_recv_sock对应的回调函数首先是对Accept队列进行判断:当前的Accept队列是否满,未满的情况下才会去创建子套接口

判断队列是否满:sk_acceptq_is_full(sk),从此也看到了sk_max_ack_backlog对Accept接收队列的限制。

inet_csk_complete_hashdance将创建好的子套接口添加到Accept队列如下:

inet_csk_complete_hashdance函数的参数:own_req仅当tcp_check_req函数中成功创建child子套接口才会为真。inet_csk_reqsk_queue_add函数会将设置req->sk = child,然后将req放入Accept队列icsk_accept_queue里面如下所示:

2、服务端接收到客户端最后一个ACK并加入Accept队列后

      服务器Accept获取Accept队列的请求套接口,并删除该请求套接口时

在1、中也提到:收到客户端最后一个ACK后·,服务器调用tcp_v4_rcv->tcp_v4_syn_rcv_sock,然后通过tcp_check_req函数进行检查,如果一切检查正常的话,使用回调syn_recv_sock处理去创建子套接口,之后由函数inet_csk_complete_hashdance将子套接口添加到ACCEPT队列中。那么当添加Accept接收队列后,就要进行队列的计数,inet_csk_reqsk_queue_add函数调用sk_accepttq_added(sk):

子套接口接入到Accept队列后调用该函数进行:sk->sk_ack_backlog++,从而对队列进行计数管理。

并且当服务执行accept()后,accept()将返回已建立的连接,此时需要删除该请求套接口,删除过程如下:

函数reqsk_queue_remove为简单的链表移除单个元素的操作,rskq_accept_head为链表的头,注意ACCEPT队列总是从头部开始移除队列中的子套接口元素,即用户层的accept操作总是取走队列中的第一个子套接口,如下图所示,绿色的线即头部重新指向被移除的next元素。

reqsk_queue_remove函数完成以上移除队列元素的操作之前执行: sk_acceptq_remove(parent)的操作,如下所示:

sk->sk_ack_backlog--操作对队列元素的个数进行更新

综上分析,sk_ack_backlog是对Accept接收队列的计数,sk_max_ack_backlog限制了Accept接收队列的最大长度,sk_max_ack_backlog也正是Listen系统调用传入的参数backlog。

小实验

1、首先创建一个服务端:

服务端开启Listen,并设置Listen函数的backlog参数为5,即全连接队列最大长度只能到6(由上面的内核分析也可知,sk_ack_backlog对Accept队列的计数是从0开始的,长度限制变量sk_max_ack_backlog就是Min(用户传入的backlog,系统默认值128),也就是说sk_max_ack_backlog为5,也就是说最大长度为6),重要的一点是服务端不进行Accept处理:

2、运行服务端程序:

3、编写客户端程序:要求向服务端发起多次连接(大于6次),使用Go语言编写的客户端程序如下,并发10个去连接服务端

4、运行客户端,可以看到连接了6次

通过ss命令:ss -nlt

-l: 显示正在监听(Listening)的socket

-n :不解析服务器名称

-t :只显示 tcp socket

Recv-Q:当前全连接队列的大小,也就是当前已完成三次握手并等待服务端 accept() 的 TCP 连接;

Send-Q:当前全连接最大队列长度(从0开始计数),上面服务器的最大全连接长度为6(0~5);

可以看到服务端 127.0.0.1:5200的Send-Q为5(0~5),即最大全连接长度为6,Recv-Q是当前的Accept队列的长度为6。10个并行连接只有6个成功完成3次握手,剩下4个都未完成三次握手。说明TCP 全连接队列过小,就容易溢出,当发生 TCP 全连接队溢出的时候,后续的请求就会被丢弃。


Linux-服务器的全连接队列(Accept队列)的评论 (共 条)

分享到微博请遵守国家法律