Linux 面试题:TCP 三次握手、四次挥手在内核中的实现要点

TCP 三次握手与四次挥手是网络编程面试中的高频考点。大多数候选人能背出状态转换图,但一旦追问“内核具体怎么实现的”“半连接队列和全连接队列有什么区别”“为什么挥手需要四次”,区分度就出来了。这篇文章从内核实现的角度,把这两个过程拆开讲清楚。

一、三次握手的内核实现

1.1 从 socket 到 listen:准备工作

服务端调用 socket() 创建套接字,bind() 绑定地址端口,listen() 将套接字转为被动监听状态。listen(fd, backlog) 中的 backlog 参数决定了全连接队列的最大长度(内核 4.6 之后语义有所调整,但面试中按经典理解即可)。

内核为监听套接字维护两个关键队列:

  • 半连接队列(SYN Queue) :收到 SYN、尚未完成三次握手的连接
  • 全连接队列(Accept Queue) :已完成三次握手、等待 accept() 取走的连接

1.2 第一步:客户端发送 SYN

客户端调用 connect(),内核构造 SYN 报文,携带客户端初始序列号(ISN)。客户端进入 SYN_SENT 状态。

ISN 不是从 0 或固定值开始,而是基于时钟和四元组哈希生成的,防止序列号预测攻击。

1.3 第二步:服务端回复 SYN+ACK

服务端收到 SYN 后,内核做几件事:

  1. 检查半连接队列是否已满。如果满了且未开启 syncookies,直接丢弃该 SYN(客户端会超时重传)。
  2. 创建一个 request_sock 结构(轻量级,不是完整的 socket),放入半连接队列。
  3. 回复 SYN+ACK,携带服务端自己的 ISN 和对客户端 ISN 的确认。

此时服务端处于 SYN_RECV 状态。

1.4 第三步:客户端回复 ACK

客户端收到 SYN+ACK 后,发送 ACK,进入 ESTABLISHED 状态。

服务端收到这个 ACK 后,内核从半连接队列取出对应的 request_sock,创建完整的 sock 结构,将其放入全连接队列,连接状态变为 ESTABLISHED。此时连接已经就绪,等待应用层调用 accept() 取走。

1.5 面试常问的两个队列问题

半连接队列满了怎么办?

  • 默认行为:丢弃新来的 SYN,客户端会重传
  • 如果 net.ipv4.tcp_syncookies=1,内核会启用 SYN Cookie:不分配 request_sock,而是将连接信息编码进 SYN+ACK 的序列号中,收到 ACK 后再解码还原。这可以抵御 SYN Flood 攻击。

全连接队列满了怎么办?

  • 默认行为:丢弃客户端的 ACK(或根据 tcp_abort_on_overflow 参数决定是否发 RST)
  • 客户端认为自己已建立连接,发数据时才发现服务端不认,触发重传
  • 可以通过 ss -lnt 查看 Send-Q 列(全连接队列大小)和 Recv-Q 列(当前排队数量)
# 查看全连接队列溢出情况
netstat -s | grep -i "listen"
# 或
nstat -az | grep -i listen

二、四次挥手的内核实现

2.1 为什么需要四次

TCP 是全双工的,每个方向需要独立关闭。主动关闭方发送 FIN 表示“我没有数据要发了”,但对方可能还有数据要发,所以 ACK 和 FIN 不能合并(除非对方恰好也没有数据要发了)。

2.2 第一步:主动关闭方发送 FIN

应用层调用 close()shutdown(SHUT_WR),内核发送 FIN 报文,主动关闭方进入 FIN_WAIT_1 状态。

注意:close() 会同时关闭读写两个方向,而 shutdown(SHUT_WR) 只关闭写方向,仍可读取对方数据。这个区别在面试中经常被问到。

2.3 第二步:被动关闭方回复 ACK

被动关闭方内核收到 FIN,回复 ACK,进入 CLOSE_WAIT 状态。

此时连接处于半关闭状态:主动关闭方进入 FIN_WAIT_2,被动关闭方还可以继续发送数据。

CLOSE_WAIT 状态如果大量堆积,通常意味着应用程序没有正确调用 close(),这是常见的线上问题。

2.4 第三步:被动关闭方发送 FIN

被动关闭方应用层处理完数据后调用 close(),内核发送 FIN,进入 LAST_ACK 状态。

2.5 第四步:主动关闭方回复 ACK

主动关闭方收到 FIN,回复 ACK。此时主动关闭方进入 TIME_WAIT 状态,等待 2MSL(报文最大生存时间的两倍)后才真正关闭。

被动关闭方收到 ACK 后,连接彻底关闭。

2.6 TIME_WAIT 的内核意义

TIME_WAIT 不是设计缺陷,它有两个核心作用:

  1. 确保最后一个 ACK 能到达对方:如果 ACK 丢失,对方会重传 FIN,处于 TIME_WAIT 的一方还能重新回复 ACK。
  2. 让旧连接的延迟报文在网络中消散:防止新旧连接四元组相同导致数据混淆。

内核相关参数:

# TIME_WAIT 状态持续时间(默认 60 秒)
cat /proc/sys/net/ipv4/tcp_fin_timeout

# 允许 TIME_WAIT 状态的 socket 被新连接复用
cat /proc/sys/net/ipv4/tcp_tw_reuse

# 快速回收 TIME_WAIT(已在新内核中移除,不建议使用)
# net.ipv4.tcp_tw_recycle 在 4.12 内核后被删除

三、内核中的关键数据结构

面试中如果被问到“内核用什么结构管理这些连接”,可以这样回答:

  • struct sock:完整的套接字结构,代表一个 established 连接
  • struct request_sock:半连接阶段的轻量结构,只保存握手必需的信息
  • struct tcp_sock:TCP 专用扩展,包含序列号、窗口、拥塞控制状态等
  • ehash / lhash:内核用哈希表管理 established 连接(ehash)和监听套接字(lhash)

三次握手的本质,就是内核从 request_sock 升级到完整 sock 的过程。

四、面试答题思路总结

遇到这类问题,建议按以下层次作答:

  1. 先画状态转换图,说清楚每个状态的进入和退出条件
  2. 再讲内核行为:队列管理、数据结构、异常处理
  3. 最后联系实际问题:SYN Flood、CLOSE_WAIT 堆积、TIME_WAIT 过多怎么排查和调优

能把这三层都讲清楚,基本可以证明你不仅背过八股文,而且真正理解 TCP 在内核中的运作方式。

未经允许不得转载:任鹏个人博客 » Linux 面试题:TCP 三次握手、四次挥手在内核中的实现要点

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