这篇文章是对上一篇博客网络编程常用接口的内核实现----sys_listen()的补充,上篇文章中我说listen()系统调用的backlog参数既是连接队列的长度,也指定了半连接队列的长度(不能说等于),而不是《Unix网络编程》中讲到的是半连接队列和连接队列之和的上限,也就是说这个说法对Linux不适用。这篇文章中通过具体的代码来说明这个结论,并且会分析如果连接队列和半连接队列都满的话,内核会怎样处理。

首先来看半连接队列的上限是怎么计算和存储的。半连接队列长度的上限值存储在listen_sock结构的max_qlen_log成员中。如果找到监听套接字的sock实例,调用inet_csk()可以获取inet_connection_sock实例,inet_connection_sock结构是描述支持面向连接特性的描述块,其成员icsk_accept_queue是用来管理连接队列和半连接队列的结构,类型是request_sock_queue。listen_sock实例就存储在request_sock_queue结构的listen_opt成员中,它们之间的关系如下图所示(注:本来下面的图应该横着画,但是横着CSDN会显示不全):

半连接队列的长度上限在reqsk_queue_alloc()中计算并设置的,代码片段如下所示:

[cpp] view plaincopy

  1. int reqsk_queue_alloc(struct request_sock_queue *queue,

  2. unsigned int nr_table_entries)

  3. {

  4. .......

  5. nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);

  6. nr_table_entries = max_t(u32, nr_table_entries, 8);

  7. nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);

  8. ......

  9. ......

  10. for (lopt->max_qlen_log = 3;

  11. (1 << lopt->max_qlen_log) < nr_table_entries;

  12. lopt->max_qlen_log++);

  13. ......

  14. }

前面的三行代码是调整存储半连接的哈希表的大小,可以看到这个值还受系统配置sysctl_max_syn_backlog的影响,所以如果想调大监听套接字的半连接队列,除了增大listen()的backlog参数外,还需要调整sysctl_max_syn_backlog系统配置的值,这个配置量对应的proc文件为/proc/sys/net/ipv4/tcp_max_syn_backlog。后面的for循环是计算nr_table_entries以2为底的对数,计算的结果就存储在max_qlen_log成员中。

接着来看连接队列长度的上限,这个比较简单,存储在sock结构的sk_max_ack_backlog成员中,在inet_listen()中设置,如下所示:

[cpp] view plaincopy

  1. int inet_listen(struct socket *sock, int backlog)

  2. {

  3. ......

  4. sk->sk_max_ack_backlog = backlog;

  5. err = 0;

  6. out:

  7. release_sock(sk);

  8. return err;

  9. }

接下来我们看如果连接队列满了的话,内核会如何处理。先写个测试程序,构造连接队列满的情况。测试程序说明如下:

1、服务器端地址为192.168.1.188,监听端口为80;客户端地址为192.168.1.192

2、服务器端在80端口建立一个监听套接字,listen()的backlog参数设置的是300,将sysctl_max_syn_backlog和sysctl_somaxconn系统配置都调整为4096,特别要注意的             是服务器端一定不要调用accept()来接收连接,在建立起监听后,让进程睡眠等待。关键代码如下:

[cpp] view plaincopy

  1. ........

  2. if ((ret = listen(fd, 300)) < 0) {

  3. perror("listen");

  4. goto err_out;

  5. }

  6. /* wait connection */

  7. while (1) {

  8. sleep(3);

  9. }

  10. ........

3、客户端通过一个循环发起1000个连接请求,为了后面进一步的分析,在第401连接建立后打印输出其本地端口,并且发送了两次数据。关键代码如下:

[cpp] view plaincopy

  1. ......

  2. ret = connect(fd, (struct sockaddr *)&sa, sizeof(sa));

  3. if (ret < 0) {

  4. fprintf(stderr, "connect fail in %d times, reason: %s.\n", i + 1, strerror(errno));

  5. return -1;

  6. }

  7. connections[i] = fd;

  8. fprintf(stderr, "Connection success, times: %d, connections: %d.\n", i + 1,

  9. check_connection_count(connections, i + 1));

  10. if (i == 400) {

  11. len = sizeof(sa);

  12. ret = getsockname(fd, (struct sockaddr *)&sa, &len);

  13. if (ret < 0) {

  14. fprintf(stderr, "getsockname fail, ret=%d.\n", ret);

  15. return -1;

  16. }

  17. fprintf(stderr, "connecton %d, local port: %u.\n", i,ntohs(sa.sin_port));

  18. str = "if i can write ,times 1";

  19. ret = write(fd, str, strlen(str));

  20. fprintf(stderr, "first writ in connection %d, ret = %d.\n", i, ret);

  21. str = "if i can write ,times 2";

  22. ret = write(fd, str, strlen(str));

  23. fprintf(stderr, "second writ in connection %d, ret = %d.\n", i, ret);

  24. }

  25. .......

在启动测试程序之前,在客户端使用tcpdump抓包,并将输出结果通过-w选项存储在192.cap文件中,便于后续使用wireshark来分析。

测试发现,在客户端建立300个连接后,客户端建立连接的速度明显慢了很多,而且最终建立完1000个连接花了20分钟左右。使用wireshark打开192.cap文件,来看抓包的情况,发现在300个连接之后有大量的ack包重传,如下图所示:

在wireshark的过滤器中选择本地端口为49274的连接来具体分析,该连接抓包情况如下所示:

上面的图中可以看到,SYN包重传了一次;在正常的三次握手之后,服务器又发送了SYN+ACK包给客户端,导致客户段再次发送ACK,而且这个过程重复了5次。在wireshark中过滤其他连接,发现情况也是如此。

问题来了,为什么要重传SYN包?为什么在三次握手之后,服务器端还要重复发送SYN+ACK包?为什么重复了5次之后就不再发了呢?要解答这些问题,我们需要深入到内核代码中看三次握手过程中内核是如何处理的,以及在连接队列满之后是怎么处理。内核中处理客户端发送的SYN包是在tcp_v4_conn_request()函数中,关键代码如下所示:

[cpp] view plaincopy

  1. int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)

  2. {

  3. ......

  4. if (inet_csk_reqsk_queue_is_full(sk) && !isn) {

  5. #ifdef CONFIG_SYN_COOKIES

  6. if (sysctl_tcp_syncookies) {

  7. want_cookie = 1;

  8. } else

  9. #endif

  10. goto drop;

  11. }

  12. /* Accept backlog is full. If we have already queued enough

  13. * of warm entries in syn queue, drop request. It is better than

  14. * clogging syn queue with openreqs with exponentially increasing

  15. * timeout.

  16. */

  17. if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)

  18. goto drop;

  19. req = inet_reqsk_alloc(&tcp_request_sock_ops);

  20. if (!req)

  21. goto drop;                                                                  ......

  22. if (__tcp_v4_send_synack(sk, req, dst) || want_cookie)

  23. goto drop_and_free;

  24. inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);

  25. return 0;

  26. drop_and_release:

  27. dst_release(dst);

  28. drop_and_free:

  29. reqsk_free(req);

  30. drop:

  31. return 0;

  32. }

我们主要看inet_csk_reqsk_queue_is_full()函数和sk_acceptq_is_full()函数的部分,这两个函数分别用来判断半连接队列和连接队列是否已满。结合上面的代码,在两种情况下会丢掉SYN包。一种是在半连接队列已满的情况下,isn的值其实TCP_SKB_CB(skb)->when的值,when在tcp_v4_rcv()中被清零,所以!isn总是为真;第二种情况是在连接队列已满并且半连接队列中还有未重传过的半连接(通过inet_csk_reqsk_queue_young()来判断)。至于我们看到的源端口为49274的连接是在哪个位置丢掉的就不知道了,这要看但是半连接队列的情况。因为有专门的定时器函数来维护半连接队列,所以在第二次发送SYN包时,包没有丢弃,所以内核会调用__tcp_v4_send_synack()函数来发送SYN+ACK包,并且分配内存用来描述当前的半连接状态。当服务器发送的SYN+ACK包到达客户端时,客户端的状态会从SYN_SENT状态变为ESTABLISHED状态,也就是说客户端认为TCP连接已经建立,然后发送ACK给服务器端,来完成三次握手。在正常情况下,服务器端接收到客户端发送的ACK后,会将描述半连接的request_sock实例从半连接队列移除,并且建立描述连接的sock结构,但是在连接队列已满的情况下,内核并不是这样处理的。

当客户端发送的ACK到达服务器后,内核会调用tcp_check_req()来检查这个ACK包是否是正确,从TCP层的接收函数tcp_v4_rcv()到tcp_check_req()的代码流程如下图所示:

如果是正确的ACK包,tcp_check_req()会调用tcp_v4_syn_recv_sock()函数创建新的套接字,在tcp_v4_syn_recv_sock()中会首先检查连接队列是否已满,如果已满的话,会直接返回NULL。当tcp_v4_syn_recv_sock()返回NULL时,会跳转到tcp_check_req()函数的listen_overflow标签处执行,如下所示:

[cpp] view plaincopy

  1. /*

  2. *  Process an incoming packet for SYN_RECV sockets represented

  3. *  as a request_sock.

  4. */

  5. struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,

  6. struct request_sock *req,

  7. struct request_sock **prev)

  8. {

  9. ......

  10. /* OK, ACK is valid, create big socket and

  11. * feed this segment to it. It will repeat all

  12. * the tests. THIS SEGMENT MUST MOVE SOCKET TO

  13. * ESTABLISHED STATE. If it will be dropped after

  14. * socket is created, wait for troubles.

  15. */

  16. child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);

  17. if (child == NULL)

  18. goto listen_overflow;

  19. .......

  20. listen_overflow:

  21. if (!sysctl_tcp_abort_on_overflow) {

  22. inet_rsk(req)->acked = 1;

  23. return NULL;

  24. }

  25. ......

  26. }

在listen_overflow处,会设置inet_request_sock的acked成员,该标志设置时表示已接收到第三次握手的ACK段,但是由于服务器繁忙或其他原因导致未能建立起连接,此时可根据该标志重新给客户端发送SYN+ACK段,再次进行连接的建立。具体检查是否需要重传是在syn_ack_recalc()函数中进行的,其代码如下所示:

[cpp] view plaincopy

  1. /* Decide when to expire the request and when to resend SYN-ACK */

  2. static inline void syn_ack_recalc(struct request_sock *req, const int thresh,

  3. const int max_retries,

  4. const u8 rskq_defer_accept,

  5. int *expire, int *resend)

  6. {

  7. if (!rskq_defer_accept) {

  8. *expire = req->retrans >= thresh;

  9. *resend = 1;

  10. return;

  11. }

  12. *expire = req->retrans >= thresh &&

  13. (!inet_rsk(req)->acked || req->retrans >= max_retries);

  14. /*

  15. * Do not resend while waiting for data after ACK,

  16. * start to resend on end of deferring period to give

  17. * last chance for data or ACK to create established socket.

  18. */

  19. *resend = !inet_rsk(req)->acked ||

  20. req->retrans >= rskq_defer_accept - 1;

  21. }

在SYN+ACK的重传次数未到达上限或者已经接收到第三次握手的ACK段后,由于繁忙或其他原因导致未能建立起连接时会重传SYN+ACK。

至此,我们不难理解为什么服务器总是会重复发送SYN+ACK。当客户端的第三次握手的ACK到达服务器端后,服务器检查ACK没有问题,接着调用tcp_v4_syn_recv_sock()来创建套接字,发现连接队列已满,因为直接返回NULL,并设置acked标志,在定时器中稍后重新发送SYN+ACK,尝试完成连接的建立。当服务器段发送的SYN+ACK到达客户端后,客户端会重新发送ACK给服务器,在这个过程中服务器端是主动方,客户端只是被动地发送响应,从抓包的情况也能看出。那如果重试多次还是不能建立连接呢,服务器会一直重复发送SYN+ACK吗?答案肯定是否定的,重传的次数受系统配置sysctl_tcp_synack_retries的影响,该值默认为5,因此我们在抓包的时候看到在重试5次之后,服务器段就再也不重发SYN+ACK包了。如果重试了5次之后还是不能建立连接,内核会将这个半连接从半连接队列上移除并释放。

到这里我们先前的所有问题都解决了,但是又有了一个新的问题,当服务器端发送SYN+ACK给客户端时,服务器端可能还处于半连接状态,没有创建描述连接的sock结构,但是我们知道客户端在接收到服务器端的SYN+ACK后,按照三次握手过程中的状态迁移这时会从SYN_SENT状态变为ESTABLISHED状态,可以参考《Unix网络编程》上的图2.5,如下所示:

所以在连接队列已满的情况下,客户端会在连接尚未完成的时候误认为连接已经建立,如果在这种情况下发送数据到服务器端是没有办法处理的。这种情况即使调用getsockopt()来检查SO_ERROR选项也是检测不到的。假设客户端在接收到第一个SYN+ACK包后,就发送数据给服务器段,服务器端并没有建立连接。当数据包传送到TCP层的接收函数tcp_v4_rcv()中处理时,因为没有找到sock实例,会直接丢掉数据包。但是在客户端调用write()发送数据时,将要发送的数据拷贝到内核缓冲区后就会返回成功,客户端依然发现不了连接其实尚未完全建立。当write返回后,TCP协议栈将数据发送到服务器端时不会受到ACK包,只能重传。因为服务器段不存在这个连接,即使重传无数次也没有用,当然服务器端的协议栈也不能允许客户端无限制地重复这样的过程,最后会以服务器端发送的RST包彻底结束这个没有正确建立的“连接”。也就是说在这种极限情况下,TCP协议的可靠性没法保证。

我们在客户端的测试程序中打印出了第401个“连接”的端口号,我们通过这个连接就可以验证我们的结论,其抓包情况如下所示:

在客户端程序中write()系统调用返回成功,但是我们在图中可以看到发送的数据一直在重传而没有收到确认包,直到最终接收到服务器端发送的RST包。

OK,到这里我们的分析算是彻底结束了,在分析的过程中忽略了一些细节的东西,感兴趣的可以自己结合源码看一看。

转载于:https://blog.51cto.com/davidbj/1601197

