快捷搜索:  汽车  科技

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的队列。

linux系统连接服务器:一文剖析Linux-服务器的全连接队列(1)

那么这全连接队列(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系统连接服务器:一文剖析Linux-服务器的全连接队列(2)

linux系统连接服务器:一文剖析Linux-服务器的全连接队列(3)

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系统连接服务器:一文剖析Linux-服务器的全连接队列(4)

在上篇文章中:一文讲解从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元素。

linux系统连接服务器:一文剖析Linux-服务器的全连接队列(5)

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、运行服务端程序:

linux系统连接服务器:一文剖析Linux-服务器的全连接队列(6)

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次

linux系统连接服务器:一文剖析Linux-服务器的全连接队列(7)

通过ss命令:ss -nlt

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

-n :不解析服务器名称

-t :只显示 tcp socket

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

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

linux系统连接服务器:一文剖析Linux-服务器的全连接队列(8)

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

原文地址:Linux-服务器的全连接队列(Accept队列) - Linux内核 - 我爱内核网 - 构建全国最权威的内核技术交流分享论坛

linux系统连接服务器:一文剖析Linux-服务器的全连接队列(9)

猜您喜欢: