Linux 网络内核协议栈深度剖析(四):TCP 状态机与连接管理源码分析

TCP 是互联网的基石协议,其可靠性、有序性和流量控制能力建立在一套精密的状态机和连接管理机制之上。本文基于 Linux 6.4-rc1 内核源码,深入剖析 TCP 协议栈从数据结构设计、三次握手、数据收发,到四次挥手和 TIME_WAIT 的完整生命周期,并给出实用的内核级诊断工具链。

一、TCP 核心数据结构

1.1 继承体系:从 sock 到 tcp_sock

Linux 内核用一条清晰的 C 语言”继承链”表示 TCP socket:

1
2
3
4
sock
└── inet_sock
└── inet_connection_sock
└── tcp_sock

tcp_sock 的第一个成员强制为 inet_connection_sock,使得 container_of 可以在各层之间自由转型。源文件 include/linux/tcp.h 中的宏 tcp_sk() 正是利用了这一特性:

1
2
/* include/linux/tcp.h, line 475 */
#define tcp_sk(ptr) container_of_const(ptr, struct tcp_sock, inet_conn.icsk_inet.sk)

1.2 struct tcp_sock 关键字段

struct tcp_sockinclude/linux/tcp.h)是内核中最复杂的结构体之一,下面聚焦与连接管理直接相关的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* include/linux/tcp.h, line 175 */
struct tcp_sock {
/* inet_connection_sock has to be the first member of tcp_sock */
struct inet_connection_sock inet_conn;
...

/* RFC793 序列号变量 */
u32 rcv_nxt; /* What we want to receive next */
u32 copied_seq;/* Head of yet unread data */
u32 rcv_wup; /* rcv_nxt on last window update sent */
u32 snd_nxt; /* Next sequence we send */

u32 snd_una; /* First byte we want an ack for */
u32 snd_wnd; /* The window we expect to receive */

/* 拥塞控制 */
u32 snd_ssthresh; /* Slow start size threshold */
u32 snd_cwnd; /* Sending congestion window */
u32 snd_cwnd_cnt; /* Linear increase counter */

/* 接收窗口 */
u32 rcv_wnd; /* Current receiver window */

/* 乱序队列(红黑树) */
struct rb_root out_of_order_queue;
struct sk_buff *ooo_last_skb;

/* RTT 测量 */
u32 srtt_us; /* smoothed round trip time << 3 in usecs */
u32 mdev_us; /* medium deviation */
u32 rttvar_us; /* smoothed mdev_max */
...
};

字段语义速查:

字段 含义
snd_una 最早未确认字节的序列号,代表发送缓冲区左边界
snd_nxt 下一个待发送字节序列号,代表发送缓冲区右边界
snd_wnd 对端通告的接收窗口大小
snd_cwnd 本端拥塞窗口,限制在途数据量
snd_ssthresh 慢启动阈值,决定何时从慢启动切换到拥塞避免
rcv_nxt 期望收到的下一字节序列号
rcv_wnd 本端向对方通告的接收窗口
rcv_wup 最近一次窗口更新时的 rcv_nxt,用于延迟 ACK 判断
out_of_order_queue 乱序到达数据包的红黑树缓冲区
srtt_us 平滑 RTT,单位微秒(左移 3 位存储)

1.3 inet_connection_sock 与半连接队列

tcp_sock 内嵌的 inet_connection_sock 里有一个关键成员:

1
2
3
4
5
6
7
8
struct inet_connection_sock {
struct inet_sock icsk_inet;
struct request_sock_queue icsk_accept_queue; /* 半/全连接队列 */
struct inet_bind_bucket *icsk_bind_hash;
struct timer_list icsk_retransmit_timer; /* 重传定时器 */
struct timer_list icsk_delack_timer; /* 延迟 ACK 定时器 */
...
};

icsk_accept_queue 同时管理两类队列:

  • 半连接队列(SYN queue):已收到客户端 SYN、正在等待三次握手完成的请求,用 request_sock 表示;
  • 全连接队列(accept queue):三次握手已完成、等待应用层 accept() 取走的连接。

二、TCP 三次握手源码解析

2.1 数据包入口:tcp_v4_rcv

