Linux TCP 三次握手在内核中发生了什么?

13次阅读

三次握手内核状态流转为TCP_LISTEN→TCP_SYN_RECV→TCP_ESTABLISHED:SYN触发tcp_conn_request()生成request_sock入半连接队列,仅ACK到达才分配完整socket并移入established队列。

Linux TCP 三次握手在内核中发生了什么?

三次握手的内核状态机怎么流转

linux 内核用 sk_state 字段维护 socket 的连接状态,三次握手全程不经过用户态,全在协议里完成。关键状态是 TCP_LISTENTCP_SYN_RECVTCP_ESTABLISHED,但中间有隐藏细节:

  • TCP_LISTEN 状态下,内核把新 SYN 包交给 tcp_conn_request() 处理,不是直接创建新 socket,而是先查 reqsk_queue(半连接队列)是否能容纳
  • 收到 SYN 后,内核生成一个 request_sock 对象,存进半连接队列,此时还没分配 Struct sock;只有收到第三次 ACK 才真正分配完整 socket 并移到 established 队列
  • 如果半连接队列满(由 net.ipv4.tcp_max_syn_backlog 控制),且未启用 syncookies,内核直接丢弃 SYN,不会回 SYN+ACK

syncookies 是怎么绕过半连接队列的

syncookies 不是“开关”,而是一种状态压缩机制:当半连接队列溢出时,内核放弃保存 request_sock,转而把客户端 IP、端口、时间戳、MSS 等信息编码进初始序列号(ISN)中。下次收到 ACK 时,再从 ACK 的确认号里反解出这些信息,验证合法性。

  • 启用条件:必须同时满足 net.ipv4.tcp_syncookies = 1 且半连接队列满(或 net.ipv4.tcp_synack_retries = 0
  • 代价:无法支持 TCP 选项如 SACK、Timestamps(除非开启 net.ipv4.tcp_timestamps = 1 且内核 ≥ 4.4)
  • 注意:tcp_syncookiesnet.ipv4.conf.all.rp_filter = 1 且反向路径校验失败时可能被静默禁用

accept() 系统调用到底在等什么

accept() 不参与三次握手,它只从内核的 established 队列(全连接队列)里取已处于 TCP_ESTABLISHED 状态的 socket。这个队列长度由 listen()backlog 参数和 net.core.somaxconn 共同决定,取二者最小值。

  • 如果全连接队列满,新完成握手的连接会被丢弃(不发 RST),表现为客户端卡在 TCP_SYN_SENT 或超时重传
  • ss -lnt 输出里的 Recv-Q 列显示当前全连接队列积压数,Send-Q 是最大长度
  • 应用层看到 EAGaiN/EWOULDBLOCK 通常不是因为队列满,而是 socket 设为非阻塞后 accept() 暂无就绪连接

TIME_WAIT 和连接复用对握手的影响

客户端主动关闭后进入 TIME_WAIT 状态(持续 2MSL),主要防止延迟到达的旧包干扰新连接。但它不影响新连接的三次握手——只要源端口不同,或目标四元组(src_ip:src_port + dst_ip:dst_port)不完全相同,就能立即建新连接。

  • 复用 TIME_WAIT socket 需显式设置 SO_REUSEADDR(服务端常用),但客户端通常不需要;SO_LINGER 设为 0 强制跳过 TIME_WAIT 有风险,可能丢包
  • 短连接服务若频繁遇到 Cannot assign requested address,大概率是本地端口耗尽,需调大 net.ipv4.ip_local_port_range 或启用端口复用
  • 内核不会因为存在 TIME_WAIT 就拒绝 SYN;真正拦截 SYN 的,通常是防火墙规则、连接跟踪表满(nf_conntrack)、或 SYN flood 防御触发

真正容易被忽略的是半连接队列和全连接队列的独立性——它们由不同参数控制,溢出表现也不同;还有 syncookies 的启用逻辑依赖运行时条件,不是写个 sysctl 就万事大吉。

text=ZqhQzanResources