> 技术文档 > TCP 半连接队列和全连接队列详解(结合 Linux 2.6.32 内核源码分析)_linux查看全连接和半连接

TCP 半连接队列和全连接队列详解(结合 Linux 2.6.32 内核源码分析)_linux查看全连接和半连接


文章目录

  • 一、什么是 TCP 半连接队列和全连接队列
    • 注意
  • 二、TCP 全连接队列
    • 1、为什么全连接队列要设计成链表?
    • 2、如何查看进程的 TCP 全连接队列大小?
      • 注意
    • 3、TCP 全连接队列溢出问题
      • 注意
    • 4、TCP 全连接队列最大长度
  • 三、TCP 半连接队列
    • 1、为什么半连接队列要设计成哈希表?
    • 2、TCP 半连接队列溢出问题
    • 3、TCP 半连接队列最大长度
      • 注意

一、什么是 TCP 半连接队列和全连接队列

TCP 三次握手期间,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列
    • 服务端收到第一次握手报文后,将该 socket 放进半连接队列
    • 半连接队列中的 socket 都处于 SYN_RCVD 状态
  • 全连接队列,也称 Accept 队列
    • 服务端收到第三次握手报文后,从半连接队列取出对应 socket 放进全连接队列
    • 全连接队列中的 socket 都处于 ESTABLISHED 状态,等着被 accept() 取走
      TCP 半连接队列和全连接队列详解(结合 Linux 2.6.32 内核源码分析)_linux查看全连接和半连接

注意

  • 服务端调用 listen(),Linux 内核会为当前处于 LISTEN 状态的 socket 分配两个队列,半连接队列和全连接队列
  • 虽然都叫队列,但其实全连接队列是个链表,而半连接队列是个哈希表
  • 不管是半连接队列还是全连接队列,都有最大长度限制

二、TCP 全连接队列

1、为什么全连接队列要设计成链表?

全连接队列存放的都是经过完整三次握手建立好连接的 socket,这些 socket 正等着被 accept() 取走,而取走的过程中,并不关心具体是哪个 socket,只要是个 socket 就行,所以直接从队列头一取就行了,时间复杂度为 O(1)

2、如何查看进程的 TCP 全连接队列大小?

# -l:显示正在监听(listening)的 socket# -n:不解析服务名称# -t:只显示 tcp socket$ ss -lnt | grep 55535State Recv-Q Send-Q Local Address:Port Peer Address:PortLISTEN 0 5  192.168.5.28:55535 *:*

注意

ss 命令的 Recv-Q/Send-Q 在「LISTEN 状态」和「非 LISTEN 状态」所表达的含义是不同的

/* Linux Kernel 2.6.32 tcp_diag.c */static void tcp_diag_get_info( ... ){ ... /* LISTEN 状态 */ if (sk->sk_state == TCP_LISTEN) { /* 当前全连接队列大小 */ r->idiag_rqueue = sk->sk_ack_backlog; /* 当前全连接队列最大长度 */ r->idiag_wqueue = sk->sk_max_ack_backlog; } else { /* 已收到但未被进程读取的字节数 */ r->idiag_rqueue = tp->rcv_nxt - tp->copied_seq; /* 已发送但未收到确认的字节数 */ r->idiag_wqueue = tp->write_seq - tp->snd_una; } ...}

「LISTEN 状态」的 Recv-Q/Send-Q 含义如下:

  • Recv-Q:当前全连接队列大小
  • Send-Q:当前全连接队列最大长度

3、TCP 全连接队列溢出问题

Linux 内核有个参数 tcp_abort_on_overflow,可以指定在 TCP 全连接队列满了的情况下使用什么策略回应客户端

/proc/sys/net/ipv4/tcp_abort_on_overflow 共有两个值:

  • 0:全连接队列满了的情况下,服务端丢弃客户端发来的 ACK 报文,并重传第二次握手报文,详见实战 - 搞懂 listen() 参数 backlog 的具体含义(附代码示例)
    • 如果全连接队列一直没有空位,并且重传次数超过 tcp_synack_retries,服务端就会断开连接,如果半连接队列有对应的 socket,一并删除
  • 1:全连接队列满了的情况下,服务端回给客户端 RST 报文

注意

通常情况下,应该把参数 tcp_abort_on_overflow 设为 0,如果服务端只是因为短暂的繁忙致使全连接队列满了,那么当全连接队列有空位时,后续接收到的第三次握手报文仍会触发服务端建立 TCP 连接,所以,tcp_abort_on_overflow=0 可以提高连接建立的成功率