所有到达本机的 TCP 段都经过 tcp_v4_rcvnet/ipv4/tcp_ipv4.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* net/ipv4/tcp_ipv4.c, line 1975 */
int tcp_v4_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
...
/* 1. 校验和检查 */
if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))
goto csum_error;

/* 2. 通过四元组查找 socket */
sk = __inet_lookup_skb(net->ipv4.tcp_death_row.hashinfo,
skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
if (!sk)
goto no_tcp_socket;

process:
/* 3. TIME_WAIT 特殊处理 */
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;

/* 4. TCP_NEW_SYN_RECV:三次握手最后一步 */
if (sk->sk_state == TCP_NEW_SYN_RECV) {
struct request_sock *req = inet_reqsk(sk);
...
nsk = tcp_check_req(sk, skb, req, false, &req_stolen);
...
}
...
}

函数的核心逻辑:先通过 __inet_lookup_skb 哈希查找匹配的 socket,再根据状态分发处理。TCP_NEW_SYN_RECV 是 Linux 4.4 引入的虚拟状态,代表半连接队列中处于 SYN_RECV 阶段的 request_sock

2.2 LISTEN 状态处理 SYN:tcp_conn_request

当 socket 处于 TCP_LISTEN 状态并收到 SYN 时,tcp_rcv_state_process 会调用 icsk->icsk_af_ops->conn_request,最终落到 tcp_conn_requestnet/ipv4/tcp_input.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* net/ipv4/tcp_input.c, line 6924 */
int tcp_conn_request(struct request_sock_ops *rsk_ops,
const struct tcp_request_sock_ops *af_ops,
struct sock *sk, struct sk_buff *skb)
{
...
syncookies = READ_ONCE(net->ipv4.sysctl_tcp_syncookies);

/* 半连接队列满时触发 SYN Cookie */
if ((syncookies == 2 || inet_csk_reqsk_queue_is_full(sk)) && !isn) {
want_cookie = tcp_syn_flood_action(sk, rsk_ops->slab_name);
if (!want_cookie)
goto drop;
}

/* 全连接队列满时直接丢包 */
if (sk_acceptq_is_full(sk)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
goto drop;
}

/* 分配 request_sock */
req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie);
...
tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0,
want_cookie ? NULL : &foc);
...
/* 发送 SYN-ACK,将 req 放入半连接队列 */
}

SYN Cookie 机制:当 sysctl_tcp_syncookies 开启且半连接队列满时,内核不分配 request_sock,而是将连接参数编码进 SYN-ACK 的序列号(ISN)中。客户端回应 ACK 时,内核从 ACK 号中解码出原始信息,重建连接,从而抵御 SYN Flood 攻击而不消耗内存。

2.3 完成握手:tcp_check_req

客户端发送 ACK 后,内核在 tcp_check_reqnet/ipv4/tcp_minisocks.c)中完成握手:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* net/ipv4/tcp_minisocks.c, line 606 */
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
bool fastopen, bool *req_stolen)
{
...
/* 检查是否为纯重传 SYN(无效时重发 SYN-ACK) */
if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn &&
flg == TCP_FLAG_SYN && !paws_reject) {
...
return NULL; /* 重发 SYN-ACK,不创建新 socket */
}

/* ACK 合法,创建新的完整 socket */
child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb,
req, NULL, req, &own_req);
...
/* 将新 socket 移入全连接队列,唤醒 accept() */
inet_csk_reqsk_queue_add(sk, req, child);
...
}

tcp_v4_syn_recv_sock 负责分配并初始化新的 tcp_sock,复制监听 socket 的参数,建立路由缓存,完成后调用 inet_csk_reqsk_queue_add 将连接放入全连接队列,等待应用层通过 accept() 取走。

2.4 状态机视角

三次握手在状态机层面的流转如下(来自 tcp_rcv_state_process):

1
2
服务端: LISTEN → [收到 SYN] → SYN_RECV(request_sock) → [收到 ACK] → ESTABLISHED
客户端: CLOSED → [发送 SYN] → SYN_SENT → [收到 SYN-ACK] → ESTABLISHED

三、数据发送路径

3.1 tcp_sendmsg 入口

应用层调用 write() / send() 时,系统调用最终到达 tcp_sendmsgnet/ipv4/tcp.c):

1
2
3
4
5
6
7
8
9
10
11
/* net/ipv4/tcp.c, line 1480 */
int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)
{
int ret;

lock_sock(sk);
ret = tcp_sendmsg_locked(sk, msg, size);
release_sock(sk);

return ret;
}

加锁后进入核心函数 tcp_sendmsg_locked

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* net/ipv4/tcp.c, line 1215 */
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
struct tcp_sock *tp = tcp_sk(sk);
...
timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
tcp_rate_check_app_limited(sk); /* 检测是否应用层限速 */

/* 仅在 ESTABLISHED 或 CLOSE_WAIT 状态允许发送 */
if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&
!tcp_passive_fastopen(sk)) {
err = sk_stream_wait_connect(sk, &timeo);
if (err != 0)
goto do_error;
}
...
mss_now = tcp_send_mss(sk, &size_goal, flags);

while (msg_data_left(msg)) {
int copy = 0;
skb = tcp_write_queue_tail(sk);
if (skb)
copy = size_goal - skb->len;

if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {
new_segment:
/* 无法追加到现有 skb,分配新的 */
if (!sk_stream_memory_free(sk))
goto wait_for_space;
...
}
/* 将用户数据 copy 到 skb */
...
}
...
tcp_push(sk, flags, mss_now, tp->nonagle, size_goal);
}

发送路径的核心是一个大循环:尝试将用户态数据追加到发送队列尾部的 sk_buff,若当前 skb 已满则通过 sk_stream_alloc_skb 分配新的 skb,最后调用 tcp_push 触发实际发送。

3.2 tcp_write_xmit:拥塞窗口与发送调度

tcp_push__tcp_push_pending_framestcp_write_xmitnet/ipv4/tcp_output.c)是真正从发送队列取出数据并发往网络的入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* net/ipv4/tcp_output.c, line 2601 */
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
int push_one, gfp_t gfp)
{
struct tcp_sock *tp = tcp_sk(sk);
struct sk_buff *skb;
unsigned int tso_segs, sent_pkts;
int cwnd_quota;
...

while ((skb = tcp_send_head(sk))) {
tso_segs = tcp_init_tso_segs(skb, mss_now);

/* 拥塞窗口检查 */
cwnd_quota = tcp_cwnd_test(tp, skb);
if (!cwnd_quota)
break;

/* 发送窗口(对端接收窗口)检查 */
if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {
is_rwnd_limited = true;
break;
}

if (tso_segs == 1) {
/* Nagle 算法检查 */
if (unlikely(!tcp_nagle_test(tp, skb, mss_now, ...)))
break;
}
...
if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
break;
...
}
...
}

每次循环要同时满足三个条件才发包:拥塞窗口有配额(tcp_cwnd_test)、对端接收窗口有空间(tcp_snd_wnd_test)、Nagle 算法允许发送(tcp_nagle_test)。

3.3 Nagle 算法

1
2
3
4
5
6
7
8
/* net/ipv4/tcp_output.c, line 1948 */
static bool tcp_nagle_check(bool partial, const struct tcp_sock *tp,
int nonagle)
{
return partial &&
((nonagle & TCP_NAGLE_CORK) ||
(!nonagle && tp->packets_out && tcp_minshall_check(tp)));
}

Nagle 算法的核心:若当前 skb 是不足一个 MSS 的小包(partial == true),且网络中已有未确认数据(tp->packets_out > 0),则暂缓发送,等待累积更多数据或收到 ACK。TCP_NODELAY socket 选项通过设置 nonagle = TCP_NAGLE_OFF 跳过此检查。

3.4 构建 TCP 头:__tcp_transmit_skb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* net/ipv4/tcp_output.c, line 1234 */
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
...
/* 计算 TCP 选项大小并填充 */
if (unlikely(tcb->tcp_flags & TCPHDR_SYN)) {
tcp_options_size = tcp_syn_options(sk, skb, &opts, &md5);
} else {
tcp_options_size = tcp_established_options(sk, skb, &opts, &md5);
}
tcp_header_size = tcp_options_size + sizeof(struct tcphdr);

/* 在 skb 头部预留 TCP 头空间 */
skb_push(skb, tcp_header_size);
skb_reset_transport_header(skb);

