Skip to content
Go back

TCP四次挥手——为什么TIME_WAIT要等2MSL?

TCP 四次挥手:TIME_WAIT 为什么等 2MSL?

一句话结论(30s)

TCP 挥手要四次而不是三次,是因为 TCP 是全双工的:客户端发 FIN 只表示”我不再发数据了”,但服务端可能还有数据要发。所以服务端先由内核立即回一个 ACK(确认收到你的 FIN),等自己的数据发完、应用调用 close() 后再发自己的 FIN。而主动关闭方最后要等 2MSL,是为了① 确保最后一个 ACK 被对端收到(丢了能重传),② 让旧连接的所有残留报文在网络中彻底死透,避免污染相同四元组的新连接。

核心原理(2min)

先停一秒,问自己:为什么建连要三次,断开却要四次?两者不都是”双方确认”吗? 区别在于”ACK 和 SYN/FIN 是不是同一个人、同一个时刻发出来的”。建连时服务端的 SYN 和 ACK 由内核同时产生,可以合并;断开时 ACK 是内核收到 FIN 立刻回的,FIN 却是应用层调 close() 才发的,中间可能隔着服务端继续发数据的漫长间隙。这个”时间差”就是四次挥手多出来那一脚的根因。带着这个直觉往下读。

底层深入(5-10min)

完整的挥手过程

客户端 (主动关闭方)             服务端 (被动关闭方)
  |                              |
  |──── FIN (seq=u) ────────────→|  ① 客户端: 我发完了,准备关
  |   FIN_WAIT_1                 |  CLOSE_WAIT
  |←─── ACK (ack=u+1) ──────────┤  ② 服务端: 知道了(内核自动回复)
  |   FIN_WAIT_2                 |
  |←─── FIN (seq=v) ────────────┤  ③ 服务端: 我也发完了
  |   (收到) TIME_WAIT           |  LAST_ACK
  |──── ACK (ack=v+1) ──────────→|  ④ 客户端: 收到
  |   TIME_WAIT (等 2MSL)        |  CLOSED
  |   CLOSED                     |

为什么服务端 ACK 和 FIN 不能合并?

客户端发 FIN 表示”我不发了”。但服务端可能还有数据要发——TCP 是全双工的,客户端关了自己的发送通道不代表服务端的发送通道也要关。

ACK 是内核立刻回复的(确认收到客户端 FIN),但 FIN 是服务端应用层决定发送的(close()shutdown() 调用)。 两者时间可能相差很远——如果服务端应用正在处理一个慢查询,FIN 可能要等几十秒。

这就是为什么挥手需要四次:ACK 和 FIN 之间有个时间间隙——那是服务端应用在发完剩余数据。

如果服务端也没数据可发(比如 HTTP Body 已经发完,就是个 GET 请求),可以开启 TCP 延迟确认机制(Delayed ACK),将 ACK 和 FIN 合并,变成”三次挥手”。

内核视角:close() 是如何一步步走到 FIN 的

主动关闭方调 close() 后,内核的 tcp_close_state() 用一个状态机表决定”当前状态该怎么转、要不要发 FIN”。注意表里 TCP_ESTABLISHED → TCP_FIN_WAIT1 | TCP_ACTION_FINTCP_ACTION_FIN 这个标志位就是”发 FIN”的指令:

/*
 *	State processing on a close. This implements the state shift for
 *	sending our FIN frame. Note that we only send a FIN for some
 *	states. A shutdown() may have already sent the FIN, or we may be
 *	closed.
 */

static const unsigned char new_state[16] = {
  /* current state:        new state:      action:	*/
  [0 /* (Invalid) */]	= TCP_CLOSE,
  [TCP_ESTABLISHED]	= TCP_FIN_WAIT1 | TCP_ACTION_FIN,
  [TCP_SYN_SENT]	= TCP_CLOSE,
  [TCP_SYN_RECV]	= TCP_FIN_WAIT1 | TCP_ACTION_FIN,
  [TCP_FIN_WAIT1]	= TCP_FIN_WAIT1,
  [TCP_FIN_WAIT2]	= TCP_FIN_WAIT2,
  [TCP_TIME_WAIT]	= TCP_CLOSE,
  [TCP_CLOSE]		= TCP_CLOSE,
  [TCP_CLOSE_WAIT]	= TCP_LAST_ACK  | TCP_ACTION_FIN,
  [TCP_LAST_ACK]	= TCP_LAST_ACK,
  [TCP_LISTEN]		= TCP_CLOSE,
  [TCP_CLOSING]		= TCP_CLOSING,
  [TCP_NEW_SYN_RECV]	= TCP_CLOSE,	/* should not happen ! */
};