4、TCP 全连接队列最大长度

min(somaxconn,backlog),其中,somaxconn 是 Linux 内核参数,可以通过 /proc/sys/net/core/somaxconn 调整,而 backlog 是函数 listen() 参数

/* Linux Kernel 2.6.32 socket.c */SYSCALL_DEFINE2(listen, int, fd, int, backlog){ ... /* /proc/sys/net/core/somaxconn */ somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn; /* TCP 全连接队列最大长度 */ if ((unsigned)backlog > somaxconn) backlog = somaxconn; ...}

三、TCP 半连接队列

1、为什么半连接队列要设计成哈希表?

半连接队列存放的都是未经过完整三次握手建立好连接的 socket,这些 socket 正等着第三次握手的到来,现在有一个第三次握手来了,需要从半连接队列取出对应 socket,如果半连接队列也是个链表,就得从队列头依次遍历才能找到我们想要的那个 socket,时间复杂度为 O(n),如果将半连接队列设计成哈希表,那么查找算法的时间复杂度为 O(1)

2、TCP 半连接队列溢出问题

  1. 如果半连接队列满了,并且没有开启 tcp_syncookies,丢弃
  2. 如果全连接队列满了,并且没有重传 SYN+ACK 的连接请求多于 1 个,丢弃
  3. 如果没有开启 tcp_syncookies,并且 tcp_max_syn_backlog - 当前半连接队列长度 > 2),丢弃
/* Linux Kernel 2.6.32 tcp_ipv4.c * TCP 第一次握手的 Linux 内核代码 */int tcp_v4_conn_request( ... ){ ... /* 条件 1 * 如果半连接队列满了 */ if (inet_csk_reqsk_queue_is_full(sk) && !isn) {#ifdef CONFIG_SYN_COOKIES if (sysctl_tcp_syncookies) { want_cookie = 1; } else#endif /* 如果半连接队列满了,并且没有开启 tcp_syncookies,丢弃 */ goto drop; } /* 条件 2 * 如果全连接队列满了,并且没有重传 SYN+ACK 的连接请求多于 1 个,丢弃 */ if (sk_acceptq_is_full(sk) && inet_csk_reqsk_queue_young(sk) > 1) goto drop; ... if (want_cookie) { ... } else if (!isn) { ... if ( ... ) { ... } /* 条件 3 * 如果没有开启 tcp_syncookies, * 并且 tcp_max_syn_backlog - 当前半连接队列长度 > 2), * 丢弃 */ else if (!sysctl_tcp_syncookies && sysctl_max_syn_backlog - inet_csk_reqsk_queue_len(sk) < (sysctl_max_syn_backlog >> 2)) && ...) { ... goto drop_and_release; } ... } ...}

3、TCP 半连接队列最大长度

tcp_max_syn_backlog < min(somaxconn,backlog) ? tcp_max_syn_backlog * 2 : min(somaxconn,backlog) * 2,其中,tcp_max_syn_backlog 是 Linux 内核参数,可以通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 调整
TCP 半连接队列和全连接队列详解(结合 Linux 2.6.32 内核源码分析)_linux查看全连接和半连接

注意

半连接队列最大长度并不代表服务端处于 SYN_RCVD 状态的最大个数

我们在上述内容中总结过 TCP 第一次握手报文被丢弃的三种条件:

  1. 如果半连接队列满了,并且没有开启 tcp_syncookies,丢弃
  2. 如果全连接队列满了,并且没有重传 SYN+ACK 的连接请求多于 1 个,丢弃
  3. 如果没有开启 tcp_syncookies,并且 tcp_max_syn_backlog - 当前半连接队列长度 > 2),丢弃

假设条件一不成立,也就是当前半连接队列长度没有超,而后的条件二也不成立,一旦满足条件三,”半连接队列最大长度并不代表服务端处于 SYN_RCVD 状态的最大个数“ 结论正确,例如 somaxconn = 128, backlog = 511,tcp_max_syn_backlog = 256,计算出半连接队列最大长度是 256,但是按照假设,一旦当前半连接队列长度 > tcp_max_syn_backlog - (tcp_max_syn_backlog >> 2),也就是 256 - 64 = 192,SYN 报文就会被丢弃,那么此时处于 SYN_RCVD 状态的最大个数是 193,而非 256