/* 填充 TCP 头各字段(seq、ack_seq、window、flags 等) */
th = (struct tcphdr *)skb->data;
th->source = inet->inet_sport;
th->dest = inet->inet_dport;
th->seq = htonl(tcb->seq);
th->ack_seq= htonl(rcv_nxt);
...

/* 交给 IP 层 */
err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
...
}

四、数据接收路径

4.1 ESTABLISHED 快速路径:tcp_rcv_established

对于已建立的连接,内核设计了一条优化的头部预测(Header Prediction)快速路径(net/ipv4/tcp_input.c):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/* net/ipv4/tcp_input.c, line 5847 */
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
const struct tcphdr *th = (const struct tcphdr *)skb->data;
struct tcp_sock *tp = tcp_sk(sk);
...

tp->rx_opt.saw_tstamp = 0;

/*
* pred_flags 是 0xS?10 << 16 + snd_wnd
* 如果收到的包符合预测(纯 ACK 或纯数据,无乱序),
* 则走快速路径,避免完整的状态机检查。
*/
if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
!after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
/* 快速路径:直接入队,无需复杂检查 */
...
goto no_ack;
}
slow_path:
/* 慢速路径:调用完整的 tcp_data_queue 等函数 */
...
}

快速路径通过比较 pred_flags 和当前包的 flags + window 一致性,以及序列号是否恰好等于 rcv_nxt(即按序到达),来判断是否可以跳过复杂检查直接处理。这是 Van Jacobson 1990 年提出的”30 指令 TCP 接收”思想的内核实现。

4.2 tcp_data_queue:数据入队与乱序处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/* net/ipv4/tcp_input.c, line 5009 */
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
...

/* 按序到达:直接放入接收队列 */
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
if (tcp_receive_window(tp) == 0) {
/* 窗口为 0,丢包 */
goto out_of_window;
}

queue_and_out:
eaten = tcp_queue_rcv(sk, skb, &fragstolen);
if (skb->len)
tcp_event_data_recv(sk, skb); /* 更新 RTT、触发延迟 ACK */

/* 尝试将乱序队列中的包追加进来 */
if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
tcp_ofo_queue(sk);
...
}
...
return;
}

/* 乱序到达:放入 out_of_order_queue 红黑树 */
if (!after(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
/* 重复数据,发送 DSACK */
...
}
tcp_data_queue_ofo(sk, skb);
}

乱序数据包进入 out_of_order_queue(红黑树,按序列号排序),每当新数据包按序到达后,tcp_ofo_queue 会尝试将树中已经可以”拼接”的数据包取出并合并到接收队列,然后统一通知应用层。

4.3 tcp_ack:确认处理与窗口更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/* net/ipv4/tcp_input.c, line 3760 */
static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
struct tcp_sock *tp = tcp_sk(sk);
u32 prior_snd_una = tp->snd_una;
u32 ack = TCP_SKB_CB(skb)->ack_seq;
...

/* ACK 太旧,忽略 */
if (before(ack, prior_snd_una)) {
if (before(ack, prior_snd_una - tp->max_window))
tcp_send_challenge_ack(sk); /* RFC 5961 防注入 */
goto old_ack;
}

/* ACK 超出 snd_nxt,非法 */
if (after(ack, tp->snd_nxt))
return -SKB_DROP_REASON_TCP_ACK_UNSENT_DATA;

if (after(ack, prior_snd_una)) {
flag |= FLAG_SND_UNA_ADVANCED;
icsk->icsk_retransmits = 0; /* 重置重传计数 */
...
}

/* 更新 snd_una,释放已确认数据的 skb */
tp->snd_una = ack;
...
/* 调用拥塞控制算法(如 CUBIC)更新 cwnd */
tcp_cong_control(sk, ack, delivered, flag, &rs);
...
}

4.4 tcp_recvmsg:用户态 read() 的内核实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* net/ipv4/tcp.c, line 2667 */
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int flags,
int *addr_len)
{
int cmsg_flags = 0, ret;
...

/* Busy poll 优化:在 ESTABLISHED 状态下自旋等待数据 */
if (sk_can_busy_loop(sk) &&
skb_queue_empty_lockless(&sk->sk_receive_queue) &&
sk->sk_state == TCP_ESTABLISHED)
sk_busy_loop(sk, flags & MSG_DONTWAIT);

lock_sock(sk);
ret = tcp_recvmsg_locked(sk, msg, len, flags, &tss, &cmsg_flags);
release_sock(sk);
...
return ret;
}

