Linux中listen()系统调用的backlog参数分析

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

  1. int reqsk_queue_alloc(struct request_sock_queue *queue,  网络

  2.               unsigned int nr_table_entries)  socket

  3. {  tcp

  4.     .......  ide

  5.       

  6.     nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);  函数

  7.     nr_table_entries = max_t(u32, nr_table_entries, 8);  测试

  8.     nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);  

  9.     ......  

  10.       

  11.     ......  

  12.       

  13.     for (lopt->max_qlen_log = 3;  

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

  15.          lopt->max_qlen_log++);  

  16.       

  17.     ......  

  18.   

  19. }  


  前面的三行代码是调整存储半链接的哈希表的大小,能够看到这个值还受系统配置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.       

  5.     sk->sk_max_ack_backlog = backlog;  

  6.     err = 0;  

  7.   

  8. out:  

  9.     release_sock(sk);  

  10.     return err;  

  11. }  

  接下来咱们看若是链接队列满了的话,内核会如何处理。先写个测试程序,构造链接队列满的状况。测试程序说明以下:

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


   二、服务器端在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.   

  7.   

  8.  /* wait connection */  

  9.  while (1) {  

  10.          sleep(3);  

  11.  }  

  12.  ........  



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


[cpp] view plaincopy

  1.  ......  

  2.   

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

  4.  if (ret < 0) {  

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

  6.      return -1;  

  7.  }  

  8.   

  9.  connections[i] = fd;  

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

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

  12.  if (i == 400) {  

  13.      len = sizeof(sa);  

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

  15.      if (ret < 0) {  

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

  17.          return -1;  

  18.      }  

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

  20.        

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

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

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

  24.   

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

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

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

  28.  }  

  29. .......  

  在启动测试程序以前,在客户端使用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.   

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

  6. #ifdef CONFIG_SYN_COOKIES   

  7.         if (sysctl_tcp_syncookies) {  

  8.             want_cookie = 1;  

  9.         } else  

  10. #endif  

  11.         goto drop;  

  12.     }  

  13.   

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

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

  16.      * clogging syn queue with openreqs with exponentially increasing 

  17.      * timeout. 

  18.      */  

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

  20.         goto drop;  

  21.   

  22.     req = inet_reqsk_alloc(&tcp_request_sock_ops);  

  23.     if (!req)  

  24.         goto drop;                                                                  ......                                                                            

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

  26.         goto drop_and_free;  

  27.   

  28.     inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);  

  29.     return 0;  

  30.   

  31. drop_and_release:  

  32.     dst_release(dst);  

  33. drop_and_free:  

  34.     reqsk_free(req);  

  35. drop:  

  36.     return 0;  

  37. }  

咱们主要看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.       

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

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

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

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

  15.      * socket is created, wait for troubles. 

  16.      */  

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

  18.     if (child == NULL)  

  19.         goto listen_overflow;  

  20.           

  21.      .......  

  22.   

  23. listen_overflow:  

  24.     if (!sysctl_tcp_abort_on_overflow) {  

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

  26.         return NULL;  

  27.     }  

  28.   

  29.     ......  

  30. }  

  在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,到这里咱们的分析算是完全结束了,在分析的过程当中忽略了一些细节的东西,感兴趣的能够本身结合源码看一看。

相关文章
相关标签/搜索