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

三次握手的内核状态机怎么流转
Linux 内核用 sk_state 字段维护 socket 的连接状态,三次握手全程不经过用户态,全在协议栈里完成。关键状态是 TCP_LISTEN → TCP_SYN_RECV → TCP_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_syncookies在net.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_WAITsocket 需显式设置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 就万事大吉。