tcp_recvmsg_locked 内部从 sk->sk_receive_queue 取出 skb,将数据 copy_to_iter 复制到用户空间,同时更新 copied_seq,并根据接收缓冲区变化决定是否发送 window update ACK。


五、四次挥手与 TIME_WAIT

5.1 主动关闭:tcp_close → FIN

应用层调用 close() 时触发 tcp_closenet/ipv4/tcp.c):

1
2
3
4
5
6
7
8
/* net/ipv4/tcp.c, line 3032 */
void tcp_close(struct sock *sk, long timeout)
{
lock_sock(sk);
__tcp_close(sk, timeout);
release_sock(sk);
sock_put(sk);
}

__tcp_close 核心逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* net/ipv4/tcp.c, line 2864 */
void __tcp_close(struct sock *sk, long timeout)
{
...
/* 若有未读数据,发 RST 而非 FIN */
if (data_was_unread) {
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, sk->sk_allocation);
} else if (tcp_close_state(sk)) {
/* 状态机转换:
* ESTABLISHED -> FIN_WAIT1
* CLOSE_WAIT -> LAST_ACK
*/
tcp_send_fin(sk);
}
...
}

5.2 被动关闭:tcp_fin 处理对端 FIN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/* net/ipv4/tcp_input.c, line 4359 */
void tcp_fin(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
inet_csk_schedule_ack(sk);
sk->sk_shutdown |= RCV_SHUTDOWN;
sock_set_flag(sk, SOCK_DONE);

switch (sk->sk_state) {
case TCP_ESTABLISHED:
/* 收到 FIN,进入 CLOSE_WAIT */
tcp_set_state(sk, TCP_CLOSE_WAIT);
inet_csk_enter_pingpong_mode(sk); /* 进入 pingpong 模式,延迟 ACK */
break;

case TCP_FIN_WAIT1:
/* 同时关闭(simultaneous close),进入 CLOSING */
tcp_send_ack(sk);
tcp_set_state(sk, TCP_CLOSING);
break;

case TCP_FIN_WAIT2:
/* 完整的四次挥手最后一步,进入 TIME_WAIT */
tcp_send_ack(sk);
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
break;

case TCP_LAST_ACK:
/* RFC793:保持 LAST_ACK 状态 */
break;
...
}
}

四次挥手的状态转换完整地体现在这个 switch 中:ESTABLISHED → CLOSE_WAIT(收到对端 FIN);FIN_WAIT2 → TIME_WAIT(收到对端 FIN 后双方均完成关闭)。

5.3 TIME_WAIT 状态处理

TIME_WAIT 持续 2MSL(约 60 秒),防止网络中残余的旧包影响新连接。内核用轻量级结构 tcp_timewait_sock 代替完整的 tcp_sock 来节省内存:

1
2
3
4
5
6
7
8
9
10
/* include/linux/tcp.h, line 482 */
struct tcp_timewait_sock {
struct inet_timewait_sock tw_sk;
u32 tw_rcv_wnd;
u32 tw_ts_offset;
u32 tw_ts_recent;
u32 tw_last_oow_ack_time;
int tw_ts_recent_stamp;
...
};

tcp_timewait_state_processnet/ipv4/tcp_minisocks.c)负责处理 TIME_WAIT 期间到达的报文——通常是回复一个 ACK,若收到合法的新 SYN 则可能缩短 TIME_WAIT 重用端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* net/ipv4/tcp_minisocks.c, line 84 */
enum tcp_tw_status
tcp_timewait_state_process(struct inet_timewait_sock *tw, struct sk_buff *skb,
const struct tcphdr *th)
{
...
if (tw->tw_substate == TCP_FIN_WAIT2) {
/* 若收到最后一个 FIN,转为真正的 TIME_WAIT */
tw->tw_substate = TCP_TIME_WAIT;
inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN); /* 重置 2MSL 定时器 */
return TCP_TW_ACK;
}
/* 真正的 TIME_WAIT 状态,处理重复 FIN/ACK */
...
}