static int tcp_close_state(struct sock *sk)
{
	int next = (int)new_state[sk->sk_state];
	int ns = next & TCP_STATE_MASK;

	tcp_set_state(sk, ns);

	return next & TCP_ACTION_FIN;
}

分析:这张表就是四次挥手”主动关闭方”那一半的状态机。返回值 next & TCP_ACTION_FIN 非零时,调用方才会真正去发 FIN。注意 TCP_CLOSE_WAIT → TCP_LAST_ACK | TCP_ACTION_FIN:这正是被动关闭方收到 FIN、回完 ACK 之后,应用调 close() 时的跃迁——它也要发自己的 FIN,印证了”两个方向各自关闭”。

💭 思考:为什么 close() 要靠一张状态机表来推进,而不是“直接发 FIN 就完事”?——因为同一个 close() 在不同状态下动作完全不同:ESTABLISHED 里要发 FIN,SYN_SENT 里却是“还没建连就放弃”,CLOSE_WAIT 里则要转 LAST_ACK 再发 FIN。把“当前状态 → 下一步状态 + 要不要发 FIN”编成一张表,内核就不用写一堆 if-else,任何状态下 close() 也都有确定归宿。这张表本质就是“主动关闭方那一半”挥手流程的完整拼图。

内核视角:FIN 到底怎么构造和发送

tcp_send_fin() 是发送 FIN 的真正实现。有意思的是它的优化路径:如果写队列里正好有一个还没发出去的 skb,就直接把 FIN 标志”搭”在那个 skb 上(tcp_flags |= TCPHDR_FIN),而不是单独发一个空包:

void tcp_send_fin(struct sock *sk)
{
	struct sk_buff *skb, *tskb, *tail = tcp_write_queue_tail(sk);
	struct tcp_sock *tp = tcp_sk(sk);

	/* Optimization, tack on the FIN if we have one skb in write queue and
	 * this skb was not yet sent, or we are under memory pressure.
	 * Note: in the latter case, FIN packet will be sent after a timeout,
	 * as TCP stack thinks it has already been transmitted.
	 */
	tskb = tail;
	if (!tskb && tcp_under_memory_pressure(sk))
		tskb = skb_rb_last(&sk->tcp_rtx_queue);

	if (tskb) {
		TCP_SKB_CB(tskb)->tcp_flags |= TCPHDR_FIN;
		TCP_SKB_CB(tskb)->end_seq++;
		tp->write_seq++;
		if (!tail) {
			/* This means tskb was already sent.
			 * Pretend we included the FIN on previous transmit.
			 * We need to set tp->snd_nxt to the value it would have
			 * if FIN had been sent. This is because retransmit path
			 * does not change tp->snd_nxt.
			 */
			WRITE_ONCE(tp->snd_nxt, tp->snd_nxt + 1);
			return;
		}
	} else {
		skb = alloc_skb_fclone(MAX_TCP_HEADER,
				       sk_gfp_mask(sk, GFP_ATOMIC |
						       __GFP_NOWARN));
		if (unlikely(!skb))
			return;

		INIT_LIST_HEAD(&skb->tcp_tsorted_anchor);
		skb_reserve(skb, MAX_TCP_HEADER);
		sk_forced_mem_schedule(sk, skb->truesize);
		/* FIN eats a sequence byte, write_seq advanced by tcp_queue_skb(). */
		tcp_init_nondata_skb(skb, sk, tp->write_seq,
				     TCPHDR_ACK | TCPHDR_FIN);
		tcp_queue_skb(sk, skb);
	}
	__tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF);
}

分析:注意 end_seq++ 和注释 “FIN eats a sequence byte”——FIN 和 SYN 一样占用一个序号,所以对端必须用 ack = seq + 1 确认它,这也是四次挥手里每一步 ack=u+1ack=v+1 的来历。最后 __tcp_push_pending_frames 把 FIN 真正推上链路。

内核视角:close() 的完整决策链

__tcp_close() 是应用 close() 的入口,它先检查接收队列里还有没有没读完的数据,再决定走 RST 还是走正常的 FIN 挥手:

	/*  We need to flush the recv. buffs.  We do this only on the
	 *  descriptor close, not protocol-sourced closes, because the
	 *  reader process may not have drained the data yet!
	 */
	while ((skb = skb_peek(&sk->sk_receive_queue)) != NULL) {
		u32 end_seq = TCP_SKB_CB(skb)->end_seq;

		if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
			end_seq--;
		if (after(end_seq, tcp_sk(sk)->copied_seq))
			data_was_unread = true;
		tcp_eat_recv_skb(sk, skb);
	}

	/* If socket has been already reset (e.g. in tcp_reset()) - kill it. */
	if (sk->sk_state == TCP_CLOSE)
		goto adjudge_to_death;

	...
	if (unlikely(tcp_sk(sk)->repair)) {
		sk->sk_prot->disconnect(sk, 0);
	} else if (data_was_unread) {
		/* Unread data was tossed, zap the connection. */
		NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
		tcp_set_state(sk, TCP_CLOSE);
		tcp_send_active_reset(sk, sk->sk_allocation,
				      SK_RST_REASON_TCP_ABORT_ON_CLOSE);
	} else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
		/* Check zero linger _after_ checking for unread data. */
		sk->sk_prot->disconnect(sk, 0);
		NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
	} else if (tcp_close_state(sk)) {
		/* We FIN if the application ate all the data before
		 * zapping the connection.
		 */
		...
		tcp_send_fin(sk);
	}

	sk_stream_wait_close(sk, timeout);

分析:这里藏着一个容易忽略的知识点——不是所有 close() 都走四次挥手。如果接收队列还有没读的数据(data_was_unread),内核会直接发 RST 而不是 FIN,因为”你都没读完整数据,我不可能优雅挥手”。只有数据读干净了,tcp_close_state() 返回 FIN 指令,才会走到 tcp_send_fin() 开始四次挥手。

💭 思考:为什么“没读完数据就 close”会直接发 RST,而不是走四次挥手?——因为四次挥手的前提是“数据已经完整、干净地收发完”,它要靠 FIN/ACK 一步步确认。如果接收队列里还躺着没读的数据就关闭,等于告诉对端“你的数据我不要了”,此时再优雅地 FIN 挥手已经没有意义,对端只会误以为数据都被正常接收。所以内核宁可发 RST 明确告知“连接异常终止”,也不让对端产生“数据都送达了”的错觉。

TIME_WAIT 为什么等 2MSL?

MSL(Maximum Segment Lifetime)= 报文在网络中的最大存活时间。Linux 默认 30 秒,所以 TIME_WAIT = 60 秒。

先设问:既然最后一条 ACK 都发出去了,直接 CLOSED 不香吗?多等 60 秒图什么? 图的是”绝对安全”。ACK 发出去不代表对方一定收到——它也可能丢。而且这条连接上所有还在路上飘的旧报文,要等它们全部”死透”才敢把同一个四元组让给新连接。这两个担忧,正好对应下面两个目的。

两个目的:

1. 确保最后的 ACK 被对方收到

如果最后 ACK 丢失,服务端会重发 FIN。TIME_WAIT 期间客户端可以重新回复 ACK。2MSL = “最后一个 ACK 的最大存活时间 + 服务端重发 FIN 的最大存活时间”。

如果 ACK 丢失:
  客户端: 发 ACK → TIME_WAIT 等待
  服务端: 没收到 ACK → 超时重发 FIN
  客户端: 收到 FIN → 重发 ACK → 重置 2MSL 计时器

2. 确保旧连接的所有报文在网络中”死透”

一个 TCP 连接由四元组(src_ip, src_port, dst_ip, dst_port)唯一标识。如果客户端立即从 TIME_WAIT 切换到 CLOSED 并重建相同四元组的连接,网络中还没到达的旧连接数据报文会被当成新连接的合法数据接收——造成数据错乱。2MSL 等待确保所有旧报文都已超时被丢弃。

内核视角:2MSL 是一个写死的 60 秒常量

TIME_WAIT 的时长在内核里不是临时算出来的,而是一个编译期常量 TCP_TIMEWAIT_LEN = 60*HZ(HZ 是每秒的时钟中断数,60*HZ 即 60 秒):

#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
				  * state, about 60 seconds	*/
#define TCP_FIN_TIMEOUT	TCP_TIMEWAIT_LEN

分析60*HZ 里的 60 秒,就是 2 个 MSL(Linux 里 MSL 取 30 秒)。TIME_WAIT 状态的实际定时器在 tcp_minisocks.c 里反复用 inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN) 排期——也就是说,“等 2MSL”不是等待一个真实的丢包,而是保守地给旧报文留足消亡时间。

💭 思考:为什么 2MSL 要写死成 60 秒常量,而不是像 RTO 那样动态算?——因为 RTO 关心的是“这条连接当前的往返时间”,会随网络波动;而 TIME_WAIT 要防的是“全网任意迷路报文的最长寿命”,这是一个与具体连接无关的全局上限,取 MSL 的两倍就够了。写死反而简单可靠:不需要采样、不需要调参,任何主动关闭方都统一等满 60 秒,保证最坏情况下的安全。

内核视角:对端 LAST_ACK 收到最后 ACK 后的收尾

服务端(被动关闭方)发完 FIN 进入 LAST_ACK,当收到客户端最后的 ACK 时,tcp_rcv_state_process()TCP_LAST_ACK 分支判定 snd_una == write_seq(即 FIN 已被确认)后直接销毁连接:

	case TCP_LAST_ACK:
		if (tp->snd_una == tp->write_seq) {
			tcp_update_metrics(sk);
			tcp_done(sk);
			goto consume;
		}
		break;

