listen函数backlog参数的一点探讨

2023-10-11 23:30

本文主要是介绍listen函数backlog参数的一点探讨,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言:

        今年上半年的时候, 因为自己工作的失误, 导致程序的TCP三次握手非常缓慢, 存在大量syn_recv状态连接. 查了很多资料(尤其是listen的相关资料)都无法完美解决问题, 虽然调大了backlog参数, 但是连接数达到一定值(backlog)后, 三次握手同样非常缓慢. 后面才发现是自己代码的原因, 具体来讲, 就是我那部分代码阻塞了进程, 相当于sleep了下(虽然不是真的sleep). 后面我把这部分代码给删除了, 程序恢复了正常.

        最近一段时间, 又想到了这个问题, 所以就想写个测试程序来模拟当时的场景, 顺便对当时搜的资料做个总结. 

         本文基于centos7环境, linux3.10内核. 好吧, 那就开始吧.

开始:

        这里有几个名词/参数需要了解下, 本文会遇到.

        全连接队列: 完成三次握手处于ESTABLISHED状态.

        未全连接队列: 此处称之为半连接队列. 服务端收到了SYN后半连接队列+1然后再回复SYN+ACK. 回复SYN + ACK后此时处于SYN_RECV状态.

        变量backlog: 函数listen的第二个参数.

        变量somaxconn: /proc/sys/net/core/somaxconn.

        变量tcp_max_syn_backlog: /proc/sys/net/ipv4/tcp_max_syn_backlog.

        变量syncookies: /proc/sys/net/ipv4/tcp_syncookies.

        变量tcp_synack_retries: net.ipv4.tcp_synack_retries, 含义是未激活的TCP连接发送SYN/ACK段的重试次数. 可以写在/etc/sysctl.conf内, 也可以通过代码设置.

        可以通过man 2 listen命令可以查看如下解释:

         大概意思是: backlog指定的是等待被accept的全连接socket队列长度, 代替了未全连接请求的数量. 未全连接socket队列的最大长度可以通过tcp_max_syn_backlog设置, 当设置了syncookies, 则不是逻辑上的最大长度, 这个设置可以忽略.

        还有一个系统默认参数somaxconn, 两者取最小值为全连接队列长度.

        读完这个解释后, 心里冒出几个问题:

        1, 半连接队列长度是多大.

        2, 全连接队列长度是多大.

        3, 半连接队列长度和全连接队列长度何时加减.

        4, 如何测试.

        关于第1, 2个问题, 详细解答可以查看下面的链接, 这里只做简答答疑.

TCP 的backlog详解及半连接队列和全连接队列_Blue summer的博客-CSDN博客_tcp 半连接

第1部分, 半连接队列长度:        

         这个函数的功能是判断半连接队列是否满了. 第一个变量为当前半连接队列的实际大小, 第二个变量为2^n(实际分析下来, 2^n就是半连接队的设置大小)中的指数n. 现在只需要知道max_qlen_log这个数值如何得来的就可以了. 看下图.

        传入的参数nr_table_entries的实际值为min(backlog, somaxconn), 那么:

        第46行: x = min(backlog, somaxconn, tcp_max_syn_backlog)

        第47行: y = max(x, 8)

        第48行: nr_table_entries的最终值为 (y + 1)最接近的2^n, 此为半连接队列大小, max_qlen_log的值为n.

第2部分, 全连接队列长度:

        这个函数的功能是判断全连接队列是否满了. 第一个变量指的是当前全连接队列的实际大小, 第二个变量是全连接队列的设置大小, 如此我们只需知道第二个变量是如何得来的就可以了.

        在这里我们需要查看两个函数, 第一个函数为listen的系统调用SYSCALL_DEFINE2(listen, int, fd, int, backlog). 第二个函数为其调用的具体函数int inet_listen(struct socket *sock, int backlog). 两个函数里分别有如下实现:

         可以看到, 函数1的实现就是取两者最小值, 函数2的实现就跟上述判断全连接队列是否满了函数的第二个变量匹配起来, 所以全连接队列的最终大小是: min(backlog, somaxconn)

第3部分, 队列加减操作: 

3.1 半连接队列+1:

        在服务端收到第一次握手的SYN时半连接队列+1, 具体调用如下.

 3.2 半连接队列-1同时全连接队列+1:

        在服务端收到第三次握手客户端发送的ACK时半连接队列会-1, 同时全连接队列会+1. 具体调用如下.

 3.3 全连接队列-1:

        在服务端调用accept时全连接队列-1. 具体调用如下.

第4部分, 如何测试: 

        主要测试的内容是, 全连接队列已满时的各种信息. 所以, 测试代码实现的是不完整的非常规socket: 服务端只listen而没有accept调用, 客户端只connect而不读写. 测试代码见末尾.

4.1 条件1:

backlogsomaxconntcp_max_syn_backlogsyncookiestcp_synack_retries
1616161(开启)127

        根据第1部分和第2部分, 我们可以知道半连接队列设置大小为32, 全连接队列设置大小为16. 下面通过测试也简单验证下.

        4.1.1 测试1:

        客户端启动10个tcp连接. 通过ss -ln命令分析, 结果如下图:

        可以看到, 全连接队列设置大小为16, 结果跟预测匹配; 全连接队列当前大小为10, 跟10个tcp连接匹配. 

        4.1.2 测试2:

        客户端启动25个tcp连接. 通过ss和netstat命令分析, 结果如下图:

        可以看到, 全连接队列当前大小为17, ESTABLISHED状态的个数也为17,结果互相匹配; 半连接状态的个数为8, 总共25个tcp连接, 结果匹配. 

        4.1.3 测试3:

        客户端启动49个tcp连接. 通过netstat命令分析, 结果如下图:

        可以看到, 半连接状态的最大值为32, 那么半连接队列当前大小的最大值为32. 

        4.1.4 测试4:

        客户端启动50个tcp连接. 通过netstat命令分析, 并用demsg查看内核日志, 结果如下图:

        可以发现, 半连接状态的最大值为32, 那么半连接队列当前大小最大值为32, 再通过和测试3比对, 可以知道半连接队列设置大小为32, 跟预测结果一致. 如果半连接队列已满时还有客户端的SYN请求, 那么内核将会出现syn flooding日志. 内核实现如下:

4.2 条件2:

backlogsomaxconntcp_max_syn_backlogsyncookiestcp_synack_retries
1616160(关闭)127

       4.2.1 测试1:

        客户端启动30个tcp连接, 用demsg命令查看内核日志, 并无明显日志.

       4.2.2 测试2:

        客户端启动31个tcp连接, 用dmesg命令查看内核日志:

        说明, 当关闭syncookies时, 半连接队列未满就已经开始丢弃SYN请求了. 内核实现如下:

结尾:

        其实我还是有些疑问的.

        1, 为什么全连接队列设置大小为16, 而最终ESTABLISHED的个数为17, 多个数值尝试都为+1的关系.

        这个疑问通过跟别人交流, 已经有了答案. 如下:

        切换到全连接队列的处理逻辑是这样子的: 先判断全连接队列是否已满, 再执行++操作; 如果backlog为16, 第16个ACK(第三次握手)到来时, 全连接队列实际大小为15, 未满, 则++, 此时才为16; 第17个ACK到来时, 实际大小为16, 而判断逻辑是>, 未满, 又++, 此时为17, 后续的就满了, 符合+1的逻辑关系.

        2, 4.1.3 测试3, 49个tcp连接, 此时并没有引起syn flooding, 为何半连接队列当前大小不能维持在32个, 因为我设置的重试次数为127次, 并没有达到重试的次数就开始降低了.

        3, 4.2.2 测试2, 根据代码, 半连接数达到13个可以使第二个条件成立(16 – 13 < 4), 那么理论上总连接数30(13 + 17全连接)个就能触发drop条件, 而最终的结果是启动31个连接数才能触发.

        本人能力有限, 文中或许有些错误, 如果有同学发现, 敬请指教. 或者有同学知道我的疑问, 也希望不吝赐教. 但是通过这次测试, 我对backlog参数有一个比较深的理解. 也期望自己能在余下的时间完善自己, 找到这些问题的答案.

