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 #define tcp_sk(ptr) container_of_const(ptr, struct tcp_sock, inet_conn.icsk_inet.sk)
1.2 struct tcp_sock 关键字段 struct tcp_sock(include/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 struct tcp_sock { struct inet_connection_sock inet_conn ; ... u32 rcv_nxt; u32 copied_seq; u32 rcv_wup; u32 snd_nxt; u32 snd_una; u32 snd_wnd; u32 snd_ssthresh; u32 snd_cwnd; u32 snd_cwnd_cnt; u32 rcv_wnd; struct rb_root out_of_order_queue ; struct sk_buff *ooo_last_skb ; u32 srtt_us; u32 mdev_us; u32 rttvar_us; ... };
字段语义速查:
字段
含义
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 ; ... };
icsk_accept_queue 同时管理两类队列:
半连接队列(SYN queue) :已收到客户端 SYN、正在等待三次握手完成的请求,用 request_sock 表示;
全连接队列(accept queue) :三次握手已完成、等待应用层 accept() 取走的连接。
二、TCP 三次握手源码解析 2.1 数据包入口:tcp_v4_rcv 所有到达本机的 TCP 段都经过 tcp_v4_rcv(net/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 int tcp_v4_rcv (struct sk_buff *skb) { struct net *net = dev_net(skb->dev); ... if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo)) goto csum_error; 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: if (sk->sk_state == TCP_TIME_WAIT) goto do_time_wait; 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_request(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 26 27 28 29 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); 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; } req = inet_reqsk_alloc(rsk_ops, sk, !want_cookie); ... tcp_parse_options(sock_net(sk), skb, &tmp_opt, 0 , want_cookie ? NULL : &foc); ... }
SYN Cookie 机制 :当 sysctl_tcp_syncookies 开启且半连接队列满时,内核不分配 request_sock,而是将连接参数编码进 SYN-ACK 的序列号(ISN)中。客户端回应 ACK 时,内核从 ACK 号中解码出原始信息,重建连接,从而抵御 SYN Flood 攻击而不消耗内存。
2.3 完成握手:tcp_check_req 客户端发送 ACK 后,内核在 tcp_check_req(net/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 struct sock *tcp_check_req (struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen, bool *req_stolen) { ... if (TCP_SKB_CB(skb)->seq == tcp_rsk(req)->rcv_isn && flg == TCP_FLAG_SYN && !paws_reject) { ... return NULL ; } child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL , req, &own_req); ... 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_sendmsg(net/ipv4/tcp.c):
1 2 3 4 5 6 7 8 9 10 11 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 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); 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: if (!sk_stream_memory_free(sk)) goto wait_for_space; ... } ... } ... 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_frames → tcp_write_xmit(net/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 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 ) { 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 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 static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt) { ... 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_push(skb, tcp_header_size); skb_reset_transport_header(skb); 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); ... 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 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 ; 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: ... }
快速路径通过比较 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 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 ) { goto out_of_window; } queue_and_out: eaten = tcp_queue_rcv(sk, skb, &fragstolen); if (skb->len) tcp_event_data_recv(sk, skb); if (!RB_EMPTY_ROOT(&tp->out_of_order_queue)) { tcp_ofo_queue(sk); ... } ... return ; } if (!after(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) { ... } 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 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; ... if (before(ack, prior_snd_una)) { if (before(ack, prior_snd_una - tp->max_window)) tcp_send_challenge_ack(sk); goto old_ack; } 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 ; ... } tp->snd_una = ack; ... 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 int tcp_recvmsg (struct sock *sk, struct msghdr *msg, size_t len, int flags, int *addr_len) { int cmsg_flags = 0 , ret; ... 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_close(net/ipv4/tcp.c):
1 2 3 4 5 6 7 8 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 void __tcp_close(struct sock *sk, long timeout){ ... if (data_was_unread) { tcp_set_state(sk, TCP_CLOSE); tcp_send_active_reset(sk, sk->sk_allocation); } else if (tcp_close_state(sk)) { 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 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: tcp_set_state(sk, TCP_CLOSE_WAIT); inet_csk_enter_pingpong_mode(sk); break ; case TCP_FIN_WAIT1: tcp_send_ack(sk); tcp_set_state(sk, TCP_CLOSING); break ; case TCP_FIN_WAIT2: tcp_send_ack(sk); tcp_time_wait(sk, TCP_TIME_WAIT, 0 ); break ; case TCP_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 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_process(net/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 enum tcp_tw_statustcp_timewait_state_process (struct inet_timewait_sock *tw, struct sk_buff *skb, const struct tcphdr *th) { ... if (tw->tw_substate == TCP_FIN_WAIT2) { tw->tw_substate = TCP_TIME_WAIT; inet_twsk_reschedule(tw, TCP_TIMEWAIT_LEN); return TCP_TW_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_LISTEN,01 = 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 $ 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 -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 $ tcpdump -i eth0 'host 1.2.3.4 and tcp port 443' -w /tmp/cap.pcap $ 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 $ 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); }' $ 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 $ 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 $ cat /proc/sys/net/ipv4/tcp_max_syn_backlog $ cat /proc/sys/net/ipv4/tcp_syncookies $ watch -d 'nstat | grep -E "SynDrop|ListenDrop|ListenOver"'
常见 SYN 丢包原因及处理:
现象
原因
解决
ListenOverflows 持续增长
全连接队列满
增大 somaxconn 和 listen(backlog)
SYN Cookie 被频繁触发
半连接队列满
增大 tcp_max_syn_backlog,或开启 syncookies
TcpAttemptFails 增长
SYN 超时无响应
检查防火墙、路由、服务端是否监听
TcpEstabResets 增长
连接被 RST
检查 tcp_tw_reuse、负载均衡或应用层异常
七、总结 本文从 struct tcp_sock 的核心字段出发,沿着数据包在内核中的完整旅程逐层剖析:
三次握手 :tcp_v4_rcv 入口 → tcp_conn_request 分配 request_sock 并发 SYN-ACK → tcp_check_req 收到 ACK 后创建完整 socket 并入全连接队列;
发送路径 :tcp_sendmsg_locked 将用户数据写入 sk_buff → tcp_write_xmit 受拥塞窗口、接收窗口、Nagle 算法共同约束 → __tcp_transmit_skb 构建 TCP 头交给 IP 层;
接收路径 :tcp_rcv_established 快速路径优先 → tcp_data_queue 处理乱序 → tcp_ack 更新 snd_una 并驱动拥塞控制;
四次挥手 :__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