Linux 4.4 之后 TCP 三路握手的新流程

陈硕·2017/02/20·不得转载

从 Linux 4.4 开始,TCP 协议栈新增了一个状态:NEW_SYN_RECV,并且

提高了 TCP 的伸缩性

,本文大致描述一下现在的 TCP 三路握手流程。

数据结构

Linux 网络协议栈中的 TCP 协议控制块(protocol control block)有三种:tcp_request_sock、tcp_sock、tcp_timewait_sock。如果用面向对象的方式来描述,它们的继承关系如下图。

Linux 的 TCP 协议栈用全局的 3 个哈希表(位于 tcp_hashinfo 对象,定义于 net/ipv4/tcp_ipv4.c)来管理全部的 TCP 协议控制块。

  • ehash 负责有名有姓(source IP/source port/destination IP/destination port 俱全)的 socket,其中 e 表示 established。以 C++ 来说,ehash 相当于unordered_map<Key, sock*>,其中 Key 是 struct Key { uint32_t source_ip, destination_ip; uint16_t source_port, destination_port; }; ,而 sock* 可以指向 tcp_request_sock、tcp_sock、tcp_timewait_sock 之一。
  • bhash 负责端口分配,其中 b 表示 bind。以 C++ 来说,bhash 的功能相当于 map<uint16_t, list<tcp_sock*>>,其中的 list 对应 inet_bind_bucket。(更准确的描述是 unordered_map<uint16_t, forward_list<tcp_sock*>>。)
  • listening_hash 负责侦听(listening) socket。

ehash 和 bhash 的 bucket 数量是在内核启动的时候确定(有参数可以配置),不会更改(即不会rehash)。而 listening_hash 的 bucket 数量是在编译内核时确定,通常是 32 个。

ehash 和 bhash 的具体大小可以从 dmesg 得到,例如在陈硕家的一台机器上,ehash 有 0.5Mi 个 bucket,而 bhash 有 64Ki 个 bucket。(bhash 不会多于 64Ki 项,因此这时 bhash 实际上可以看成 inet_bind_bucket 的指针数组,而不是哈希表。)

[    0.354971] NET: Registered protocol family 2
[    0.355392] TCP established hash table entries: 524288 (order: 10, 4194304 bytes)
[    0.355844] TCP bind hash table entries: 65536 (order: 8, 1048576 bytes)
[    0.355947] TCP: Hash tables configured (established 524288 bind 65536)
[    0.355963] TCP: reno registered
[    0.356008] UDP hash table entries: 32768 (order: 8, 1048576 bytes)

注意为了抵御算法复杂度攻击(拒绝服务攻击的一种,通过构造 hash collisions,让哈希表退化为链表,使得原本 O(1) 的操作变成 O(N),从而消耗服务器 CPU 资源,造成响应迟缓甚至超时),ehash 使用的哈希函数有一个安全种子,这样就很难人为大量制造 hash collisions 了。

接下来我们看看这几个哈希表是起什么作用的,在后文的示意图中,ehash 和 bhash 各有 1024 个 bucket。阅读以下两节请同时参考题图中的 TCP 状态转换图,括号中的数字是 TCP state enum 对应的数值,在单步跟踪源码的时候可依据 sk_state 变量的值得知 sock 所处的状态。

客户端主动发起连接

基本流程与 4.4 以前的版本一样:

  1. 调用 socket(2) 创建 TCP socket,tcp_sock 处于 CLOSE 状态。(图二)

  2. 调用 connect(2) 发起连接,tcp_sock 进入 SYN_SENT 状态。
    1. 选择 ephemeral port
    2. 创建 inet_bind_bucket 并加入 bhash
    3. 将 tcp_sock 加入 ehash
    4. 发送 SYN segment
      这时的数据结构:
  3. 收到服务器发来的 SYN+ACK segment。
    1. 从 ehash 中找到 tcp_sock
    2. 完成握手,进入 ESTABLISHED 状态。数据结构不变,图略。
    3. 发送 ACK segment

思考:如果对调 2.3 和 2.4 这两个步骤会有什么 race condition ?

服务器被动接受连接

  1. 调用 socket(2) 创建 TCP socket,tcp_sock 处于 CLOSE 状态。数据结构如图二。
  2. 调用 bind(2) 将 tcp_sock 加入 bhash。
  3. 调用 listen(2) 将 tcp_sock 设为 LISTEN 状态,并加入 listening_hash。
    这时数据结构的状态如下图:
    原图:chenshuo.github.io/note
  4. 收到 SYN segment,从 listening_hash 找到 listen tcp_sock,创建 tcp_request_sock(NEW_SYN_RECV 状态),并将其加入 ehash,然后发送 SYN+ACK。
    原图:chenshuo.github.io/note
  5. 收到 ACK segment,从 ehash 中找到 tcp_request_sock,顺藤摸瓜找到 listen tcp_sock,创建新的 tcp_sock(SYN_RECV 状态),将其链入 listen tcp_sock 所属的 inet_bind_bucket 中,然后用它替换 tcp_request_sock 在 ehash 中的位置。接下来把 tcp_request_sock 加入 listen socket 的 accept queue 中,最后经过一系列处理,把新 tcp_sock 设为 ESTABLISHED 状态,可以接收数据了。
  6. 用户程序调用 accept(2),从 listen socket 的 accept queue 中取出新 tcp_sock,销毁 tcp_request_sock,然后创建 file/dentry/socket_alloc 等对象将 tcp_sock 加入进程的文件描述符表中,返回相应的文件描述符。

与以前的版本相比,第 4 步改动最大,原来是把 tcp_request_sock 挂在 listen socket 下,收到 ACK 之后从 listening_hash 找到 listen socket 再进一步找到 tcp_request_sock;新的做法是直接把 tcp_request_sock 挂在 ehash 中,这样收到 ACK 之后可以直接找到 tcp_request_sock,减少了锁的争用(contention)。具体的 commit 是:


commit 079096f103faca2dd87342cca6f23d4b34da8871
Author: Eric Dumazet <[email protected]>
Date:   Fri Oct 2 11:43:32 2015 -0700

    tcp/dccp: install syn_recv requests into ehash table

    In this patch, we insert request sockets into TCP/DCCP
    regular ehash table (where ESTABLISHED and TIMEWAIT sockets
    are) instead of using the per listener hash table.

    ACK packets find SYN_RECV pseudo sockets without having
    to find and lock the listener.

    In nominal conditions, this halves pressure on listener lock.

    Note that this will allow for SO_REUSEPORT refinements,
    so that we can select a listener using cpu/numa affinities instead
    of the prior 'consistent hash', since only SYN packets will
    apply this selection logic.

    We will shrink listen_sock in the following patch to ease
    code review.

    Signed-off-by: Eric Dumazet <[email protected]>
    Cc: Ying Cai <[email protected]>
    Cc: Willem de Bruijn <[email protected]>
    Signed-off-by: David S. Miller <[email protected]>

更具体的调用流程见 Call Graphs - Shuo Chen's Notes

后记

本文没有考虑 SO_REUSEPORT、TCP Fast Open、SYN flood 的情况,只讨论了最传统的 TCP 三路握手流程。

Linux 4.4 ~ 4.9 内核有不少针对TCP/IP网络协议栈的优化,有条件的话不妨试试(Ubuntu 16.04 LTS 搭载的是 4.4 内核,预计今年发布的 Debian 9 会使用 4.9 内核),或许能解决一些令人头疼的问题。

编辑于 2017-02-21 12:48

Published

Category

Zhihu

Tags