代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>typedef struct config
{int mode;char *addr;unsigned short int port;int backlog;int conn_num;struct sockaddr_in addr_in;socklen_t  addr_in_len;
}config_t;static int tcp_socket(const config_t *conf)
{int sfd = socket(AF_INET, SOCK_STREAM, 0);if (sfd == -1){printf("socket failed, err msg: %s\n", strerror(errno));return -1;}int val1 = 1;if (setsockopt(sfd, SOL_SOCKET, SO_REUSEADDR | (conf->mode == 0 ? SO_REUSEPORT : 0), (void *)&val1, sizeof(val1)) == -1){printf("setsockopt failed, err msg: %s\n", strerror(errno));goto FAILED;               }if (conf->mode == 0){if (bind(sfd, (struct sockaddr*)&conf->addr_in, conf->addr_in_len) == -1){printf("bind failed, err msg: %s\n", strerror(errno));goto FAILED;               }if (listen(sfd, conf->backlog)){printf("bind failed, err msg: %s\n", strerror(errno));goto FAILED;                              }                       }else{if (connect(sfd, (struct sockaddr*)&conf->addr_in, conf->addr_in_len)){printf("connect failed, err msg: %s\n", strerror(errno));goto FAILED;                        }                }return sfd;FAILED:close(sfd);return -1;
}static void sig_call(int sig)
{printf("capture sig: %d\n", sig);
}static void server(config_t *conf)
{int sock_fd = tcp_socket(conf);if (sock_fd == -1){return;}// 不acceptsleep(3600);
}static void client(config_t *conf)
{const int SIZE = conf->conn_num;int fds[SIZE], i = 0;for(; i < SIZE; ++i){if ((fds[i] = tcp_socket(conf)) == -1){return;}if (i > conf->backlog){sleep(1);}}sleep(3600);
}int main(int argc, char *argv[])
{signal(SIGPIPE, sig_call);config_t conf = {.mode = atoi(argv[1]),          // 0-服务端, 非0-客户端.addr = argv[2],                // 地址.port = atoi(argv[3]),          // 端口.backlog = atoi(argv[4]),       // 服务端listen的第二个参数.conn_num = atoi(argv[5]),      // 客户端启动TCP连接数.addr_in.sin_family = AF_INET,.addr_in.sin_addr.s_addr = inet_addr(argv[2]),.addr_in.sin_port = htons(atoi(argv[3])),.addr_in_len = sizeof(struct sockaddr_in),};if (conf.mode == 0){server(&conf);}else{client(&conf);}return 0;
}

这篇关于listen函数backlog参数的一点探讨的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/191608

相关文章

Pandas中统计汇总可视化函数plot()的使用

《Pandas中统计汇总可视化函数plot()的使用》Pandas提供了许多强大的数据处理和分析功能,其中plot()函数就是其可视化功能的一个重要组成部分,本文主要介绍了Pandas中统计汇总可视化... 目录一、plot()函数简介二、plot()函数的基本用法三、plot()函数的参数详解四、使用pl

SpringBoot请求参数接收控制指南分享

《SpringBoot请求参数接收控制指南分享》:本文主要介绍SpringBoot请求参数接收控制指南,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Spring Boot 请求参数接收控制指南1. 概述2. 有注解时参数接收方式对比3. 无注解时接收参数默认位置

Python的time模块一些常用功能(各种与时间相关的函数)

《Python的time模块一些常用功能(各种与时间相关的函数)》Python的time模块提供了各种与时间相关的函数,包括获取当前时间、处理时间间隔、执行时间测量等,:本文主要介绍Python的... 目录1. 获取当前时间2. 时间格式化3. 延时执行4. 时间戳运算5. 计算代码执行时间6. 转换为指

Python正则表达式语法及re模块中的常用函数详解

《Python正则表达式语法及re模块中的常用函数详解》这篇文章主要给大家介绍了关于Python正则表达式语法及re模块中常用函数的相关资料,正则表达式是一种强大的字符串处理工具,可以用于匹配、切分、... 目录概念、作用和步骤语法re模块中的常用函数总结 概念、作用和步骤概念: 本身也是一个字符串,其中

Python使用getopt处理命令行参数示例解析(最佳实践)

《Python使用getopt处理命令行参数示例解析(最佳实践)》getopt模块是Python标准库中一个简单但强大的命令行参数处理工具,它特别适合那些需要快速实现基本命令行参数解析的场景,或者需要... 目录为什么需要处理命令行参数?getopt模块基础实际应用示例与其他参数处理方式的比较常见问http

Linux内核参数配置与验证详细指南

《Linux内核参数配置与验证详细指南》在Linux系统运维和性能优化中,内核参数(sysctl)的配置至关重要,本文主要来聊聊如何配置与验证这些Linux内核参数,希望对大家有一定的帮助... 目录1. 引言2. 内核参数的作用3. 如何设置内核参数3.1 临时设置(重启失效)3.2 永久设置(重启仍生效

SpringMVC获取请求参数的方法

《SpringMVC获取请求参数的方法》:本文主要介绍SpringMVC获取请求参数的方法,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下... 目录1、通过ServletAPI获取2、通过控制器方法的形参获取请求参数3、@RequestParam4、@

shell编程之函数与数组的使用详解

《shell编程之函数与数组的使用详解》:本文主要介绍shell编程之函数与数组的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录shell函数函数的用法俩个数求和系统资源监控并报警函数函数变量的作用范围函数的参数递归函数shell数组获取数组的长度读取某下的

MySQL高级查询之JOIN、子查询、窗口函数实际案例

《MySQL高级查询之JOIN、子查询、窗口函数实际案例》:本文主要介绍MySQL高级查询之JOIN、子查询、窗口函数实际案例的相关资料,JOIN用于多表关联查询,子查询用于数据筛选和过滤,窗口函... 目录前言1. JOIN(连接查询)1.1 内连接(INNER JOIN)1.2 左连接(LEFT JOI

MySQL中FIND_IN_SET函数与INSTR函数用法解析

《MySQL中FIND_IN_SET函数与INSTR函数用法解析》:本文主要介绍MySQL中FIND_IN_SET函数与INSTR函数用法解析,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友一... 目录一、功能定义与语法1、FIND_IN_SET函数2、INSTR函数二、本质区别对比三、实际场景案例分