Linux中listen()系统调用的backlog参数分析相关推荐

  1. listen函数的第二个参数_signal(SIGPIPE,?SIG_IGN)listen函数中backlog参数分析

    signal(SIGPIPE, SIG_IGN); TCP是全双工的信道, 可以看作两条单工信道, TCP连接两端的两个端点各负责一条. 当对端调用close时, 虽然本意是关闭整个两条信道, 但本端 ...

  2. linux中的系统调用

    前言:本文只讨论linux中的系统调用,不考虑windows等其他操作系统. 两点: 1.系统调用时,进程调用的是操作系统的内核函数,不是进程. 2.系统调用时,会出现上下文切换,但和进程调度时的上下 ...

  3. linux源码文件名,Linux中文件名解析处理源码分析

    Linux中文件名解析处理源码分析 前言 Linux中对一个文件进行操作的时候,一件很重要的事情是对文件名进行解析处理,并且找到对应文件的inode对象,然后创建表示文件的file对象.在此,对文件名 ...

  4. listen()函数中backlog参数分析

    背景知识 Unix网络编程描述如下: 总结 0. accept()函数不参与三次握手,而只负责从已建立连接队列中取出一个连接和sockfd进行绑定: 1. backlog参数决定了未完成队列和已完成队 ...

  5. Linux中brk()系统调用,sbrk(),mmap(),malloc(),calloc()的异同【转】

    转自:http://blog.csdn.net/kobbee9/article/details/7397010 brk和sbrk主要的工作是实现虚拟内存到内存的映射.在GNUC中,内存分配是这样的: ...

  6. MIPS(loongson)linux 中添加系统调用

    在基于MISP(loongson)架构处理器与基于 x86 架构处理器的 linux 内核中添加系统调用时更改的文件是不同的,x86 中需要更改 arch/x86/kernel/syscall_tab ...

  7. imx6 通过移植XRM117x(SPI转串口)对Linux中的SPI驱动框架进行分析

    最近分析了一下Linux 中的SPI驱动框架,将自己的理解总结一下,不足之处还请斧正! 1.SPI通信基础知识 SPI(Serial Peripheral Interface)是一种串行(一次发送1b ...

  8. linux中文件记录的时间参数,【Linux】stat命令查看文件的三个时间参数

     在Windows中创建一个文件都会有相应的创建时间,修改时间,访问时间来记录文件的一些属性.在Linux中也不例外,文件也有三个时间来记录文件的变动,这三个时间分别是Modification t ...

  9. linux中安shell怎么传入参数,【linux】linux 下 shell命令 执行结果赋值给变量【两种方式】...

    方法1:[通用方法] 使用Tab键上面的反引号 例子如下: find命令 模糊查询在/apps/swapping目录下 查找 文件名中包含swapping并且以.jar结尾的文件 使用反引号 引住命令 ...

最新文章

  1. mysql数据库千万级别数据的查询优化和分页测试
  2. 玛纽尔扫地机器人怎样_扫地机器人哪个牌子好?满足日常清洁需求才值得推荐...
  3. linux复制以a开头的文件,linux部分试题
  4. UNIX 环境高级编程(八)—— fork 函数
  5. python报表自动化系列 - 通过Python使用MySQL数据库
  6. 为VMware虚拟机内安装的Ubuntu 16.04设置静态IP地址
  7. 三菱PLC仿真软件的序列号
  8. 三菱plc指令dediv_三菱PLC指令[]学习指导书.ppt
  9. 支持javascript的ppt软件_14款基于Javascript的数据可视化工具
  10. 生物化学-第二章-氨基酸
  11. 爬小猪短租发布的房子信息
  12. Android Nginx Retrofit+Okhttp.HTTP 504 Unsatisfiable Request (only-if-cached)
  13. 2021年高考成绩查询湖南电信,湖南省通信管理局关于开展2021年全省电信网码号资源年报和检查工作的通知...
  14. vimdiff 使用笔记
  15. 游戏中的造型师——3D美术
  16. mosquitto 群晖下载_从此“不再”登陆PT站!RSS订阅+qbittorrent自动下载使用教程
  17. DELL笔记本拆机添加内存条
  18. 量化股票是什么意思?
  19. Linux(Ubuntu/Deepin/UOS)安装显卡驱动(附卸载)
  20. POP / IMAP 服务器的构建( Dovecot )

热门文章

  1. LeetCode 1108. Defanging an IP Address--C++,Python解法
  2. 手机php环境的openss_php开启openssl的方法
  3. java实现随机字母数字验证码
  4. C++中substr()函数用法详解
  5. mysql 主键选择_mysql – 无论列名如何,都选择主键?
  6. python模拟键盘输入字符_Python | 根据关键字符串遍历窗口 模拟键盘输入操作
  7. java不同的数据源如何处理_java – 如何在不同的数据源上创建两个类之间的关系?...
  8. 贪吃蛇python零基础教程_自学python-tkinter项目-贪吃蛇的程序(0基础入门学习)...
  9. linux查看设备内存代码,Linux下内存查看命令(示例代码)
  10. laravel 中间件不生效_laravel中间件实战(luke)