linux系统连接服务器:一文剖析Linux-服务器的全连接队列
linux系统连接服务器:一文剖析Linux-服务器的全连接队列由连接请求块-存储队列的结构体可以看到全连接队列-Accept队列由struct request_sock结构体进行表示,如下所示,服务器端收到SYN请求之后,内核会建立连接请求块(req)Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂连接请求块的存储队列是对SYN同步队列(半连接)队列(服务端收到客户端SYN请求并回复SYN ACK的队列)、接收(全连接)队列的描述。在Linux内核中使用request_sock_queue 进行表示,如下结构体所示:struct request_sock_queue { spinlock_t rskq_lock; u8 rskq_defer_accept; u32 synflood_warned; atomic_t qlen;//半连接队列长度计数 atomic_t young;
上篇文章:一文讲解从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 进行表示,如下结构体所示:
struct request_sock_queue {
spinlock_t rskq_lock;
u8 rskq_defer_accept;
u32 synflood_warned;
atomic_t qlen;//半连接队列长度计数
atomic_t young;
struct request_sock *rskq_accept_head;//接收队列队列头部
struct request_sock *rskq_accept_tail;//
struct fastopen_queue fastopenq; /* Check max_qlen != 0 to determine
* if TFO is enabled.
*/
};
该结构体描述了两种队列的相关信息,第一个是半连接队列的长度,使用atomic_t qlen来表示,第二个是Accept队列链表,使用struct request_sock *rskq_accept_head;来表示 Accept队列链表的头部,struct request_sock *rskq_accept_tail;表示Accept队列链表的尾部。
更多linux内核视频教程文档资料免费领取后台私信【内核】自行获取.
Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈-学习视频教程-腾讯课堂
Accept接收(全连接)队列由连接请求块-存储队列的结构体可以看到全连接队列-Accept队列由struct request_sock结构体进行表示,如下所示,服务器端收到SYN请求之后,内核会建立连接请求块(req)
struct request_sock {
struct sock_common __req_common;
#define rsk_refcnt __req_common.skc_refcnt
#define rsk_hash __req_common.skc_hash
#define rsk_listener __req_common.skc_listener
#define rsk_window_clamp __req_common.skc_window_clamp
#define rsk_rcv_wnd __req_common.skc_rcv_wnd
struct request_sock *dl_next;
u16 mss;
u8 num_retrans; /* number of retransmits */
u8 cookie_ts:1; /* syncookie: encode TCPopts in timestamp */
u8 num_timeout:7; /* number of timeouts */
u32 ts_recent;
struct timer_list rsk_timer;
const struct request_sock_ops *rsk_ops;
struct sock *sk;
u32 *saved_syn;
u32 secid;
u32 peer_secid;
};
结构体成员变量 <span>request_sock *dl_next</span> 指向队列中下一个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开始。
reqsk_queue_alloc(&icsk->icsk_accept_queue);
sk->sk_max_ack_backlog = backlog;
sk->sk_ack_backlog = 0;
inet_csk_delack_init(sk);
下面举例分析一下 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函数:
struct sock *tcp_check_req(struct sock *sk struct sk_buff *skb
struct request_sock *req
bool fastopen)
{
......
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk skb req NULL
req &own_req);
......
return inet_csk_complete_hashdance(sk child req own_req);
......
}
syn_recv_sock对应的回调函数首先是对Accept队列进行判断:当前的Accept队列是否满,未满的情况下才会去创建子套接口
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)
{
......
if (sk_acceptq_is_full(sk))
goto exit_overflow;
......
}
判断队列是否满:sk_acceptq_is_full(sk),从此也看到了sk_max_ack_backlog对Accept接收队列的限制。
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
inet_csk_complete_hashdance将创建好的子套接口添加到Accept队列如下:
struct sock *inet_csk_complete_hashdance(struct sock *sk struct sock *child
struct request_sock *req bool own_req)
{
if (own_req) {
inet_csk_reqsk_queue_drop(sk req);
reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue req);
if (inet_csk_reqsk_queue_add(sk req child))
return child;
}
/* Too bad another child took ownership of the request undo. */
bh_unlock_sock(child);
sock_put(child);
return NULL;
}
inet_csk_complete_hashdance函数的参数:own_req仅当tcp_check_req函数中成功创建child子套接口才会为真。inet_csk_reqsk_queue_add函数会将设置req->sk = child,然后将req放入Accept队列icsk_accept_queue里面如下所示:
struct sock *inet_csk_reqsk_queue_add(struct sock *sk
struct request_sock *req
struct sock *child)
{
struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
spin_lock(&queue->rskq_lock);
if (unlikely(sk->sk_state != TCP_LISTEN)) {
inet_child_forget(sk req child);
child = NULL;
} else {
req->sk = child;//req与子套接口关联
req->dl_next = NULL;
/*如果全连接队列头部为空,也就是队列为空时
if (queue->rskq_accept_head == NULL)
queue->rskq_accept_head = req;//设置当前的req队列为头部
else
/*如果全连接队列不为空时*/
queue->rskq_accept_tail->dl_next = req;//尾插到队列最后
queue->rskq_accept_tail = req;//设置当前的req为队列尾部
sk_acceptq_added(sk);
}
spin_unlock(&queue->rskq_lock);
return child;
}
**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):
static inline void sk_acceptq_added(struct sock *sk)
{
sk->sk_ack_backlog ;
}
子套接口接入到Accept队列后调用该函数进行:sk->sk_ack_backlog 从而对队列进行计数管理。
并且当服务执行accept()后,accept()将返回已建立的连接 此时需要删除该请求套接口,删除过程如下:
static inline struct request_sock *reqsk_queue_remove(struct request_sock_queue *queue
struct sock *parent)
{
struct request_sock *req;
spin_lock_bh(&queue->rskq_lock);
req = queue->rskq_accept_head;
if (req) {
sk_acceptq_removed(parent);
queue->rskq_accept_head = req->dl_next;
if (queue->rskq_accept_head == NULL)
queue->rskq_accept_tail = NULL;
}
spin_unlock_bh(&queue->rskq_lock);
return req;
}
函数reqsk_queue_remove为简单的链表移除单个元素的操作,rskq_accept_head为链表的头,注意ACCEPT队列总是从头部开始移除队列中的子套接口元素,即用户层的accept操作总是取走队列中的第一个子套接口 如下图所示 绿色的线即头部重新指向被移除的next元素。
reqsk_queue_remove函数完成以上移除队列元素的操作之前执行: sk_acceptq_remove(parent)的操作,如下所示:
static inline void sk_acceptq_removed(struct sock *sk)
{
sk->sk_ack_backlog--;
}
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处理:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc char *argv[])
{
int sock_fd conn_fd;
struct sockaddr_in server_addr;
int port_number = 5200;
// 创建socket描述符
if ((sock_fd = socket(AF_Inet SOCK_STREAM 0)) == -1) {
fprintf(stderr "Socket error:%s\n\a" strerror(errno));
exit(1);
}
// 填充sockaddr_in结构
bzero(&server_addr sizeof(struct sockaddr_in));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(port_number);
// 绑定sock_fd描述符
if (Bind(sock_fd (struct sockaddr *)(&server_addr) sizeof(struct sockaddr)) == -1) {
fprintf(stderr "Bind error:%s\n\a" strerror(errno));
exit(1);
}
// 监听sock_fd描述符
if(listen(sock_fd 5) == -1) {
fprintf(stderr "Listen error:%s\n\a" strerror(errno));
exit(1);
}
//阻塞到这,不去Accept与接收数据,这将导致全连接队列
while (1){
}
if ((conn_fd = accept(sock_fd (struct sockaddr *)NULL NULL)) == -1) {
printf("accept socket error: %s\n\a" strerror(errno));
}
close(conn_fd);
close(sock_fd);
exit(0);
}
2、运行服务端程序:
3、编写客户端程序: 要求向服务端发起多次连接(大于6次),使用Go语言编写的客户端程序如下,并发10个去连接服务端
package main
import (
"fmt"
"net"
"time"
)
func connect() {
_ err := net.Dial("tcp4" "127.0.0.1:5200")
if err != nil {
fmt.Println(err)
}
fmt.Println("三次握手成功!\n")
}
func main(){
for i := 0; i < 10; i {
go connect()
}
time.Sleep(time.Minute * 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队列) - Linux内核 - 我爱内核网 - 构建全国最权威的内核技术交流分享论坛