5.4 tcp_tw_reuse 与 tcp_tw_recycle

net.ipv4.tcp_tw_reuse(推荐开启):允许在满足 PAWS(Protection Against Wrapped Sequences)时序条件的情况下,新连接复用 TIME_WAIT 端口,有效减少端口耗尽问题。原理是通过 TCP Timestamp 选项区分新旧连接。

net.ipv4.tcp_tw_recycle(已在 Linux 4.12 废弃):曾允许快速回收 TIME_WAIT,但在 NAT 环境下会因为不同客户端时间戳不单调而导致连接被错误拒绝,已彻底从内核中移除。


六、TCP 连接诊断方法

6.1 ss -tiepm:全面查看 TCP socket 状态

1
2
3
4
5
6
7
8
9
10
$ ss -tiepm state established '( dport = :443 )'
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp ESTAB 0 0 192.168.1.10:54321 1.2.3.4:443
cubic wscale:7,7 rto:201 rtt:1.2/0.5 ato:40 mss:1448 pmtu:1500
rcvmss:1448 advmss:1448 cwnd:10 ssthresh:2147483647
bytes_sent:12345 bytes_retrans:0 bytes_acked:12345 bytes_received:6789
segs_out:100 segs_in:80 data_segs_out:90 data_segs_in:70
send 96.5Mbps lastsnd:5 lastrcv:5 lastack:5 pacing_rate 193Mbps
retrans:0/0 rcv_rtt:1.5 rcv_space:14480 rcv_ssthresh:64088
minrtt:0.8 skmem:(r0,rb131072,t0,tb87040,f0,w0,o0,bl0,d0)

字段含义:

  • rto - 当前重传超时(毫秒)
  • rtt/rttvar - 平滑 RTT 及方差(毫秒)
  • cwnd - 拥塞窗口(段数)
  • ssthresh - 慢启动阈值
  • bytes_retrans - 累计重传字节数,非零说明有丢包
  • pacing_rate - 发送速率控制
  • rcv_space - 接收缓冲区大小(字节)
  • skmem - socket 内存使用:接收/发送缓冲、过滤器等

6.2 /proc/net/tcp 格式解析

