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

在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,如下所示:

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

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

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

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

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

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

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://www.heiqu.com/30c2be731595075c8993b6af16dcaabb.html