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

  这篇文章是对上一篇博客网络编程常用接口的内核实现----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()中计算并设置的,代码片段如下所示:

int reqsk_queue_alloc(struct request_sock_queue *queue,
		      unsigned int nr_table_entries)
{
	.......
	
	nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
	nr_table_entries = max_t(u32, nr_table_entries, 8);
	nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
	......
	
	......
	
	for (lopt->max_qlen_log = 3;
	     (1 << lopt->max_qlen_log) < nr_table_entries;
	     lopt->max_qlen_log++);
	
	......

}

  前面的三行代码是调整存储半连接的哈希表的大小,可以看到这个值还受系统配置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()中设置,如下所示:

int inet_listen(struct socket *sock, int backlog)
{
	......
	
	sk->sk_max_ack_backlog = backlog;
	err = 0;

out:
	release_sock(sk);
	return err;
}

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

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

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

       ........
       if ((ret = listen(fd, 300)) < 0) {
                perror("listen");
                goto err_out;
        }


        /* wait connection */
        while (1) {
                sleep(3);
        }
        ........

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

        ......

        ret = connect(fd, (struct sockaddr *)&sa, sizeof(sa));
        if (ret < 0) {
            fprintf(stderr, "connect fail in %d times, reason: %s.\n", i + 1, strerror(errno));
            return -1;
        }

        connections[i] = fd;
        fprintf(stderr, "Connection success, times: %d, connections: %d.\n", i + 1, 
                check_connection_count(connections, i + 1));
        if (i == 400) {
            len = sizeof(sa);
            ret = getsockname(fd, (struct sockaddr *)&sa, &len);
            if (ret < 0) {
                fprintf(stderr, "getsockname fail, ret=%d.\n", ret);
                return -1;
            }
            fprintf(stderr, "connecton %d, local port: %u.\n", i,ntohs(sa.sin_port));
            
            str = "if i can write ,times 1";
            ret = write(fd, str, strlen(str));
            fprintf(stderr, "first writ in connection %d, ret = %d.\n", i, ret);

            str = "if i can write ,times 2";
            ret = write(fd, str, strlen(str));
            fprintf(stderr, "second writ in connection %d, ret = %d.\n", i, ret);
        }
       .......
  在启动测试程序之前,在客户端使用tcpdump抓包,并将输出结果通过-w选项存储在192.cap文件中,便于后续使用wireshark来分析。

  测试发现,在客户端建立300个连接后,客户端建立连接的速度明显慢了很多,而且最终建立完1000个连接花了20分钟左右。使用wireshark打开192.cap文件,来看抓包的情况,发现在300个连接之后有大量的ack包重传,如下图所示:
Linux中listen()系统调用的backlog参数分析_第1张图片

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

Linux中listen()系统调用的backlog参数分析_第2张图片

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

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

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

	if (inet_csk_reqsk_queue_is_full(sk) && !isn) {
#ifdef CONFIG_SYN_COOKIES 
		if (sysctl_tcp_syncookies) {
			want_cookie = 1;
		} else
#endif
		goto drop;
	}

	/* Accept backlog is full. If we have already queued enough
	 * of warm entries in syn queue, drop request. It is better than
	 * clogging syn queue with openreqs with exponentially increasing
	 * timeout.
	 */
	if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1)
		goto drop;

	req = inet_reqsk_alloc(&tcp_request_sock_ops);
	if (!req)
		goto drop;																	......																			
	if (__tcp_v4_send_synack(sk, req, dst) || want_cookie)
		goto drop_and_free;

	inet_csk_reqsk_queue_hash_add(sk, req, TCP_TIMEOUT_INIT);
	return 0;

drop_and_release:
	dst_release(dst);
drop_and_free:
	reqsk_free(req);
drop:
	return 0;
}
我们主要看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()的代码流程如下图所示:

Linux中listen()系统调用的backlog参数分析_第3张图片

 如果是正确的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标签处执行,如下所示:

/*
 *	Process an incoming packet for SYN_RECV sockets represented
 *	as a request_sock.
 */
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
			   struct request_sock *req,
			   struct request_sock **prev)
{
	......
	
	/* OK, ACK is valid, create big socket and
	 * feed this segment to it. It will repeat all
	 * the tests. THIS SEGMENT MUST MOVE SOCKET TO
	 * ESTABLISHED STATE. If it will be dropped after
	 * socket is created, wait for troubles.
	 */
	child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL);
	if (child == NULL)
		goto listen_overflow;
		
     .......

listen_overflow:
	if (!sysctl_tcp_abort_on_overflow) {
		inet_rsk(req)->acked = 1;
		return NULL;
	}

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

/* Decide when to expire the request and when to resend SYN-ACK */
static inline void syn_ack_recalc(struct request_sock *req, const int thresh,
				  const int max_retries,
				  const u8 rskq_defer_accept,
				  int *expire, int *resend)
{
	if (!rskq_defer_accept) {
		*expire = req->retrans >= thresh;
		*resend = 1;
		return;
	}
	*expire = req->retrans >= thresh &&
		  (!inet_rsk(req)->acked || req->retrans >= max_retries);
	/*
	 * Do not resend while waiting for data after ACK,
	 * start to resend on end of deferring period to give
	 * last chance for data or ACK to create established socket.
	 */
	*resend = !inet_rsk(req)->acked ||
		  req->retrans >= rskq_defer_accept - 1;
}
在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个“连接”的端口号,我们通过这个连接就可以验证我们的结论,其抓包情况如下所示:

Linux中listen()系统调用的backlog参数分析_第4张图片

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

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

你可能感兴趣的:(Linux中listen()系统调用的backlog参数分析)