1
2
3
4
$ cat /proc/net/tcp | head -3
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 12345 1 ...
1: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 23456 1 ...
  • local_address / rem_address:十六进制 IP:Port(小端序)
  • st:TCP 状态(0A = TCP_LISTEN01 = TCP_ESTABLISHED
  • tx_queue:rx_queue:发送/接收队列中积压的字节数,非零预示缓冲区积压

6.3 netstat -s / nstat 统计计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看 TCP 全局统计
$ netstat -s | grep -E "failed|retrans|SYN"
20 failed connection attempts
145 resets received for embryonic SYN_RECV sockets
1023 SYNs to LISTEN sockets dropped
456 TCP retransmissions

# 使用 nstat 查看增量计数器
$ nstat -z | grep -E "Tcp|Retrans"
TcpActiveOpens 1234 0.0
TcpAttemptFails 5 0.0
TcpEstabResets 2 0.0
TcpRetransSegs 78 0.0
TcpInErrs 0 0.0

关键计数器含义:

  • TcpAttemptFails:连接建立失败次数(SYN 超时或被 RST)
  • TcpRetransSegs:重传段总数,持续增长说明网络质量差
  • TcpEstabResets:ESTABLISHED 状态 RST 次数,预示连接被强制断开
  • LINUX_MIB_LISTENOVERFLOWS(对应 ListenOverflows):全连接队列溢出,说明 backlog 不足或 accept() 处理慢

6.4 tcpdump 抓包分析握手

1
2
3
4
5
# 抓取与 1.2.3.4:443 的完整握手过程
$ tcpdump -i eth0 'host 1.2.3.4 and tcp port 443' -w /tmp/cap.pcap

# 过滤 SYN/FIN 包分析握手延迟
$ tcpdump -r /tmp/cap.pcap 'tcp[tcpflags] & (tcp-syn|tcp-fin) != 0' -ttt

典型握手抓包输出:

1
2
3
00:00:00.000000 IP client.54321 > server.443: Flags [S], seq 0,  win 65535
00:00:00.001234 IP server.443 > client.54321: Flags [S.], seq 0, ack 1, win 65535
00:00:00.001456 IP client.54321 > server.443: Flags [.], ack 1, win 65535

[S] = SYN,[S.] = SYN+ACK,[.] = 纯 ACK,[F.] = FIN+ACK。

6.5 bpftrace 追踪 TCP 状态变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 追踪所有 TCP 状态转换
$ bpftrace -e '
kprobe:tcp_set_state {
$sk = (struct sock *)arg0;
$old = arg1; /* 旧状态 */
$new = arg2; /* 新状态 */
printf("pid=%d comm=%s %d -> %d\n",
pid, comm, $old, $new);
}'

# 追踪 SYN-ACK 发送(定位握手延迟)
$ bpftrace -e '
kprobe:tcp_v4_send_synack {
printf("pid=%d comm=%s SYN-ACK sent\n", pid, comm);
}'

状态值参考(include/uapi/linux/tcp.h):1=ESTABLISHED, 2=SYN_SENT, 3=SYN_RECV, 4=FIN_WAIT1, 5=FIN_WAIT2, 6=TIME_WAIT, 7=CLOSE, 8=CLOSE_WAIT, 9=LAST_ACK, 10=LISTEN, 11=CLOSING

6.6 排查 SYN 丢包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看 SYN 丢包相关统计
$ netstat -s | grep -i syn
1023 SYNs to LISTEN sockets dropped
145 resets received for embryonic SYN_RECV sockets

# 检查全连接队列溢出
$ ss -lnt | awk 'NR>1 {print $2, $3, $4}' | column -t
# Recv-Q 为全连接队列中等待 accept() 的连接数
# Send-Q 为 backlog(最大全连接队列长度)

# 检查半连接队列状态
$ cat /proc/sys/net/ipv4/tcp_max_syn_backlog # 半连接队列最大长度
$ cat /proc/sys/net/ipv4/tcp_syncookies # SYN Cookie 是否开启

# 实时观察 SYN 丢包增量
$ watch -d 'nstat | grep -E "SynDrop|ListenDrop|ListenOver"'

常见 SYN 丢包原因及处理:

现象 原因 解决
ListenOverflows 持续增长 全连接队列满 增大 somaxconnlisten(backlog)
SYN Cookie 被频繁触发 半连接队列满 增大 tcp_max_syn_backlog,或开启 syncookies
TcpAttemptFails 增长 SYN 超时无响应 检查防火墙、路由、服务端是否监听
TcpEstabResets 增长 连接被 RST 检查 tcp_tw_reuse、负载均衡或应用层异常

七、总结

本文从 struct tcp_sock 的核心字段出发,沿着数据包在内核中的完整旅程逐层剖析:

  1. 三次握手tcp_v4_rcv 入口 → tcp_conn_request 分配 request_sock 并发 SYN-ACK → tcp_check_req 收到 ACK 后创建完整 socket 并入全连接队列;
  2. 发送路径tcp_sendmsg_locked 将用户数据写入 sk_bufftcp_write_xmit 受拥塞窗口、接收窗口、Nagle 算法共同约束 → __tcp_transmit_skb 构建 TCP 头交给 IP 层;
  3. 接收路径tcp_rcv_established 快速路径优先 → tcp_data_queue 处理乱序 → tcp_ack 更新 snd_una 并驱动拥塞控制;
  4. 四次挥手__tcp_close 发送 FIN → tcp_fin 处理对端 FIN 驱动状态转换 → TIME_WAIT 用轻量结构保持 2MSL 防止旧包干扰新连接。

理解这套机制后,无论是排查 SYN 队列溢出、重传风暴,还是优化高并发场景下的 TIME_WAIT 堆积,都能直达问题根因。


参考源文件(Linux 6.4-rc1):

  • /include/linux/tcp.h — tcp_sock 结构定义
  • /net/ipv4/tcp.c — 发送、接收主入口
  • /net/ipv4/tcp_input.c — 接收处理、状态机
  • /net/ipv4/tcp_output.c — 发送处理、Nagle、拥塞控制
  • /net/ipv4/tcp_ipv4.c — IPv4 入口,socket 查找
  • /net/ipv4/tcp_minisocks.c — TIME_WAIT、tcp_check_req