分析snd_una == write_seq 是”我发出去的所有字节(含 FIN 占的那个序号)都已被确认”的判据。一旦成立,tcp_done() 直接把 socket 清掉——被动关闭方这边没有 TIME_WAIT,它确认完就干净退场。而 TIME_WAIT 只落在主动关闭方身上,这也是”大量 TIME_WAIT 往往出现在主动 close 的服务端”的原因。

服务端大量 TIME_WAIT 怎么办?

服务端主动关闭连接才会产生 TIME_WAIT。常见的主动关闭方是客户端(浏览器),但以下场景服务端会成为主动关闭方:

  1. HTTP 长连接超时keepalive_timeout 到期,Nginx 主动断开
  2. 连接数达上限keepalive_requests 到上限,Nginx 主动断开
  3. HTTP 短连接:每次请求后 Nginx 关闭

优化措施

# 1. 开启 TIME_WAIT 复用(让新连接复用 TIME_WAIT 状态的端口)
net.ipv4.tcp_tw_reuse = 1

# 2. 调整 Nginx 减少主动关闭
keepalive_timeout 120;          # 长连接保持更久
keepalive_requests 10000;       # 单连接处理更多请求

# 3. 扩大 TIME_WAIT 可追踪的连接数
net.ipv4.tcp_max_tw_buckets = 262144

总结

状态持有方含义
FIN_WAIT_1主动关闭方已发 FIN,等对方 ACK
FIN_WAIT_2主动关闭方已收对方 ACK,等对方 FIN
CLOSE_WAIT被动关闭方已收 FIN 并回了 ACK,等应用调 close()
LAST_ACK被动关闭方已发 FIN,等对方最后 ACK
TIME_WAIT主动关闭方等 2MSL,确保连接彻底消亡

章末提问

追问 1:为什么挥手是四次,不能像握手那样合并成三次? 结论先行——因为中间那两次的触发者不同、时间点也可能差很远。第二次 ACK 是内核收到 FIN 后立刻自动回复的(协议栈行为);第三次 FIN 却是服务端应用调 close()/shutdown() 才触发的(应用行为)。两者之间可能隔着服务端继续发数据的漫长间隙,所以无法合并。只有当服务端确实没有数据要发时,Delayed ACK 才会把 ACK 和 FIN 合到一起,变成”三次挥手”。

追问 2:为什么 TIME_WAIT 只出现在主动关闭方,被动关闭方没有? 结论先行——因为只有主动关闭方需要”为这条连接收尾兜底”。主动方发了最后一条 ACK,它要负责确认这条 ACK 真的被对方收到(否则对方会重发 FIN);被动方在 LAST_ACK 收到最后 ACK 后,tcp_done() 直接退场,不需要再为任何人兜底。所以内核源码里 TIME_WAIT 的定时器只在主动关闭方排期。

追问 3:2MSL 具体是哪两个 MSL 相加?为什么是 2 而不是 1 或 3? 结论先行——一个 MSL 是”最后一条 ACK 从主动方到被动方的最大存活时间”,另一个 MSL 是”被动方超时重发的 FIN 回到主动方的最大存活时间”,两者相加正好 2 个 MSL。等于 1 个 MSL 时,可能 ACK 还在路上、对方重发的 FIN 还没到,主动方就关了;等于 3 个 MSL 则纯属多余,因为最长的一来一回也就 2 个 MSL。

追问 4:如果服务端在 CLOSE_WAIT 一直不调 close(),会怎样? 结论先行——连接会一直僵在 CLOSE_WAIT,服务端资源(文件描述符、socket)无法释放,最终耗尽连接数。因为 CLOSE_WAIT 是”被动关闭方收到了 FIN、回了 ACK,但应用还没调 close()“的中间态,内核只能等应用,自己不会主动推进。这是典型的应用层 bug(比如代码里没在合适的时机 close,或进程异常但没关 socket),排查时看 netstat 里大量 CLOSE_WAIT 就能定位。

追问 5:TIME_WAIT 过多和服务端有什么关系?怎么优化? 结论先行——TIME_WAIT 只落在主动关闭方,服务端一旦成为主动关闭方(比如 Nginx 的 keepalive 超时、短连接),就会堆积大量 TIME_WAIT。优化思路有三:一是让服务端尽量别当主动关闭方(调大 keepalive_timeout/keepalive_requests);二是复用端口(tcp_tw_reuse);三是扩大可追踪的 TIME_WAIT 数量上限(tcp_max_tw_buckets),避免溢出后被内核强制回收。


Share this post on:

Previous Post
TCP拥塞控制——从慢启动到BBR的演进
Next Post
TCP与UDP区别:从传输层两大协议到QUIC的演进