基于 Linux Kernel 源码深度分析 路径:
net/ipv4/|include/linux/tcp.h|include/net/sock.h
- TCP 在内核中的位置:socket 继承链
- tcp_sock 核心数据结构
- TCP 连接建立:三次握手内核实现
- TCP 状态机:11 个状态及转换
- 数据发送路径
- 数据接收路径
- 重传机制:RTO、RACK、SACK
- 拥塞控制框架:可插拔设计
- CUBIC 算法深度
- BBR 算法深度
- TCP 高性能优化
- TCP 调优参数
- net/ipv4/ 目录主要文件一览
- TCP 连接状态机完整实现深度剖析
- 拥塞控制算法框架深度实现
- TCP Fast Open 深度实现
- 接收缓冲区自动调整算法
- GRO 和 GSO 机制深度剖析
- TCP 重传机制深度:RTO 计算、快速重传、SACK
- TCP TIME_WAIT 处理:tw_reuse 实现
- SO_REUSEPORT 完整实现
- TCP Segmentation Offload 深度
- QUIC 在内核中的实现方向与现状
Linux 网络栈的 socket 是一个层次分明的继承体系。每一层都在前一层的基础上添加协议所需的字段,通过 C 语言结构体内嵌(而非指针)实现零开销的"继承"。
用户空间
|
| sys_socket() / sys_connect() / sys_send()
|
VFS 层
|
+---v--------------+
| struct socket | <-- include/linux/net.h
| (VFS 层抽象) | socket.type, socket.state
| .sk --> sock | .ops = inet_stream_ops
+---+---------------+
|
| (sk 指针,通过 sock->sk 访问)
|
+---v--------------+ include/net/sock.h
| struct sock_common| <-- 最小公共部分(timewait sock 也共用)
| skc_daddr | 目标 IP
| skc_rcv_saddr | 本地 IP
| skc_dport | 目标端口
| skc_num | 本地端口
| skc_state | 连接状态(TCP_ESTABLISHED 等)
| skc_family | AF_INET / AF_INET6
| skc_refcnt | 引用计数
+------------------+
|
| (内嵌于 struct sock.__sk_common)
|
+---v--------------+ include/net/sock.h
| struct sock | <-- 所有协议通用 socket
| sk_receive_queue| 接收队列(sk_buff 链表)
| sk_write_queue | 发送队列
| tcp_rtx_queue | 重传队列(红黑树)
| sk_sndbuf | 发送缓冲区大小
| sk_rcvbuf | 接收缓冲区大小
| sk_pacing_rate | pacing 速率(BBR 使用)
| sk_state | = skc_state
| sk_lock | socket 锁
+------------------+
|
| (内嵌 struct inet_sock.sk)
|
+---v--------------+ include/net/inet_sock.h:218
| struct inet_sock| <-- IPv4 专用扩展
| inet_saddr | 源 IP(alias skc_rcv_saddr)
| inet_daddr | 目标 IP(alias skc_daddr)
| inet_sport | 源端口
| inet_dport | 目标端口(alias skc_dport)
| tos | Type of Service
| uc_ttl | 单播 TTL
| inet_id | IP ID 计数器
+------------------+
|
| (内嵌 struct inet_connection_sock.icsk_inet)
|
+---v--------------------+ include/net/inet_connection_sock.h:82
| struct inet_connection |
| _sock | <-- 面向连接协议通用(TCP/SCTP)
| icsk_accept_queue | accept 队列(含 SYN 队列)
| icsk_delack_timer | 延迟 ACK 定时器
| icsk_keepalive_timer | keepalive 定时器
| icsk_rto | 当前 RTO(Retransmission Timeout)
| icsk_rto_min | 最小 RTO
| icsk_ca_ops | 拥塞控制算法指针
| icsk_ca_state | CA 状态(Open/Disorder/CWR/Recovery/Loss)
| icsk_retransmits | 重传次数
| icsk_backoff | 退避指数
| icsk_ca_priv[104/8] | 拥塞控制算法私有数据(104 字节)
+------------------------+
|
| (内嵌 struct tcp_sock.inet_conn)
|
+---v--------------------+ include/linux/tcp.h:197
| struct tcp_sock | <-- TCP 专用,终极 socket
| (见第 2 节详细展开) |
+------------------------+
关键宏定义(类型安全转换):
tcp_sk(ptr):container_of(ptr, struct tcp_sock, inet_conn.icsk_inet.sk)—net/ipv4/tcp.c中频繁使用inet_csk(ptr):container_of(ptr, struct inet_connection_sock, icsk_inet.sk)—include/net/inet_connection_sock.h:152inet_sk(ptr):container_of(ptr, struct inet_sock, sk)—include/net/inet_sock.h:362
这种设计让同一个 struct sock * 指针可以被强制转型为任何一层的结构体,且开销为零(首字段内嵌,地址相同)。
struct tcp_sock 定义在 include/linux/tcp.h:197,是 Linux 内核中最复杂的结构体之一,约 200+ 字段。内核对其进行了精心的 Cache Line 分组优化。
struct tcp_sock {
struct inet_connection_sock inet_conn; /* 必须是第一个成员 */
/* ---- TX 只读热路径 Cache Line ---- */
__cacheline_group_begin(tcp_sock_read_tx);
u32 max_window; /* 对端通告过的最大窗口 */
u32 rcv_ssthresh; /* 当前接收窗口钳位值 */
u32 reordering; /* 乱序度量 */
u32 notsent_lowat; /* TCP_NOTSENT_LOWAT setsockopt */
u16 gso_segs; /* 每 GSO 包最大段数 */
struct sk_buff *retransmit_skb_hint; /* 重传队列扫描指针 */
__cacheline_group_end(tcp_sock_read_tx);
/* ---- TXRX 读写热路径 Cache Line ---- */
__cacheline_group_begin(tcp_sock_read_txrx);
u32 tsoffset; /* 时间戳偏移 */
u32 snd_wnd; /* 当前发送窗口(对端通告) */
u32 mss_cache; /* 缓存的有效 MSS */
u32 snd_cwnd; /* 发送拥塞窗口(核心!) */
u32 lost_out; /* 判定丢失的包数 */
u32 sacked_out; /* SACK 确认的包数 */
__cacheline_group_end(tcp_sock_read_txrx);
/* ---- RX 只读热路径 Cache Line ---- */
__cacheline_group_begin(tcp_sock_read_rx);
u32 copied_seq; /* 应用层已读到的序列号 */
u32 tlp_high_seq; /* TLP 发送时的 snd_nxt */
u32 rttvar_us; /* RTT 方差(平滑后) */
u32 retrans_out; /* 重传中的包数 */
u32 snd_ssthresh; /* 慢启动阈值 */
struct minmax rtt_min; /* 最小 RTT 窗口(RACK 使用) */
struct rb_root out_of_order_queue; /* 乱序队列(红黑树) */
__cacheline_group_end(tcp_sock_read_rx);
/* ---- TX 读写热路径 Cache Line(独立 Cache Line 对齐) ---- */
__cacheline_group_begin(tcp_sock_write_tx) ____cacheline_aligned;
u32 write_seq; /* 发送缓冲区尾序号 */
u32 pushed_seq; /* 已推入网络的序号 */
u64 tcp_wstamp_ns; /* 下一个包的 departure time(pacing) */
struct list_head tsorted_sent_queue; /* 按时间排序的已发未确认队列 */
__cacheline_group_end(tcp_sock_write_tx);
/* ---- TXRX 读写热路径 Cache Line ---- */
__cacheline_group_begin(tcp_sock_write_txrx);
u32 rcv_nxt; /* 期望收到的下一个序号 */
u32 snd_nxt; /* 下一个要发送的序号 */
u32 snd_una; /* 最早未被确认的序号 */
u32 srtt_us; /* 平滑 RTT(左移 3 位,单位 us) */
u32 packets_out; /* 在途(已发未确认)包数 */
u32 rcv_wnd; /* 当前接收窗口大小 */
struct tcp_options_received rx_opt; /* PAWS/SACK/TS 协商结果 */
__cacheline_group_end(tcp_sock_write_txrx);
发送方视角:
snd_una snd_nxt write_seq
| | |
...................|..............|..........|.............
(已确认) (在途/未确认) (已写入buf (未写入)
待发送)
接收方视角:
copied_seq rcv_nxt
| |
...................|..............|.............
(已复制到用户) (内核已收到 (期望下一个)
待复制)
| 字段 | 说明 |
|---|---|
snd_cwnd |
拥塞窗口(单位:MSS 数),控制在途包上限 |
snd_ssthresh |
慢启动阈值,低于此值使用慢启动,高于使用拥塞避免 |
snd_cwnd_cnt |
拥塞避免中的 ACK 计数(Reno AIMD 使用) |
prior_cwnd |
进入丢失恢复前的 cwnd(PRR 算法使用) |
prr_delivered |
Recovery 中新传包数(PRR) |
prr_out |
Recovery 中已发总包数(PRR) |
/* include/linux/tcp.h:379 */
struct tcp_rack {
u64 mstamp; /* 最近一次(重)传的时间戳 */
u32 rtt_us; /* 对应 RTT */
u32 end_seq; /* 发送包的结束序号 */
u32 last_delivered;
u8 reo_wnd_steps; /* 允许的乱序窗口大小 */
u8 dsack_seen:1;
u8 advanced:1;
} rack;TCP 使用的定时器分布在两个结构中:
inet_connection_sock:
icsk_delack_timer --> 延迟 ACK(200ms 内合并 ACK)
icsk_keepalive_timer --> Keepalive 探测
sock(通过 sock.tcp_retransmit_timer):
tcp_retransmit_timer --> RTO 超时重传(由 icsk_pending 标志区分用途)
ICSK_TIME_RETRANS = 1 重传定时器
ICSK_TIME_PROBE0 = 3 零窗口探测
ICSK_TIME_LOSS_PROBE=5 TLP(Tail Loss Probe)
ICSK_TIME_REO_TIMEOUT=6 RACK 乱序超时
Linux 使用两段式队列设计,防止 SYN Flood 攻击:
客户端 服务端内核
| |
|------- SYN ------------>|
| +-----+------+
| | SYN Queue | icsk_accept_queue.syn_table[]
| | (半连接队列)| 大小 = /proc/sys/net/ipv4/tcp_max_syn_backlog
| | req_sock | 状态: TCP_NEW_SYN_RECV
|<------ SYN+ACK ----| |
| +------------+
|------- ACK ------------>|
| +-----+------+
| |Accept Queue| icsk_accept_queue.rskq_accept_head
| | (全连接队列)| 大小 = listen() 的 backlog 参数
| | sk (ESTAB)| 状态: TCP_ESTABLISHED
| +------------+
| |
| app: accept()
入口函数:net/ipv4/tcp_input.c:7640 tcp_conn_request()
tcp_v4_rcv() net/ipv4/tcp_ipv4.c
--> tcp_rcv_state_process() net/ipv4/tcp_input.c
--> tcp_conn_request() net/ipv4/tcp_input.c:7640
|
|--> inet_reqsk_alloc() 分配 tcp_request_sock
| 记录 rcv_isn(客户端 ISN)
| 记录 snt_isn(服务端 ISN,随机生成)
|
|--> SYN Cookie 检查
| 若 SYN Queue 满,启用 Cookie(无状态)
|
|--> tcp_v4_send_synack() 发送 SYN+ACK
| tcp_make_synack() 构造 SYN+ACK skb
|
+--> inet_csk_reqsk_queue_hash_add() 加入 SYN Queue
tcp_v4_rcv()
--> tcp_check_req() net/ipv4/tcp_minisocks.c
验证 ACK 序号和确认号
|
--> tcp_v4_syn_recv_sock() 创建子 socket(full socket)
inet_csk_clone_lock() 克隆 listener socket
tcp_create_openreq_child()
|
--> inet_csk_complete_hashdance() 从 SYN Queue 移除
--> inet_csk_reqsk_queue_add() 加入 Accept Queue
子 socket 状态: TCP_ESTABLISHED
connect() syscall
--> tcp_v4_connect() net/ipv4/tcp_ipv4.c
tcp_set_state(sk, TCP_SYN_SENT)
tcp_connect(sk)
--> tcp_send_syn_data() 或 tcp_send_syn()
构造 SYN skb,设置 ISN(write_seq)
tcp_transmit_skb()
开启 RTO 定时器(等待 SYN+ACK)
TFO 允许在 SYN 包中携带数据,减少一个 RTT 的连接建立开销。
普通连接: SYN --> SYN+ACK --> ACK --> 数据 (1.5 RTT 才能传数据)
TFO 连接: SYN+数据 --> SYN+ACK+数据 (0.5 RTT 即可收到响应数据)
实现: net/ipv4/tcp_fastopen.c
服务端: tcp_try_fastopen() 验证 TFO Cookie
客户端: tcp_sendmsg_fastopen() 在 connect 时携带数据
状态枚举定义在 include/net/tcp_states.h:12:
enum {
TCP_ESTABLISHED = 1, /* 连接已建立 */
TCP_SYN_SENT, /* 已发送 SYN,等待 SYN+ACK */
TCP_SYN_RECV, /* 收到 SYN,已发 SYN+ACK(半连接) */
TCP_FIN_WAIT1, /* 发送了 FIN,等待对端 ACK */
TCP_FIN_WAIT2, /* 收到 ACK of FIN,等待对端 FIN */
TCP_TIME_WAIT, /* 等待确认对端已收到 ACK(2MSL) */
TCP_CLOSE, /* 连接已关闭 */
TCP_CLOSE_WAIT, /* 收到对端 FIN,等待本端关闭 */
TCP_LAST_ACK, /* 已发 FIN,等待对端最终 ACK */
TCP_LISTEN, /* 监听状态 */
TCP_CLOSING, /* 双方同时发 FIN(罕见) */
TCP_NEW_SYN_RECV, /* 内核内部:SYN Queue 中的请求 */
}; CLOSED
|
listen() | connect()
|
+--------------+------------------+
| |
v v
LISTEN SYN_SENT
| |
recv SYN | recv SYN+ACK|
send SYN+ACK send ACK |
| |
v v
SYN_RECV <--- (simultaneous ESTABLISHED <----------+
| open: both | |
recv ACK | send SYN) close()/shutdown() |
| send FIN |
v | |
ESTABLISHED v recv FIN
| FIN_WAIT1 send ACK
close() | | |
send FIN | recv FIN | recv ACK |
| send ACK +----------------> FIN_WAIT2
v | |
CLOSE_WAIT v recv FIN |
| CLOSING send ACK |
close() | | v
send FIN | recv ACK| TIME_WAIT
| | 2*MSL timeout|
v v |
LAST_ACK CLOSED <---------------------+------+
|
recv ACK |
v
CLOSED
| 转换 | 函数 | 文件 |
|---|---|---|
| CLOSED -> LISTEN | inet_listen() |
net/ipv4/af_inet.c |
| LISTEN -> SYN_RECV | tcp_conn_request() |
net/ipv4/tcp_input.c:7640 |
| SYN_RECV -> ESTABLISHED | tcp_check_req() |
net/ipv4/tcp_minisocks.c |
| CLOSED -> SYN_SENT | tcp_v4_connect() |
net/ipv4/tcp_ipv4.c |
| SYN_SENT -> ESTABLISHED | tcp_rcv_synsent_state_process() |
net/ipv4/tcp_input.c |
| ESTABLISHED -> FIN_WAIT1 | tcp_close() |
net/ipv4/tcp.c |
| FIN_WAIT1 -> TIME_WAIT | tcp_time_wait() |
net/ipv4/tcp_minisocks.c |
tcp_set_state() 是统一的状态切换函数,位于 include/net/tcp.h,内部会更新 sk->sk_state 并触发统计计数器。
应用层 write()/send()
|
v
tcp_sendmsg() net/ipv4/tcp.c:1460
|
v
tcp_sendmsg_locked() net/ipv4/tcp.c:1130
|
|--> 等待连接完成(sk_stream_wait_connect)
|
|--> 主循环:将用户数据拷贝到 sk_buff
| sk_stream_alloc_skb() 分配 skb
| skb_add_data_nocache() 零拷贝或 memcpy
| tcp_push() 触发发送
|
v
tcp_push()
|
v
tcp_write_xmit() net/ipv4/tcp_output.c:2966
|
|--> tcp_cwnd_test() 检查拥塞窗口
|--> tcp_snd_wnd_test() 检查接收窗口
|--> tcp_nagle_test() Nagle 算法检查
|--> tcp_tso_segs() 计算 TSO 分段数
|--> tso_fragment() 如有必要分片
|
v
tcp_transmit_skb() net/ipv4/tcp_output.c
|
|--> 构造 TCP 头(序号、ACK、窗口、选项)
|--> icsk_af_ops->queue_xmit() --> ip_queue_xmit()
|
v
IP 层 --> 网络设备驱动
TSO(TCP Segmentation Offload):将大 skb 的分段工作卸载到网卡。
内核构造大 skb(最大 64KB)
|
v
tcp_write_xmit()
设置 skb->gso_size = mss,gso_segs = N
|
v
网卡驱动
|
+--> 若网卡支持 TSO:硬件自动切分 --> 发送 N 个 MSS 大小的包
|
+--> 若不支持:GSO 在软件层(net/core/gso.c)切分后发送
tcp_tso_autosize() 函数(net/ipv4/tcp_output.c:2235)根据 RTT 和 pacing rate 动态计算每次发送的 TSO 段数:
/* net/ipv4/tcp_output.c:2243 */
r = tcp_min_rtt(tcp_sk(sk)) >> sysctl_tcp_tso_rtt_log;
/* 用 RTT 来决定 TSO 聚合窗口大小,RTT 越小,TSO 段数越小 */tcp_nagle_test() 实现 Nagle 算法(net/ipv4/tcp_output.c):
规则:若满足以下任一条件,允许发送小包:
1. 发送队列为空(no packets in flight)
2. 数据量 >= MSS(full-size segment)
3. 设置了 TCP_NODELAY(禁用 Nagle)
4. 设置了 TCP_CORK 但超时
nonagle 字段控制:
tp->nonagle & TCP_NAGLE_OFF -- TCP_NODELAY 设置时
tp->nonagle & TCP_NAGLE_CORK -- TCP_CORK 设置时
tp->nonagle & TCP_NAGLE_PUSH -- 强制推送
网卡中断
|
v
驱动 NAPI poll
|
v
netif_receive_skb() 网络核心层
|
GRO(Generic Receive Offload)合并小包
|
v
ip_rcv() --> ip_local_deliver()
|
v
tcp_v4_rcv() net/ipv4/tcp_ipv4.c
|
|--> 查找 socket(ehash 或 bhash 哈希表)
|--> tcp_filter() BPF/socket filter
|
v
tcp_rcv_established() net/ipv4/tcp_input.c:6519
|
|--> 头部预测(Fast Path)
| pred_flags 匹配 --> 最快路径
|
|--> Slow Path:
| tcp_data_queue() 数据入接收队列
| tcp_ack() 处理 ACK,更新 snd_una
| tcp_check_ofo_queue() 乱序重组
|
v
tcp_rcv_space_adjust() 动态调整接收缓冲区
|
recvmsg() / read()
|
tcp_recvmsg() net/ipv4/tcp.c
|
+--> skb_copy_datagram_msg() 数据从内核拷贝到用户空间
tcp_rcv_established() 的核心优化,源自 Van Jacobson 1988 年的论文:
/* net/ipv4/tcp_input.c:6559 */
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)) {
/* Fast Path: 包按序到达,无特殊标志 */
...
}pred_flags = 0x5?10 << 16 + snd_wnd,编码了期望的 TCP 头部特征。命中率高时无需完整解析 TCP 选项。
乱序包通过红黑树 out_of_order_queue 管理,按 seq 排序:
out_of_order_queue(红黑树):
[seq=500-599]
/ \
[seq=300-399] [seq=700-799]
当 rcv_nxt 到达缺口时,tcp_ofo_queue() 将有序段移入接收队列
tcp_rbtree_insert() 在 net/ipv4/tcp_output.c:70 同样用于管理重传队列 tcp_rtx_queue。
GRO 是软件层的 LRO(Large Receive Offload),在 softirq 中将多个小 TCP 包合并为一个大包,减少协议栈处理次数:
多个小包(MSS 大小): [pkt1][pkt2][pkt3][pkt4]
|
GRO 合并(按连接聚合)
|
一个大 skb(4*MSS)
|
tcp_rcv_established() 一次调用处理 4 个包的数据
实现在 net/ipv4/tcp_input.c:1071,tcp_rtt_estimator() 函数:
SRTT(平滑 RTT)更新:
err = mrtt - (srtt >> 3) /* 误差(新测量值 - 当前估计/8) */
srtt = srtt + err /* 等价于 srtt = 7/8*srtt + 1/8*mrtt */
tp->srtt_us = max(1U, srtt) /* srtt_us 是左移 3 位的,单位 us */
RTTVAR(RTT 方差)更新:
mdev = 3/4 * mdev + 1/4 * |err|
tp->rttvar_us = max(mdev_max, tcp_rto_min_us)
RTO 计算(RFC 6298):
RTO = srtt/8 + 4 * rttvar
= (tp->srtt_us >> 3) + tp->rttvar_us
[TCP_RTO_MIN = 200ms, TCP_RTO_MAX = 120s]
icsk->icsk_rto = 指数退避(每次翻倍,直到 MAX)
RACK 是 Linux 4.5+ 引入的丢包检测算法,比传统 dupACK 计数更准确:
核心思想:
若一个包的发送时间比某已确认包早 RTT+reorder_window,
且该包仍未被确认,则判定为丢失。
tcp_rack.mstamp: 最近一次成功送达包的发送时间戳
tcp_rack.rtt_us: 对应 RTT
检测流程(net/ipv4/tcp_recovery.c):
tcp_rack_mark_lost() 主入口
--> tcp_rack_detect_loss() 遍历未确认包
tcp_rack_skb_timeout() 计算每个包的超时时间
timeout = rack.mstamp + rack.rtt_us + reo_wnd
若 skb.mstamp + timeout < now --> 标记丢失
优势:
- 对乱序友好(可配置 reo_wnd)
- 不依赖 dupACK 计数(对无序 ACK 鲁棒)
- 结合 DSACK 动态调整乱序容忍度
SACK(Selective ACK)允许接收方通知发送方哪些段已收到,避免重传已收到的数据:
/* include/linux/tcp.h:102 */
struct tcp_sack_block {
u32 start_seq;
u32 end_seq;
};
/* tcp_sock 中 */
struct tcp_sack_block selective_acks[4]; /* 发送给对端的 SACK 块 */
struct tcp_sack_block recv_sack_cache[4]; /* 收到的 SACK 块缓存 */
struct tcp_sack_block duplicate_sack[1]; /* D-SACK 块 */SACK 处理流程:
收到 ACK with SACK options
|
v
tcp_sacktag_write_queue() net/ipv4/tcp_input.c
|
|--> 遍历重传队列(tcp_rtx_queue,红黑树)
|--> 标记 SACK'd 包:TCP_SKB_CB(skb)->sacked |= TCPCB_SACKED_ACKED
|--> 更新 sacked_out 计数
|
v
tcp_fastretrans_alert()
|
|--> 若 lost_out > 0:进入快速重传
|--> tcp_enter_recovery():切换 CA 状态到 TCP_CA_Recovery
|--> tcp_retransmit_skb():重传标记丢失的包
RTO 超时触发:
tcp_retransmit_timer() net/ipv4/tcp_timer.c
|
|--> tcp_write_timeout() 检查是否超过最大重试次数
| sysctl_tcp_retries1 = 3 (触发路由更新)
| sysctl_tcp_retries2 = 15 (放弃连接,约 15 分钟)
|
|--> tcp_retransmit_skb() 重传队首包
|--> icsk->icsk_backoff++ 退避指数++
|--> icsk_rto <<= 1 RTO 翻倍(指数退避)
|--> 重新启动定时器
TLP(Tail Loss Probe,尾部丢失探测):
在快速重传之前,通过发送 probe 包提前发现尾部丢失
避免 RTO 超时的长延迟
struct tcp_congestion_ops 定义在 include/net/tcp.h:1275:
struct tcp_congestion_ops {
/* 快路径字段(放在第一个 cache line) */
/* (a) 经典模式:只需实现 cong_avoid */
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked);
/* (b) 自定义模式:完全控制(BBR 使用此接口) */
void (*cong_control)(struct sock *sk, u32 ack, int flag,
const struct rate_sample *rs);
/* 必须实现:计算新的慢启动阈值 */
u32 (*ssthresh)(struct sock *sk);
/* 可选:撤销减窗(网络条件好转时恢复 cwnd) */
u32 (*undo_cwnd)(struct sock *sk);
/* 可选:状态变更通知 */
void (*set_state)(struct sock *sk, u8 new_state);
/* 可选:cwnd 事件(TX_START、CWND_RESTART 等) */
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
/* 可选:每次 ACK 到达回调 */
void (*in_ack_event)(struct sock *sk, u32 flags);
/* 可选:包确认统计(BBR 使用) */
void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
/* 慢路径字段 */
void (*init)(struct sock *sk); /* 初始化私有数据 */
void (*release)(struct sock *sk); /* 释放私有数据 */
char name[TCP_CA_NAME_MAX];
u32 flags; /* TCP_CONG_NON_RESTRICTED / TCP_CONG_NEEDS_ECN */
} ____cacheline_aligned_in_smp;/* net/ipv4/tcp_cong.c:22 */
static LIST_HEAD(tcp_cong_list); /* 全局算法链表 */
/* 注册 */
tcp_register_congestion_control(ops); /* 加入链表,用 jhash 生成 key */
/* 查找 */
tcp_ca_find("cubic"); /* 按名查找 */
tcp_ca_find_key(key); /* 按 hash key 查找 */
/* 分配给连接 */
tcp_assign_congestion_control(sk); /* net/ipv4/tcp_cong.c:216 */
--> ca = rcu_dereference(net->ipv4.tcp_congestion_control);
--> icsk->icsk_ca_ops = ca;
--> memset(icsk->icsk_ca_priv, 0, 104); /* 清零私有数据区 */TCP_CA_Open -- 正常,无丢包
TCP_CA_Disorder -- 检测到 SACK 或 dupACK,但还未确认丢包
TCP_CA_CWR -- ECN 拥塞通知,正在减速
TCP_CA_Recovery -- 快速重传/恢复中
TCP_CA_Loss -- RTO 超时,慢启动恢复
转换触发:
tcp_fastretrans_alert() --> 进入 Recovery
tcp_enter_loss() --> 进入 Loss
tcp_try_undo_recovery() --> 退出 Recovery -> Open
# 查看可用算法
cat /proc/sys/net/ipv4/tcp_available_congestion_control
# cubic reno bbr
# 修改默认算法
echo "bbr" > /proc/sys/net/ipv4/tcp_congestion_control
# 单连接指定算法(setsockopt)
setsockopt(fd, IPPROTO_TCP, TCP_CONGESTION, "bbr", strlen("bbr"));CUBIC 是 Linux 默认拥塞控制算法(自 Linux 2.6.19),实现在 net/ipv4/tcp_cubic.c。
/* net/ipv4/tcp_cubic.c:86 */
struct bictcp {
u32 cnt; /* cwnd 增长计数器:每 cnt 个 ACK 增加 1 */
u32 last_max_cwnd; /* 最近一次丢包前的最大 cwnd(W_max) */
u32 bic_origin_point; /* 当前 epoch 的原点 cwnd */
u32 bic_K; /* 到达 W_max 需要的时间(单位 BICTCP_HZ) */
u32 delay_min; /* 测量到的最小 RTT(usec) */
u32 epoch_start; /* 当前 epoch 开始时间 */
u32 ack_cnt; /* epoch 内 ACK 计数 */
u32 tcp_cwnd; /* TCP-friendly 估算的 cwnd */
/* HyStart 字段 */
u32 round_start; /* 当前轮次开始时间 */
u32 end_seq; /* 轮次结束序号 */
u32 curr_rtt; /* 当前轮次最小 RTT */
};CUBIC 的核心是一个三次函数,在时间维度(而非 ACK 数维度)上增长:
W(t) = C * (t - K)^3 + W_max
其中:
W_max = 丢包前的 cwnd(last_max_cwnd)
C = bic_scale / 1024 = 41/1024 ≈ 0.04(默认值)
K = 3_sqrt( W_max * (1-beta) / C )
= cubic_root(cube_factor * (W_max - cwnd_now))
t = 从本 epoch 开始经过的时间(秒)
beta = 717/1024 ≈ 0.7(乘性减小因子)
cwnd
| /
| /
W_max|. . . . . . . . . . . . . . .* . ./. . <-- 达到 W_max
| * /
| 丢包 * /
| \ K^3 * / <-- CUBIC 函数加速区
| \ * /
beta*W_max|. . . .*/ . . . . . . . . . . . . <-- 丢包后减到 beta*W_max
| /|
| / |
| / |
+--K---+-----------------------------------------> t(时间)
epoch start
/* net/ipv4/tcp_cubic.c:214 */
static void bictcp_update(struct bictcp *ca, u32 cwnd, u32 acked)
{
/* 计算时间 t = jiffies - epoch_start(转换单位为 BICTCP_HZ) */
t = (s32)(tcp_jiffies32 - ca->epoch_start) + ...;
/* 计算 |t - K| */
offs = (t < ca->bic_K) ? (ca->bic_K - t) : (t - ca->bic_K);
/* cubic 目标: C/rtt * (t-K)^3 */
delta = (cube_rtt_scale * offs * offs * offs) >> (10 + 3*BICTCP_HZ);
if (t < ca->bic_K) /* t < K:仍在 W_max 以下,concave 阶段 */
bic_target = ca->bic_origin_point - delta;
else /* t > K:超过 W_max,convex 阶段 */
bic_target = ca->bic_origin_point + delta;
/* 转换为每 N 个 ACK 增加 1 MSS 的速率 */
ca->cnt = cwnd / (bic_target - cwnd);
}当检测到网络中有新流时,主动降低 W_max,加快向公平点收敛:
/* net/ipv4/tcp_cubic.c:349 */
if (tcp_snd_cwnd(tp) < ca->last_max_cwnd && fast_convergence)
/* 新流竞争:W_max 不是真实瓶颈,提前降低 */
ca->last_max_cwnd = (tcp_snd_cwnd(tp) * (BICTCP_BETA_SCALE + beta))
/ (2 * BICTCP_BETA_SCALE);
else
ca->last_max_cwnd = tcp_snd_cwnd(tp); /* 记录当前 cwnd 为 W_max */HyStart 用于在慢启动阶段更早退出,避免过冲(overshooting):
触发条件(任一):
1. ACK Train(包列探测):相邻 ACK 间隔 < hystart_ack_delta_us(2ms)
表示链路无排队,可能快到瓶颈了
2. Delay Increase(时延增加探测):当前轮 RTT 比 delay_min 增加超过
HYSTART_DELAY_THRESH(4ms ~ 16ms)
触发后:退出慢启动,设置 ssthresh = cwnd(进入拥塞避免)
BBR(Bottleneck Bandwidth and RTprop)由 Google 于 2016 年提出,实现在 net/ipv4/tcp_bbr.c,是一种基于带宽-延迟模型的拥塞控制,而非基于丢包。
两个关键变量:
BtlBw(Bottleneck Bandwidth):瓶颈带宽
= windowed_max(delivered / elapsed, 10 RTTs)
用 minmax 滑动窗口维护
RTprop(Round-trip propagation delay):传播延迟
= windowed_min(rtt, 10 seconds)
用 min_rtt_us 字段维护
发送策略:
pacing_rate = pacing_gain * BtlBw
cwnd = cwnd_gain * BtlBw * RTprop
/* net/ipv4/tcp_bbr.c:81 */
enum bbr_mode {
BBR_STARTUP, /* 指数增长,探测带宽上限 */
BBR_DRAIN, /* 排空启动阶段产生的队列 */
BBR_PROBE_BW, /* 稳态:周期性探测带宽,控制排队 */
BBR_PROBE_RTT, /* 降低 inflight 至最小,重新测量 RTprop */
};状态转换:
STARTUP
| 3 轮带宽不再增长 25%(full_bw_reached)
v
DRAIN
| inflight <= BDP(Bandwidth-Delay Product)
v
PROBE_BW <-+
| 10 秒未见更小 RTT |(probe 完成)
v |
PROBE_RTT --------------+
PROBE_BW 增益周期(bbr_pacing_gain[],net/ipv4/tcp_bbr.c:162):
8 个阶段的 pacing_gain 值:
[5/4, 3/4, 1, 1, 1, 1, 1, 1]
周期1:pacing_gain=1.25 超速发送,探测是否有更多带宽
周期2:pacing_gain=0.75 降速,排空前一周期可能产生的队列
周期3-8:pacing_gain=1.0 巡航,以估算带宽发送
/* net/ipv4/tcp_bbr.c:214 */
static u32 bbr_max_bw(const struct sock *sk)
{
struct bbr *bbr = inet_csk_ca(sk);
return minmax_get(&bbr->bw); /* 10 RTT 滑动最大值 */
}带宽样本来自 rate_sample 结构(由 tcp_rate.c 维护):
每次 ACK 时:
delivered_rate = (delivered - prev_delivered) / elapsed_time
minmax_running_max(&bbr->bw, bbr_bw_rtts, ...) /* 10轮次最大值 */
/* net/ipv4/tcp_bbr.c:256 */
static void bbr_set_pacing_rate(struct sock *sk, u32 bw, int gain)
{
unsigned long rate = bbr_bw_to_pacing_rate(sk, bw, gain);
/* 写入 sk->sk_pacing_rate,由 fq qdisc 或内部 hrtimer 控制发送速率 */
WRITE_ONCE(sk->sk_pacing_rate, rate);
}维度 CUBIC BBR
----------- ----------------------- -----------------------
丢包响应 丢包立即减半 cwnd 不直接响应丢包
带宽利用 可能高(但会造成排队) 接近理论最优
队列延迟 高(buffer bloat) 低(主动控制)
公平性 与 CUBIC 流公平 与 CUBIC 流共存时可能占优
适用场景 高 BDP、低误码率链路 卫星链路、无线(有随机丢包)
内核支持 默认算法 需编译或 sysctl 开启
/* 使用场景:延迟敏感的交互式应用(SSH、游戏、RPC) */
int flag = 1;
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
/* 内核实现:net/ipv4/tcp.c */
tp->nonagle |= TCP_NAGLE_OFF;
/* tcp_nagle_test() 直接放行小包 */sendfile() 系统调用完全在内核中完成文件数据到 TCP 的传输,用户空间无数据拷贝:
传统 send(): sendfile()(零拷贝):
磁盘 --> 内核页缓存 磁盘 --> 内核页缓存
--> 用户空间缓冲区 |
--> 内核 socket 缓冲区 |--> DMA 直接到网卡
--> DMA 到网卡 (两次 DMA + 零 CPU 拷贝)
(四次拷贝,两次 CPU 拷贝)
内核实现:net/ipv4/tcp.c 中 tcp_sendpage() → do_tcp_sendpages() → 使用 skb_add_data() 添加页面引用(不拷贝)。
MSG_ZEROCOPY(Linux 4.14+)进一步支持用户态缓冲区的零拷贝:
/* 需先设置 SO_ZEROCOPY */
setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one));
/* 发送时指定 MSG_ZEROCOPY */
send(fd, buf, len, MSG_ZEROCOPY);
/* 通过错误队列得到完成通知 */
recvmsg(fd, &msg, MSG_ERRQUEUE);减少连接建立的 RTT 开销:
/* net/ipv4/tcp_fastopen.c */
/* 服务端启用 TFO */
int qlen = 1024;
setsockopt(listenfd, IPPROTO_TCP, TCP_FASTOPEN, &qlen, sizeof(qlen));
/* 或全局开启 */
/* echo 3 > /proc/sys/net/ipv4/tcp_fastopen */
/* 客户端发送 TFO 请求 */
sendto(fd, data, len, MSG_FASTOPEN, ...);
/* 等价于 connect() + send() 合并为一次 SYN+DATA */TFO Cookie 机制防止 SYN 放大攻击(tcp_fastopen_init_key_once() 在 net/ipv4/tcp_fastopen.c:94)。
允许多个 socket 绑定同一端口,内核对入连接进行负载均衡:
新连接
|
内核哈希分发(4-tuple hash)
/ | \
worker0 worker1 worker2 ...
fd0 fd1 fd2
(同一端口的不同 socket)
优点:
- 消除 accept() 锁竞争
- 每个 worker 有独立 accept queue
- CPU 亲和性更好
int on = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on));
/* 配合 BPF 可实现自定义分发策略 */内核动态调整 TCP 缓冲区大小(tcp_rcv_space_adjust()):
接收侧自动调整:
rcvq_space.space 初始 = 87380 字节
每次 RTT 测量 throughput = bytes_received / rtt
若 throughput > space/2,翻倍 space(上限 tcp_rmem[2])
调用 tcp_set_rcvlowat()
发送侧自动调整:
sk_sndbuf 在 tcp_sndbuf_expand() 中动态扩展
上限 = tcp_wmem[2]
Linux 5.6+ 原生支持 MPTCP,允许连接使用多个网络路径:
应用层看到一个普通 TCP socket
--> MPTCP 层(net/mptcp/)透明地管理多个子流
--> 每个子流是一个标准 TCP 连接
| 参数 | 默认值 | 说明 |
|---|---|---|
tcp_rmem |
4096 131072 6291456 |
接收缓冲区 [最小, 默认, 最大](字节) |
tcp_wmem |
4096 16384 4194304 |
发送缓冲区 [最小, 默认, 最大](字节) |
tcp_mem |
内核计算 | TCP 全局内存限制 [最小, 压力, 最大](页) |
# 为 10GbE 高吞吐优化(BDP = 10Gbps * 50ms = 62.5MB)
echo "4096 87380 67108864" > /proc/sys/net/ipv4/tcp_rmem
echo "4096 65536 67108864" > /proc/sys/net/ipv4/tcp_wmem| 参数 | 默认值 | 说明 |
|---|---|---|
tcp_max_syn_backlog |
128~1024 | SYN Queue(半连接队列)最大长度 |
somaxconn |
128 | Accept Queue 最大长度(全局上限) |
tcp_syn_retries |
6 | SYN 重试次数(约 127 秒超时) |
tcp_synack_retries |
5 | SYN+ACK 重试次数(服务端) |
| 参数 | 默认值 | 说明 |
|---|---|---|
tcp_retries1 |
3 | 触发路由更新的重传次数 |
tcp_retries2 |
15 | 放弃连接的最大重传次数(约 15 分钟) |
tcp_orphan_retries |
0 | 孤儿 socket 重传次数(0=8) |
tcp_fin_timeout |
60 | FIN_WAIT2 超时时间(秒) |
tcp_tw_reuse |
2 | TIME_WAIT socket 复用(1=开启,需 timestamps) |
| 参数 | 默认值 | 说明 |
|---|---|---|
tcp_congestion_control |
cubic | 默认拥塞控制算法 |
tcp_slow_start_after_idle |
1 | 空闲后重新慢启动 |
tcp_early_retrans |
3 | 早期重传(减少 dupACK 等待) |
tcp_recovery |
1 | RACK/TLP 开关 |
tcp_sack |
1 | SACK 开关 |
tcp_timestamps |
1 | TCP 时间戳开关(PAWS 和 RTT 测量依赖) |
| 参数 | 值 | 说明 |
|---|---|---|
tcp_fastopen |
0 | 禁用 |
| 1 | 客户端启用 | |
| 2 | 服务端启用 | |
| 3 | 双端启用 | |
tcp_fastopen_blackhole_timeout_sec |
3600 | TFO 黑洞检测超时 |
| 参数 | 默认值 | 说明 |
|---|---|---|
tcp_no_metrics_save |
0 | 不保存路由缓存的 TCP 指标 |
tcp_moderate_rcvbuf |
1 | 自动调整接收缓冲区 |
tcp_tso_win_divisor |
3 | TSO 发送窗口除数(影响 TSO 聚合) |
tcp_min_tso_segs |
2 | TSO 最小段数 |
# 查看当前所有 TCP 参数
sysctl -a | grep net.ipv4.tcp_
# 高性能服务器推荐配置
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_max_syn_backlog=65535
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fastopen=3
sysctl -w net.ipv4.tcp_congestion_control=bbrnet/ipv4/
|
|-- tcp.c # TCP 主体:sendmsg/recvmsg/setsockopt/getsockopt
| # tcp_sendmsg_locked() 发送入口
| # tcp_recvmsg() 接收入口
| # tcp_close() 关闭连接
|
|-- tcp_input.c # TCP 接收路径(最大单文件之一,~7700 行)
| # tcp_rcv_established() 已建立连接的接收处理
| # tcp_rcv_state_process() 状态机驱动
| # tcp_ack() ACK 处理
| # tcp_data_queue() 数据入队
| # tcp_conn_request() 服务端 SYN 处理
| # tcp_rtt_estimator() RTT 估算
|
|-- tcp_output.c # TCP 发送路径(~4200 行)
| # tcp_write_xmit() 核心发送循环
| # tcp_transmit_skb() 底层发送(构造 TCP 头)
| # tso_fragment() TSO 分片
| # tcp_make_synack() SYN+ACK 构造
|
|-- tcp_timer.c # 定时器处理
| # tcp_retransmit_timer() RTO 超时
| # tcp_delack_timer() 延迟 ACK
| # tcp_keepalive_timer() Keepalive
|
|-- tcp_cong.c # 拥塞控制框架
| # tcp_register/unregister_congestion_control()
| # tcp_assign_congestion_control()
|
|-- tcp_cubic.c # CUBIC 算法实现(默认算法)
|-- tcp_bbr.c # BBR 算法实现(Google,v1)
|-- tcp_reno.c # Reno(基础算法,新 CA 的参考实现)
|-- tcp_bic.c # BIC(CUBIC 前身)
|-- tcp_dctcp.c # DCTCP(数据中心 TCP,基于 ECN)
|-- tcp_vegas.c # Vegas(基于延迟)
|-- tcp_htcp.c # H-TCP
|-- tcp_hybla.c # Hybla(卫星链路优化)
|
|-- tcp_recovery.c # RACK 丢包检测
| # tcp_rack_mark_lost()
| # tcp_rack_reo_timeout()
|
|-- tcp_minisocks.c # TIME_WAIT/SYN_RECV 轻量 socket
| # tcp_time_wait()
| # tcp_check_req() 三次握手第三步
|
|-- tcp_fastopen.c # TCP Fast Open 实现
| # tcp_try_fastopen()
| # reqsk_fastopen_remove()
|
|-- tcp_ipv4.c # IPv4 特定实现
| # tcp_v4_rcv() IPv4 入口
| # tcp_v4_connect() 客户端连接
| # tcp_v4_send_synack() 发送 SYN+ACK
| # tcp_v4_send_reset() 发送 RST
|
|-- tcp_offload.c # GSO/GRO 卸载支持
|-- tcp_metrics.c # TCP 路由度量缓存(记录历史 RTT/CWND)
|-- tcp_diag.c # inet_diag 接口(ss/netstat 使用)
|-- tcp_bpf.c # BPF sock ops 支持
|-- tcp_ulp.c # Upper Layer Protocol 支持(TLS/KCM)
|-- tcp_ao.c # TCP-AO(Authentication Option)
|-- tcp_plb.c # PLB(Protective Load Balancing)
|
|-- ip_output.c # IP 层发送(tcp_transmit_skb 的下一站)
|-- ip_input.c # IP 层接收(tcp_v4_rcv 的上一站)
发送路径:
write() --> tcp_sendmsg() --> tcp_write_xmit() --> tcp_transmit_skb()
--> ip_queue_xmit() --> ip_output() --> dev_queue_xmit()
接收路径:
网卡中断 --> napi_poll() --> netif_receive_skb() --> ip_rcv()
--> tcp_v4_rcv() --> tcp_rcv_established() --> tcp_data_queue()
--> read() --> tcp_recvmsg() --> 用户空间
握手路径:
服务端: tcp_v4_rcv() --> tcp_conn_request() --> [SYN Queue]
--> tcp_check_req() --> [Accept Queue] --> accept()
客户端: tcp_v4_connect() --> tcp_connect() --> [RTO Timer]
--> tcp_rcv_synsent_state_process() --> ESTABLISHED
tcp_rcv_state_process() 是非 ESTABLISHED 状态下的核心驱动函数,位于 net/ipv4/tcp_input.c。它处理除 ESTABLISHED 和 TIME_WAIT 之外所有状态下收到的报文。
tcp_v4_rcv()
|
+--> sk->sk_state == TCP_ESTABLISHED?
| 是 --> tcp_rcv_established() 快速路径
| 否 --> tcp_rcv_state_process() 慢速路径(状态机)
|
+--> sk->sk_state == TCP_TIME_WAIT?
是 --> tcp_timewait_state_process()
状态机函数内部的 switch-case 分发结构:
/* net/ipv4/tcp_input.c (简化) */
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
switch (sk->sk_state) {
case TCP_CLOSE:
/* 关闭状态,忽略或发 RST */
goto discard;
case TCP_LISTEN:
/* 监听状态:仅接受 SYN */
if (th->syn) {
acceptable = icsk->icsk_af_ops->conn_request(sk, skb);
/* conn_request = tcp_v4_conn_request
--> tcp_conn_request() */
}
goto discard;
case TCP_SYN_SENT:
/* 客户端等待 SYN+ACK */
tp->rx_opt.saw_tstamp = 0;
queued = tcp_rcv_synsent_state_process(sk, skb, th);
/* 验证 SYN+ACK,发 ACK,进入 ESTABLISHED */
break;
case TCP_SYN_RECV:
/* 服务端收到最终 ACK(已在 tcp_check_req 处理)
实际上内核使用 TCP_NEW_SYN_RECV 表示 request_sock */
break;
case TCP_LAST_ACK:
/* 收到对端对 FIN 的 ACK */
if (tp->snd_una == tp->write_seq) {
tcp_update_metrics(sk);
tcp_done(sk); /* 进入 CLOSED */
}
break;
case TCP_FIN_WAIT1:
/* 处理 FIN 或 FIN+ACK */
...
break;
}
}当客户端处于 SYN_SENT 状态并收到服务端 SYN+ACK 时:
tcp_rcv_synsent_state_process() net/ipv4/tcp_input.c
|
|--> 验证 ACK(确认号 == ISN+1)
|--> 验证 SYN 标志
|
|--> tcp_parse_options() 解析 TCP 选项
| MSS 协商
| 窗口缩放因子
| SACK 是否支持
| 时间戳(PAWS)
|
|--> tcp_ack() 处理 ACK,确认 SYN 已被接收
|
|--> tcp_set_state(sk, TCP_ESTABLISHED)
|
|--> tcp_send_ack() 发送第三次握手 ACK
|
+--> sk->sk_state_change(sk) 唤醒 connect() 调用者
四次挥手中最复杂的是主动关闭方的状态处理:
主动关闭方(A):
被动关闭方(B):
ESTABLISHED ESTABLISHED
| |
| close()/shutdown() |
v |
FIN_WAIT1 -------- FIN ---------->|
| CLOSE_WAIT
|<------- ACK ------------ |
FIN_WAIT2 | close()
| v
|<------- FIN ------------LAST_ACK
| send ACK |
v |
TIME_WAIT CLOSED
| 2*MSL(60s)
v
CLOSED
内核中 FIN 的发送路径:
/* net/ipv4/tcp.c */
void tcp_close(struct sock *sk, long timeout)
{
/* ... */
tcp_set_state(sk, TCP_FIN_WAIT1);
/* 构造 FIN 包 */
tcp_send_fin(sk); /* 将 FIN 追加到发送队列 */
/* ... */
}已建立连接的数据接收是 TCP 最热的路径,tcp_rcv_established() 实现了极致优化:
tcp_rcv_established() net/ipv4/tcp_input.c:6519
|
|-- 【头部预测 Fast Path】
| 条件:
| - pred_flags 匹配(无 URG/SYN/FIN/RST 等特殊标志)
| - seq == rcv_nxt(按序到达)
| - ack_seq <= snd_nxt(ACK 不超前)
|
| Fast Path 分两支:
| [a] 纯 ACK(数据长度为0):
| tcp_ack()
| 直接返回,不进入慢路径
| [b] 数据包(按序):
| tcp_rcv_rtt_measure_ts() 时间戳测 RTT
| __skb_pull() 去掉 TCP 头
| tcp_rcv_space_adjust() 调整接收缓冲区
| sk_eat_skb() 数据直接加入 sk_receive_queue
| tcp_event_data_recv() 触发延迟 ACK 逻辑
|
|-- 【慢路径 Slow Path】(特殊情况)
tcp_validate_incoming() 检查序号合法性
tcp_ack() 处理 ACK(更新 snd_una/cwnd)
tcp_data_queue() 数据入队(可能乱序)
tcp_data_snd_check() 尝试发送新数据
tcp_ack_snd_check() 检查是否需要发送 ACK
pred_flags 的计算方式揭示了它的含义:
/* include/net/tcp.h */
#define TCP_HP_BITS (~(TCP_RESERVED_BITS | TCP_FLAG_PSH))
/* pred_flags = tcp_flag_word(th) & ~TCP_HP_BITS + snd_wnd_scale */命中率通常超过 90%,这是 TCP 吞吐量的关键保障。
net/ipv4/tcp_cong.c 是算法框架的核心,通过模块化设计将算法与框架解耦:
/* net/ipv4/tcp_cong.c:26 */
struct tcp_congestion_ops *tcp_ca_find(const char *name)
{
struct tcp_congestion_ops *e;
list_for_each_entry_rcu(e, &tcp_cong_list, list) {
if (strcmp(e->name, name) == 0)
return e;
}
return NULL;
}
/* net/ipv4/tcp_cong.c:93 */
int tcp_register_congestion_control(struct tcp_congestion_ops *ca)
{
int ret = 0;
/* 使用 jhash 生成唯一 key,用于快速查找 */
ca->key = jhash(ca->name, strlen(ca->name), strlen(ca->name));
spin_lock(&tcp_cong_list_lock);
if (ca->key == TCP_CA_UNSPEC || tcp_ca_find_key(ca->key)) {
/* key 冲突 */
ret = -EEXIST;
} else {
list_add_tail_rcu(&ca->list, &tcp_cong_list);
}
spin_unlock(&tcp_cong_list_lock);
return ret;
}当新 TCP 连接建立时,拥塞控制算法的初始化链路:
tcp_create_openreq_child() 三次握手完成,创建子 socket
|
v
tcp_init_sock()
|
v
tcp_assign_congestion_control() net/ipv4/tcp_cong.c:216
|
|--> 读取 net namespace 默认算法
| ca = rcu_dereference(net->ipv4.tcp_congestion_control)
|
+--> icsk->icsk_ca_ops = ca
icsk->icsk_ca_priv 清零(104 字节私有数据区)
v(稍后)
tcp_init_congestion_control() net/ipv4/tcp_cong.c:236
|
+--> if (ca->init) ca->init(sk) 调用算法自定义初始化
/* CUBIC 在此初始化 bictcp 结构 */
/* BBR 在此分配 bbr 私有结构 */
每次收到 ACK 时,框架调用拥塞控制算法的核心入口:
/* net/ipv4/tcp_input.c 中 tcp_ack() 的拥塞控制部分 */
static void tcp_cong_control(struct sock *sk, u32 ack, u32 acked_sacked,
int flag, const struct rate_sample *rs)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
if (icsk->icsk_ca_ops->cong_control) {
/* BBR 走此分支:完全自定义控制 */
icsk->icsk_ca_ops->cong_control(sk, ack, flag, rs);
return;
}
if (tcp_in_cwnd_reduction(sk)) {
/* Recovery/Loss 状态:PRR 算法 */
tcp_cwnd_reduction(sk, acked_sacked, rs->losses, flag);
} else if (tcp_may_raise_cwnd(sk, flag)) {
/* Open/Disorder 状态:CUBIC/Reno 增长 */
tcp_cong_avoid(sk, ack, acked_sacked);
}
tcp_update_pacing_rate(sk);
}Linux 默认使用 PRR 算法(RFC 6937)代替传统的 cwnd 减半,在 Recovery 状态下更平滑地控制发送速率:
PRR 目标:Recovery 期间,以 ssthresh/cwnd 的比例减少在途包数
避免在 Recovery 期间发送过多或过少
关键公式:
prr_out - 当前 Recovery 中已发送的总包数
prr_delivered - 当前 Recovery 中已被 ACK 确认的包数
sndcnt = FLOOR(delivered * ssthresh / pipe0 - prr_out)
其中 pipe0 = Recovery 开始时的 in-flight 包数
实现位置:net/ipv4/tcp_input.c:3109 tcp_cwnd_reduction()
ECN 允许路由器在队列满之前通知端点减速,避免丢包:
路由器:检测到拥塞
--> 设置 IP 头 ECN 字段 (CE: Congestion Experienced)
|
v
接收方:
--> 在 ACK 中设置 ECE (ECN-Echo) 标志
|
v
发送方:
--> 收到 ECE 标志的 ACK
--> tcp_enter_cwr() 进入 CWR 状态
tcp_ca_event(CA_EVENT_ECN_IS_CE)
ssthresh = ca_ops->ssthresh(sk) 减小 ssthresh
snd_cwnd = ssthresh 减小 cwnd
--> 在下一个数据包中设置 CWR 标志
通知对端已响应拥塞
TCP Fast Open 的核心挑战是安全性:如果服务端在 SYN 中接收数据而不验证客户端身份,攻击者可以伪造 SYN+DATA 进行攻击。TFO 用 Cookie 机制解决这个问题。
TFO Cookie 生命周期:
【第一次连接(获取 Cookie)】
客户端 服务端
| |
| SYN + TFO Option(len=0) | 请求 Cookie
|------------------------------>|
| | tcp_fastopen_cookie_gen()
| | 用 AES-128 对客户端 IP 加密
| SYN+ACK + TFO Cookie |
|<------------------------------|
| ACK |
|------------------------------>|
客户端缓存 Cookie
【后续连接(使用 Cookie)】
客户端 服务端
| |
| SYN + TFO Cookie + DATA |
|------------------------------>|
| | tcp_try_fastopen()
| | 验证 Cookie 合法性
| | 创建子 socket 接收数据
| SYN+ACK + DATA(响应) |
|<------------------------------|
数据在 ACK 之前已发送
net/ipv4/tcp_fastopen.c:94 中的密钥初始化:
void tcp_fastopen_init_key_once(struct net *net)
{
u8 key[TCP_FASTOPEN_KEY_LENGTH];
struct tcp_fastopen_context *ctxt;
rcu_read_lock();
ctxt = rcu_dereference(net->ipv4.tcp_fastopen_ctx);
if (ctxt) {
rcu_read_unlock();
return;
}
rcu_read_unlock();
/* 生成随机密钥(每个 net namespace 独立) */
get_random_bytes(key, sizeof(key));
tcp_fastopen_reset_cipher(net, NULL, key, NULL);
}密钥存储在 net->ipv4.tcp_fastopen_ctx 中,支持密钥轮转(最多保留两个密钥,用于平滑过渡)。
net/ipv4/tcp_fastopen.c:437:
struct sock *tcp_try_fastopen(struct sock *sk, struct sk_buff *skb,
struct request_sock *req,
struct tcp_fastopen_cookie *foc,
const struct dst_entry *dst)
{
bool syn_data = TCP_SKB_CB(skb)->end_seq !=
TCP_SKB_CB(skb)->seq + 1; /* SYN 中有数据 */
/* 检查服务端是否开启 TFO */
if (!((tcp_fastopen & TFO_SERVER_ENABLE) &&
(syn_data || foc->len >= 0) &&
tcp_fastopen_queue_check(sk))) {
foc->len = -1;
return NULL;
}
if (foc->len == 0) {
/* 客户端只是请求 Cookie,不携带数据 */
tcp_fastopen_cookie_gen(sk, req, skb, &valid_foc);
} else if (foc->len > 0) {
/* 验证 Cookie */
ret = tcp_fastopen_cookie_gen_check(sk, req, skb, foc, &valid_foc);
if (ret) {
/* Cookie 有效,创建子 socket 立即接受数据 */
child = tcp_fastopen_create_child(sk, skb, req);
/* child 已处于 SYN_RECV 状态,数据已排队 */
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPFASTOPENPASSIVE);
return child;
}
}
return NULL;
}某些中间设备(防火墙、NAT)会丢弃带数据的 SYN,导致 TFO "黑洞"。Linux 内核会自动检测并临时禁用 TFO:
/* net/ipv4/tcp_fastopen.c:580 */
void tcp_fastopen_active_disable(struct sock *sk)
{
/* 更新禁用时间戳 */
WRITE_ONCE(net->ipv4.tfo_active_disable_stamp, jiffies);
/* 递增禁用计数器 */
atomic_inc(&net->ipv4.tfo_active_disable_times);
}
/* 检查是否应该禁用 */
bool tcp_fastopen_active_should_disable(struct sock *sk)
{
/* 默认禁用窗口:1小时 */
unsigned int tfo_bh_timeout =
READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_fastopen_blackhole_timeout_sec);
timeout = READ_ONCE(sock_net(sk)->ipv4.tfo_active_disable_stamp)
+ tfo_bh_timeout * HZ;
if (time_before(jiffies, timeout))
return true;
/* 超时后重置计数器,重新尝试 */
return false;
}TFO 使用了专用的 fastopen_queue 结构(嵌入在 inet_connection_sock 中)来管理处于 SYN_RECV 状态的 TFO 子 socket:
/* include/net/request_sock.h */
struct fastopen_queue {
struct request_sock *rskq_rst_head; /* RST'd request list(60秒窗口) */
struct request_sock *rskq_rst_tail;
spinlock_t lock;
int qlen; /* 当前 TFO 半连接数 */
int max_qlen; /* 最大 TFO 半连接数 */
};TFO 连接状态图:
SYN+DATA 到达
|
tcp_try_fastopen()
|
tcp_fastopen_create_child()
|
子 socket 处于 SYN_RECV,数据已在接收队列
子 socket 加入 fastopenq(非普通 accept queue)
|
应用调用 accept()
|
inet_csk_accept() 从 fastopenq 取出子 socket
|
三次握手完成(收到最终 ACK)
|
reqsk_fastopen_remove() 清理 request_sock
Linux 内核通过 DRS 算法动态调整 sk_rcvbuf,使其恰好足以容纳一个 BDP(带宽延迟积)的数据,同时避免浪费内存。
核心函数 tcp_rcvbuf_grow() 位于 net/ipv4/tcp_input.c:912:
void tcp_rcvbuf_grow(struct sock *sk, u32 newval)
{
const struct net *net = sock_net(sk);
struct tcp_sock *tp = tcp_sk(sk);
u32 rcvwin, rcvbuf, cap, oldval;
u32 rtt_threshold, rtt_us;
u64 grow;
oldval = tp->rcvq_space.space;
tp->rcvq_space.space = newval;
/* 若用户锁定了 rcvbuf 或 moderate_rcvbuf 关闭,不调整 */
if (!READ_ONCE(net->ipv4.sysctl_tcp_moderate_rcvbuf) ||
(sk->sk_userlocks & SOCK_RCVBUF_LOCK))
return;
/* DRS 落后一个 RTT,目标窗口 = 2 * 实测吞吐量 */
rcvwin = newval << 1;
rtt_us = tp->rcv_rtt_est.rtt_us >> 3;
rtt_threshold = READ_ONCE(net->ipv4.sysctl_tcp_rcvbuf_low_rtt);
if (rtt_us < rtt_threshold) {
/* 低延迟场景:线性增长,避免内存浪费 */
grow = div_u64((u64)rcvwin * rtt_us, rtt_threshold);
} else {
/* 慢启动式:允许发送方翻倍速率 */
grow = div_u64(((u64)rcvwin << 1) * (newval - oldval), oldval);
}
rcvwin += grow;
/* 考虑乱序队列中的数据 */
if (!RB_EMPTY_ROOT(&tp->out_of_order_queue))
rcvwin += TCP_SKB_CB(tp->ooo_last_skb)->end_seq - tp->rcv_nxt;
/* 限制在 tcp_rmem[2] 以内 */
cap = READ_ONCE(net->ipv4.sysctl_tcp_rmem[2]);
rcvbuf = min_t(u32, tcp_space_from_win(sk, rcvwin), cap);
if (rcvbuf > sk->sk_rcvbuf) {
WRITE_ONCE(sk->sk_rcvbuf, rcvbuf);
/* 同步更新 window_clamp */
WRITE_ONCE(tp->window_clamp, tcp_win_from_space(sk, rcvbuf));
}
}tcp_rcv_space_adjust() 在 net/ipv4/tcp_input.c:961,在每次数据被应用层读走时调用:
void tcp_rcv_space_adjust(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
int time, inq, copied;
/* 无 RTT 估算时跳过(连接刚建立) */
if (unlikely(!tp->rcv_rtt_est.rtt_us))
return;
/* 仅每 RTT 调整一次(避免频繁调整) */
time = tcp_stamp_us_delta(tp->tcp_mstamp, tp->rcvq_space.time);
if (time < (tp->rcv_rtt_est.rtt_us >> 3)) /* < RTT/8 时跳过 */
return;
/* 计算本 RTT 内应用层实际消费的字节数 */
copied = tp->copied_seq - tp->rcvq_space.seq;
/* 减去仍在内核接收队列中的数据(未被应用读走) */
inq = tp->rcv_nxt - tp->copied_seq;
copied -= inq;
/* 若消费量超过当前 space,触发扩容 */
if (copied <= tp->rcvq_space.space)
goto new_measure;
tcp_rcvbuf_grow(sk, copied);
new_measure:
/* 更新测量基准 */
tp->rcvq_space.seq = tp->copied_seq;
tp->rcvq_space.time = tp->tcp_mstamp;
}为了自动调整缓冲区,内核需要在接收侧估算 RTT。这通过 tcp_rcv_rtt_measure() 实现:
接收端 RTT 估算方式:
方式1(TCP Timestamps 开启时):
delta = tcp_rtt_tsopt_us(tp)
用 tsecr(对端回显的时间戳)与本地时间之差估算 RTT
方式2(无时间戳):
每接收到一个窗口大小的数据为一个 RTT 轮次
rcv_rtt_est.seq 标记轮次起始序号
当 rcv_nxt 越过 seq 时,记录一次 RTT
存储位置:
tp->rcv_rtt_est.rtt_us 接收侧 RTT 估算值(左移 3 位)
接收缓冲区大小直接影响向对端通告的接收窗口大小:
接收窗口通告值的计算链:
sk_rcvbuf 接收缓冲区上限
|
v
tcp_full_space(sk) = sk_rcvbuf - sk_rmem_alloc (已用)
|
v
tcp_win_from_space(sk, space) = space >> 1 (减半做安全余量)
|
v
window_clamp 通告窗口上限
|
v
rcv_wnd 实际通告值(受 window_clamp 限制)
|
v
TCP 头部 Window 字段(16位,可缩放)
窗口缩放(Window Scale)选项在三次握手时协商,最大可将窗口扩展到 2^30 = 1GB。
除了应用读取触发的扩展,tcp_grow_window() 在数据包到达时也会触发接收窗口扩展:
/* net/ipv4/tcp_input.c:700 */
static void tcp_grow_window(struct sock *sk, const struct sk_buff *skb,
bool allow_shrink)
{
struct tcp_sock *tp = tcp_sk(sk);
int room;
room = min_t(int, tp->window_clamp, tcp_space(sk)) - tp->rcv_ssthresh;
/* 若接收速率高(快速扩展阶段)且还有空间 */
if (room > 0 && !tcp_under_memory_pressure(sk)) {
int incr;
/* 每次至多增加 2*skb->len,避免过快增长 */
if (tp->rcv_ssthresh < tp->window_clamp &&
(int)tp->rcv_ssthresh + incr < tp->window_clamp)
tp->rcv_ssthresh += incr;
}
}GRO 在 net/ipv4/tcp_offload.c 中实现,是内核软件层的接收合并优化。
网卡收到多个小包(每个 MSS = 1460 字节):
[TCP pkt1: seq=0-1459] [TCP pkt2: seq=1460-2919] [TCP pkt3: seq=2920-4379]
| | |
+----------[GRO 引擎]-----------+--------------------+
|
v
合并后的大 skb(seq=0-4379, gso_segs=3, gso_size=1460)
|
v
tcp_rcv_established() 一次调用处理 3 个报文的数据量
GRO 的触发点在 NAPI 轮询的 napi_gro_receive() 中,每个连接维护一个 GRO flow 列表。
net/ipv4/tcp_offload.c:419:
struct sk_buff *tcp4_gro_receive(struct list_head *head, struct sk_buff *skb)
{
struct iphdr *iph = skb_gro_network_header(skb);
struct tcphdr *th;
/* 验证 IP checksum(GRO 层可以硬件卸载) */
if (!NAPI_GRO_CB(skb)->flush &&
skb_gro_checksum_validate(skb, IPPROTO_TCP,
inet_gro_compute_pseudo)) {
NAPI_GRO_CB(skb)->flush = 1;
return NULL;
}
/* 调用通用 TCP GRO 处理 */
th = tcp_gro_header_pull(skb);
return tcp_gro_receive(head, skb, th);
}tcp_gro_receive() 核心合并判断(net/ipv4/tcp_offload.c:286):
struct sk_buff *tcp_gro_receive(struct list_head *head, struct sk_buff *skb,
struct tcphdr *th)
{
/* 在 head 列表中查找同一 TCP 连接的已有聚合 skb */
p = tcp_gro_lookup(head, th);
if (!p)
goto out_check_final;
th2 = tcp_hdr(p);
/* 判断是否可以合并(flush = 1 表示不能合并):
* - TCP 标志不同(除 FIN/PSH 外)
* - ACK 序号不同
* - TCP 选项不同
* - seq 不连续
* - skb 加密状态不同
*/
flush = (__force int)((flags ^ tcp_flag_word(th2)) &
~(TCP_FLAG_FIN | TCP_FLAG_PSH));
flush |= (__force int)(th->ack_seq ^ th2->ack_seq);
flush |= (ntohl(th2->seq) + skb_gro_len(p)) ^ ntohl(th->seq);
if (flush || skb_gro_receive(p, skb)) {
mss = 1;
goto out_check_final;
}
/* 合并成功:合并 FIN/PSH 标志 */
tcp_flag_word(th2) |= flags & (TCP_FLAG_FIN | TCP_FLAG_PSH);
...
}当 GRO 聚合的 skb 被"冲刷"到协议栈时,需要设置 GSO 元数据,以便后续处理知道这是合并过的包:
/* net/ipv4/tcp_offload.c:369 */
void tcp_gro_complete(struct sk_buff *skb)
{
struct tcphdr *th = tcp_hdr(skb);
struct skb_shared_info *shinfo;
/* 设置 checksum 信息,允许后续校验 */
skb->csum_start = (unsigned char *)th - skb->head;
skb->csum_offset = offsetof(struct tcphdr, check);
skb->ip_summed = CHECKSUM_PARTIAL;
shinfo = skb_shinfo(skb);
/* 记录合并的段数,用于 GSO 分拆 */
shinfo->gso_segs = NAPI_GRO_CB(skb)->count;
/* AccECN:若合并包中有 CWR 标志 */
if (th->cwr)
shinfo->gso_type |= SKB_GSO_TCP_ACCECN;
}GSO 是发送方向的对应机制。当内核在发送大 skb 时,若网卡不支持硬件分段,tcp_gso_segment() 在软件层完成分段:
/* net/ipv4/tcp_offload.c:133 */
struct sk_buff *tcp_gso_segment(struct sk_buff *skb,
netdev_features_t features)
{
struct tcphdr *th;
unsigned int mss;
struct sk_buff *segs;
unsigned int seq;
th = tcp_hdr(skb);
mss = skb_shinfo(skb)->gso_size; /* 从 skb 元数据取 MSS */
/* 若网卡支持 GSO,不需要软件分段(skb_gso_ok 检查) */
if (skb_gso_ok(skb, features | NETIF_F_GSO_ROBUST)) {
skb_shinfo(skb)->gso_segs = DIV_ROUND_UP(skb->len, mss);
segs = NULL;
goto out;
}
/* 软件分段:调用通用 skb_segment() */
segs = skb_segment(skb, features);
/* 逐段更新 TCP 序号和 checksum */
seq = ntohl(th->seq);
while (skb->next) {
th->fin = th->psh = 0; /* 中间段清除 FIN/PSH */
seq += mss;
skb = skb->next;
th = tcp_hdr(skb);
th->seq = htonl(seq); /* 更新序号 */
}
/* 最后一段保留 FIN/PSH */
...
}TSO(硬件分段) GSO(软件分段) 区别
------------------ ------------------ ----------------
网卡硬件执行 内核软件执行 执行位置不同
NETIF_F_TSO 标志 NETIF_F_GSO 标志 Feature 标志不同
SKB_GSO_TCPV4 SKB_GSO_TCPV4 GSO type 相同
发送延迟极低 有少量 CPU 开销 性能差异
内核判断逻辑:
tcp_transmit_skb()
|
v
netif_needs_gso(skb, features)?
|
+--> 不需要分段(网卡支持 TSO):直接发送大 skb
|
+--> 需要软件 GSO:调用 __skb_gso_segment()
--> tcp_gso_segment()
--> 分成多个小 skb 发送
/* include/linux/skbuff.h */
struct skb_shared_info {
/* GRO/GSO 元数据 */
__u16 gso_segs; /* 合并/待分段的段数 */
__u16 gso_size; /* 每段大小(MSS) */
__u32 gso_type; /* SKB_GSO_TCPV4 / SKB_GSO_TCPV6 等 */
/* gso_type 定义 include/linux/skbuff.h:669 */
/* SKB_GSO_TCPV4 = 1 << 0 IPv4 TCP */
/* SKB_GSO_TCPV6 = 1 << 4 IPv6 TCP */
};
/* NAPI GRO 控制块(嵌入 skb->cb) */
struct napi_gro_cb {
u16 count; /* 已合并的段数 */
u8 same_flow; /* 属于同一流 */
u8 flush; /* 强制冲刷 */
u8 is_flist; /* 使用 fragment list 合并 */
u32 hash; /* 流哈希值 */
...
};tcp_rtt_estimator() 位于 net/ipv4/tcp_input.c,严格按照 RFC 6298 实现:
static void tcp_rtt_estimator(struct sock *sk, long mrtt_us)
{
struct tcp_sock *tp = tcp_sk(sk);
long m = mrtt_us; /* 本次测量的 RTT(微秒) */
u32 srtt = tp->srtt_us;
if (srtt != 0) {
/* RFC 6298: SRTT = (1-alpha)*SRTT + alpha*RTT (alpha=1/8) */
m -= (srtt >> 3); /* m = RTT - SRTT/8 = 误差 */
srtt += m; /* srtt = srtt + err = 7/8*srtt + 1/8*RTT */
/* RTTVAR = (1-beta)*RTTVAR + beta*|SRTT-RTT| (beta=1/4) */
if (m < 0) m = -m; /* |误差| */
m -= (tp->mdev_us >> 2);
tp->mdev_us += m; /* mdev_us = 3/4*mdev + 1/4*|err| */
/* 维护 mdev_max:本 RTT 轮次中 mdev 的最大值 */
if (tp->mdev_us > tp->mdev_max_us) {
tp->mdev_max_us = tp->mdev_us;
if (tp->mdev_max_us > tp->rttvar_us)
tp->rttvar_us = tp->mdev_max_us;
}
/* 每个 RTT 轮次重置 mdev_max */
if (after(tp->snd_una, tp->rtt_seq)) {
if (tp->mdev_max_us < tp->rttvar_us)
tp->rttvar_us -= (tp->rttvar_us - tp->mdev_max_us) >> 2;
tp->rtt_seq = tp->snd_nxt;
tp->mdev_max_us = tcp_rto_min_us(sk);
}
} else {
/* 第一次测量:直接初始化 */
srtt = m << 3; /* srtt = 8*RTT(因为存储时左移3位) */
tp->mdev_us = m << 1;
tp->rttvar_us = max(tp->mdev_us, tcp_rto_min_us(sk));
tp->mdev_max_us = tp->rttvar_us;
tp->rtt_seq = tp->snd_nxt;
}
tp->srtt_us = max(1U, srtt);
}RTO 的最终计算(RFC 6298 公式):
RTO = SRTT + max(G, 4*RTTVAR)
在内核中:
icsk_rto = (tp->srtt_us >> 3) + tp->rttvar_us
= SRTT + RTTVAR(RTTVAR 已包含 4* 因子)
限制范围:
TCP_RTO_MIN = HZ/5 = 200ms (include/net/tcp.h:142)
TCP_RTO_MAX = 120*HZ = 120s
退避指数(每次 RTO 超时翻倍):
icsk_rto = min(icsk_rto << icsk_backoff, TCP_RTO_MAX)
当 RTO 超时触发时,tcp_enter_loss() 将连接置于最糟糕的恢复状态:
/* net/ipv4/tcp_input.c:2553 */
void tcp_enter_loss(struct sock *sk)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
bool new_recovery = icsk->icsk_ca_state < TCP_CA_Recovery;
/* 标记所有在途包为丢失 */
tcp_timeout_mark_lost(sk);
/* 更新 ssthresh(若本窗口内首次进入 Loss) */
if (icsk->icsk_ca_state <= TCP_CA_Disorder ||
!after(tp->high_seq, tp->snd_una) ||
(icsk->icsk_ca_state == TCP_CA_Loss && !icsk->icsk_retransmits)) {
tp->prior_ssthresh = tcp_current_ssthresh(sk);
tp->prior_cwnd = tcp_snd_cwnd(tp);
tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk);
tcp_ca_event(sk, CA_EVENT_LOSS);
tcp_init_undo(tp);
}
/* cwnd 设为在途包数 + 1(保守策略) */
tcp_snd_cwnd_set(tp, tcp_packets_in_flight(tp) + 1);
tp->snd_cwnd_cnt = 0;
tp->snd_cwnd_stamp = tcp_jiffies32;
/* F-RTO:尝试避免错误的超时重传 */
tp->frto = READ_ONCE(net->ipv4.sysctl_tcp_frto) &&
(new_recovery || icsk->icsk_retransmits) &&
!inet_csk(sk)->icsk_mtup.probe_size;
}快速重传由 tcp_fastretrans_alert() 驱动,位于 net/ipv4/tcp_input.c:3328:
触发条件(满足任一):
1. 收到 3 个重复 ACK(dupACK >= 3)
2. SACK 信息显示有丢包
3. RACK 算法判定包超时
tcp_fastretrans_alert() 内部逻辑:
|
|--> 若在 Open/Disorder 状态且 dupACK >= 3:
| tcp_enter_recovery() 进入 Recovery
|
|--> 若在 Recovery 状态:
| tcp_sacktag_write_queue() 更新 SACK 评分板
| tcp_verify_retransmit_hint() 定位重传起始点
| tcp_retransmit_skb() 重传第一个标记丢失的包
|
+--> 若撤销条件成立(收到 DSACK 或对端确认序号回退):
tcp_try_undo_recovery() 尝试撤销减窗
TCP 维护一个"评分板"(scoreboard)记录每个在途包的状态:
TCP_SKB_CB(skb)->sacked 标志位含义:
TCPCB_SACKED_ACKED (0x01) 已被 SACK 确认
TCPCB_SACKED_RETRANS (0x02) 已重传
TCPCB_LOST (0x04) 判定为丢失
TCPCB_TAGBITS (0x06) SACK + RETRANS 位掩码
TCPCB_REPAIRED (0x10) 已修复(重传后被 SACK)
评分板示例:
snd_una snd_nxt
| |
v v
[pkt1:ACK'd][pkt2:LOST][pkt3:SACK'd][pkt4:SACK'd][pkt5:in-flight]
^------- 需要重传 --------^
packets_out = 3 (in-flight: pkt2+pkt5 + pkt4还未退出)
sacked_out = 2 (pkt3+pkt4)
lost_out = 1 (pkt2)
retrans_out = 0 (尚未重传)
TLP 是一种在 RTO 超时之前提前检测尾部丢包的机制,避免了 200ms+ 的 RTO 等待:
TLP 触发场景:
发送了数据后,没有收到 ACK
计时器设置为 max(2*RTT, 10ms)(远小于 RTO)
TLP 操作:
1. 发送一个探测包(最新数据或重传最后一包)
2. 若探测包触发 ACK 带 SACK 信息,RACK 可据此判断丢包
3. 避免了等待 RTO 超时(从 ~200ms 降到 ~2*RTT)
内核实现:
tcp_send_loss_probe() net/ipv4/tcp_output.c
ICSK_TIME_LOSS_PROBE = 5 定时器类型
tp->tlp_high_seq TLP 发送时的 snd_nxt(用于追踪)
TIME_WAIT 状态存在 2MSL(Maximum Segment Lifetime,默认 60 秒)的原因:
原因1:防止旧连接的迟到报文干扰新连接
连接 A(相同 4-tuple)的旧报文可能在网络中延迟
2*MSL 确保所有旧报文已经"消亡"
原因2:确保最后一个 ACK 到达对端
主动关闭方发送最终 ACK,若丢失对端会重传 FIN
TIME_WAIT 状态接收 FIN 并重传 ACK
代价:
TIME_WAIT socket 占用内存(约 300 字节/个,使用 tcp_timewait_sock)
高并发短连接场景可能耗尽本地端口
net/ipv4/tcp_minisocks.c:327 中,进入 TIME_WAIT 时会将完整的 tcp_sock 替换为轻量级的 inet_timewait_sock:
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
const struct inet_connection_sock *icsk = inet_csk(sk);
struct tcp_sock *tp = tcp_sk(sk);
struct net *net = sock_net(sk);
struct inet_timewait_sock *tw;
/* 分配轻量级 TIME_WAIT socket(约 300 字节,比 tcp_sock 小 10 倍) */
tw = inet_twsk_alloc(sk, &net->ipv4.tcp_death_row, state);
if (tw) {
struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);
/* 只复制必要信息 */
tw->tw_rcv_wscale = tp->rx_opt.rcv_wscale;
tcptw->tw_rcv_nxt = tp->rcv_nxt;
tcptw->tw_snd_nxt = tp->snd_nxt;
tcptw->tw_rcv_wnd = tcp_receive_window(tp);
tcptw->tw_ts_recent = tp->rx_opt.ts_recent;
tcptw->tw_ts_recent_stamp = tp->rx_opt.ts_recent_stamp;
tcptw->tw_ts_offset = tp->tsoffset;
/* 计算超时时间 */
if (timeo < rto) timeo = rto;
if (state == TCP_TIME_WAIT) timeo = TCP_TIMEWAIT_LEN; /* 60s */
/* 加入 TIME_WAIT hash 表,启动定时器 */
inet_twsk_hashdance_schedule(tw, sk, net->ipv4.tcp_death_row.hashinfo, timeo);
}
/* 销毁原始 tcp_sock */
tcp_update_metrics(sk);
tcp_done(sk);
}TIME_WAIT 状态下收到报文时的处理逻辑(net/ipv4/tcp_minisocks.c:101):
收到报文(处于 TIME_WAIT)
|
v
tcp_timewait_state_process()
|
|--> 检查是否在 FIN_WAIT2 子状态
| 若是,处理对端 FIN,转入真正的 TIME_WAIT
|
|--> TIME_WAIT 中的合法 SYN 处理(RFC 1122):
| 条件:
| - 新 SYN 的序号 > 期望的 rcv_nxt(或时间戳更新)
| - 非 PAWS 拒绝(时间戳不回退)
| 动作:
| *tw_isn = tcptw->tw_snd_nxt + 65535 + 2
| return TCP_TW_SYN (允许新连接复用)
|
|--> RST 报文:
| 若 sysctl_tcp_rfc1337 = 0(默认):立即销毁 TIME_WAIT socket
| 若 sysctl_tcp_rfc1337 = 1:忽略 RST,防止 TIME_WAIT 刺杀
|
+--> 窗口外报文或重复 ACK:
发送 ACK 响应(限速保护)
return TCP_TW_ACK
tcp_tw_reuse 允许在满足条件时复用 TIME_WAIT socket,由 tcp_twsk_unique() 实现(net/ipv4/tcp_ipv4.c:118):
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
int reuse = READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_tw_reuse);
const struct inet_timewait_sock *tw = inet_twsk(sktw);
const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
struct tcp_sock *tp = tcp_sk(sk);
/* FIN_WAIT2 子状态不能复用 */
if (READ_ONCE(tw->tw_substate) == TCP_FIN_WAIT2)
reuse = 0;
/* reuse=2(默认值):仅环回地址可复用(更安全) */
if (reuse == 2) {
bool loopback = false;
if (tw->tw_bound_dev_if == LOOPBACK_IFINDEX)
loopback = true;
if (ipv4_is_loopback(tw->tw_daddr) ||
ipv4_is_loopback(tw->tw_rcv_saddr))
loopback = true;
if (!loopback)
reuse = 0;
}
/* 复用条件:
* 1. TIME_WAIT socket 有时间戳记录(ts_recent_stamp != 0)
* (依赖 TCP timestamps 防止序号空间混淆)
* 2. 距离进入 TIME_WAIT 已超过 reuse_delay(默认 1 秒)
*/
reuse_thresh = READ_ONCE(tw->tw_entry_stamp) +
READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_tw_reuse_delay);
if (ts_recent_stamp &&
(!twp || (reuse && time_after32(tcp_clock_ms(), reuse_thresh)))) {
/* 复用:继承时间戳信息,确保 PAWS 正确工作 */
if (likely(!tp->repair)) {
/* 设置初始序号,避免与旧连接序号空间重叠 */
u32 seq = tcptw->tw_snd_nxt + 65535 + 2;
WRITE_ONCE(tp->write_seq, seq);
tp->rx_opt.ts_recent = tcptw->tw_ts_recent;
tp->rx_opt.ts_recent_stamp = ts_recent_stamp;
}
return 1; /* 可以复用 */
}
return 0;
}sysctl_tcp_tw_reuse 取值:
0 = 禁用复用
1 = 允许所有地址复用(包括公网,有 PAWS 保护)
2 = 默认值,仅允许环回地址复用(最保守,最安全)
sysctl_tcp_tw_reuse_delay(Linux 6.x 新增):
默认 1000ms(1秒)
在进入 TIME_WAIT 后至少等待此时间才允许复用
确保对端有足够时间完成 TIME_WAIT 处理
sysctl_tcp_rfc1337:
0 = 关闭(默认):TIME_WAIT 收到 RST 立即销毁
1 = 开启(RFC 1337 合规):忽略 TIME_WAIT 中的 RST
防止 TIME_WAIT 刺杀攻击(第三方伪造 RST)
已废弃的 tw_recycle(Linux 4.12 已删除):
tw_recycle 利用每主机时间戳缓存加速 TIME_WAIT 回收
但在 NAT 环境下会导致连接被 PAWS 错误拒绝
已从内核彻底删除,不应再使用
传统的 TCP 服务器使用单个监听 socket,多个工作线程通过 accept() 竞争锁。SO_REUSEPORT 允许多个 socket 绑定同一端口,内核负责将新连接分发给不同的 socket,彻底消除 accept 锁竞争:
传统架构(单 listen socket): SO_REUSEPORT 架构:
listen_fd listen_fd0 listen_fd1 listen_fd2
| | | |
accept_lock ← 所有线程竞争 worker0 worker1 worker2
/ | \ 独立 accept queue,无锁竞争
w0 w1 w2
net/core/sock_reuseport.c 中维护 SO_REUSEPORT 组的核心结构:
/* include/net/sock_reuseport.h */
struct sock_reuseport {
struct rcu_head rcu;
u16 max_socks; /* 数组容量 */
u16 num_socks; /* 当前 socket 数 */
u16 num_closed_socks;/* 已关闭的 socket 数 */
u16 incoming_cpu; /* 上次服务的 CPU(SO_INCOMING_CPU 优化) */
unsigned int synq_overflow_ts;
unsigned int reuseport_id;
unsigned int bind_inany:1;
unsigned int has_conns:1; /* 是否有 ESTABLISHED 连接 */
struct bpf_prog __rcu *prog; /* 可选的 BPF 分发程序 */
struct sock *socks[]; /* socket 数组 */
};net/core/sock_reuseport.c:568:
struct sock *reuseport_select_sock(struct sock *sk,
u32 hash,
struct sk_buff *skb,
int hdr_len)
{
struct sock_reuseport *reuse;
struct bpf_prog *prog;
struct sock *sk2 = NULL;
u16 socks;
rcu_read_lock();
reuse = rcu_dereference(sk->sk_reuseport_cb);
if (!reuse)
goto out;
prog = rcu_dereference(reuse->prog);
socks = READ_ONCE(reuse->num_socks);
if (likely(socks)) {
smp_rmb(); /* 与 __reuseport_add_sock() 的 smp_wmb() 配对 */
if (prog && skb) {
if (prog->type == BPF_PROG_TYPE_SK_REUSEPORT)
/* BPF SK_REUSEPORT 程序:完全自定义分发逻辑 */
sk2 = bpf_run_sk_reuseport(reuse, sk, prog, skb, NULL, hash);
else
/* 传统 SO_ATTACH_REUSEPORT_CBPF/EBPF */
sk2 = run_bpf_filter(reuse, socks, prog, skb, hdr_len);
}
/* 无 BPF 或 BPF 返回 NULL:回退到哈希分发 */
if (!sk2)
sk2 = reuseport_select_sock_by_hash(reuse, hash, socks);
}
out:
rcu_read_unlock();
return sk2;
}哈希分发函数 reuseport_select_sock_by_hash() 在 net/core/sock_reuseport.c:527,使用 hash % num_socks 选择目标 socket(对 2 的幂取模优化为位运算)。
结合 SO_INCOMING_CPU 可实现 CPU 亲和性分发,减少缓存 miss:
/* 内核在分发时优先选择与当前 CPU 关联的 socket */
static void reuseport_get_incoming_cpu(struct sock *sk,
struct sock_reuseport *reuse)
{
/* 若 SO_INCOMING_CPU 设置了 preferred_cpu,
* 优先将连接分发给绑定该 CPU 的 socket */
}Linux 4.19+ 支持 BPF_PROG_TYPE_SK_REUSEPORT,允许用 eBPF 程序自定义分发策略:
/* 用户态 BPF 程序骨架(伪代码) */
SEC("sk_reuseport")
int reuseport_selector(struct sk_reuseport_md *ctx)
{
/* ctx->data : 原始报文数据 */
/* ctx->hash : 连接 4-tuple 哈希 */
/* ctx->ip_protocol : IPPROTO_TCP */
/* 示例:按目标端口选择 socket */
if (ctx->remote_port == 8080)
return bpf_sk_select_reuseport(ctx, &sock_map, &key, 0);
/* 默认哈希分发 */
return SK_PASS;
}应用场景:
- 按 HTTP/2 stream ID 将同一客户端的请求发给同一 worker(保持有序)
- 动态调整分发权重(如金丝雀发布)
- 将特定协议的连接发给专用 worker
net/ipv4/inet_connection_sock.c 中的 SO_REUSEPORT 端口冲突检测:
/* net/ipv4/inet_connection_sock.c:173 */
static bool inet_bind_conflict(const struct sock *sk,
struct sock *sk2,
kuid_t sk_uid,
bool relax,
bool reuseport_cb_ok,
bool reuseport_ok)
{
/* 若两个 socket 都设置了 SO_REUSEPORT,且
* 没有 BPF 程序(或 BPF 程序一致),则不冲突 */
if (!relax || (!reuseport_ok && sk->sk_reuseport &&
sk2->sk_reuseport && reuseport_cb_ok &&
(sk->sk_state == TCP_TIME_WAIT ||
uid_eq(sk_uid, sock_i_uid(sk2)))))
return false; /* 允许共享 */
return true; /* 冲突,不允许绑定 */
}TSO/GSO 是现代高性能网络的基石,彻底改变了数据发送路径:
【无 TSO/GSO 时】:内核每 MSS 构造一个 skb,CPU 开销 O(N)
send()
|
v
tcp_write_xmit()
循环 N 次:
[skb1:1460B] --> ip_queue_xmit() --> dev_queue_xmit() --> 网卡
[skb2:1460B] --> ip_queue_xmit() --> dev_queue_xmit() --> 网卡
...
【有 TSO 时】:内核构造一个大 skb,CPU 开销 O(1)
send()
|
v
tcp_write_xmit()
一次:
[skb:64KB, gso_size=1460, gso_segs=44] --> 网卡
↓
网卡硬件分成 44 个包发送
net/ipv4/tcp_output.c:1734:
static int tcp_set_skb_tso_segs(struct sk_buff *skb, unsigned int mss_now)
{
int tso_segs;
if (skb->len <= mss_now) {
/* 小于 MSS 的包不需要 TSO */
tcp_skb_pcount_set(skb, 1);
TCP_SKB_CB(skb)->tcp_gso_size = 0;
return 1;
}
/* 计算需要分成多少段 */
tso_segs = DIV_ROUND_UP(skb->len, mss_now);
tcp_skb_pcount_set(skb, tso_segs);
TCP_SKB_CB(skb)->tcp_gso_size = mss_now;
return tso_segs;
}net/ipv4/tcp_output.c:2235,这是 TSO 性能的关键函数:
static u32 tcp_tso_autosize(const struct sock *sk, unsigned int mss_now,
int min_tso_segs)
{
unsigned long bytes;
u32 r;
/* 基于 pacing rate 计算每次发送量(1ms 的发送量)
* sk_pacing_rate 单位:字节/秒
* >> sk_pacing_shift(默认 10,即除以 1024)= ~1ms 间隔
*/
bytes = READ_ONCE(sk->sk_pacing_rate) >> READ_ONCE(sk->sk_pacing_shift);
/* 基于 RTT 的额外增量:RTT 越大,聚合越多(高 BDP 链路) */
r = tcp_min_rtt(tcp_sk(sk)) >>
READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_tso_rtt_log);
if (r < BITS_PER_TYPE(sk->sk_gso_max_size))
bytes += sk->sk_gso_max_size >> r;
bytes = min_t(unsigned long, bytes, sk->sk_gso_max_size);
return max_t(u32, bytes / mss_now, min_tso_segs);
}设计意图:
- 低 RTT 链路(数据中心):TSO 段数少,延迟优先
- 高 RTT 链路(跨洋):TSO 段数多,吞吐量优先
net/ipv4/tcp_output.c:2431,在某些情况下推迟发送可以聚合更多数据:
static bool tcp_tso_should_defer(struct sock *sk, struct sk_buff *skb,
bool *is_cwnd_limited,
bool *is_rwnd_limited, u32 max_segs)
{
/* Recovery 状态不推迟(需要快速重传) */
if (icsk->icsk_ca_state >= TCP_CA_Recovery)
goto send_now;
/* 最近没有发送(超过 1ms)不推迟 */
delta = tp->tcp_clock_cache - tp->tcp_wstamp_ns - NSEC_PER_MSEC;
if (delta > 0)
goto send_now;
/* 若窗口足够发送一个完整 TSO 包(max_segs * MSS),立即发 */
if (limit >= max_segs * tp->mss_cache)
goto send_now;
/* 推迟:等待更多数据聚合后一起发,减少包数量 */
return true;
send_now:
return false;
}/* net/ipv4/tcp_output.c 中 __tcp_transmit_skb() */
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
/* ... 构造 TCP 头 ... */
/* 设置 GSO 类型(通知网卡此包需要分段) */
skb_shinfo(skb)->gso_type = sk->sk_gso_type;
/* sk_gso_type 在 socket 建立时根据 IP 版本设置:
* IPv4: SKB_GSO_TCPV4 = 1 << 0 (include/linux/skbuff.h:669)
* IPv6: SKB_GSO_TCPV6 = 1 << 4 (include/linux/skbuff.h:679)
*/
/* Pacing:若启用了 pacing,更新发送时间戳 */
if (sk->sk_pacing_status != SK_PACING_NONE) {
unsigned long rate = READ_ONCE(sk->sk_pacing_rate);
/* tcp_wstamp_ns = 上一包发送时间 + 包大小/发送速率 */
}
}Linux 3.6+ 引入 TCP Small Queue,限制每个 socket 在 qdisc 层堆积的数据量,避免单连接独占网卡队列:
/* net/ipv4/tcp_output.c:2835 */
static bool tcp_small_queue_check(struct sock *sk, const struct sk_buff *skb,
unsigned int factor)
{
unsigned long limit;
/* 允许最多 2 个 TSO 包在 qdisc 中等待 */
limit = max_t(unsigned long,
2 * skb->truesize,
READ_ONCE(sk->sk_pacing_rate) >> READ_ONCE(sk->sk_pacing_shift));
limit = min_t(unsigned long, limit,
READ_ONCE(sock_net(sk)->ipv4.sysctl_tcp_limit_output_bytes));
limit <<= factor;
if (refcount_read(&sk->sk_wmem_alloc) > limit) {
/* 队列已满,不再发送,等待 TSQ 回调 */
set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags);
/* TSQ 回调在 tcp_wfree() 中触发 */
return true;
}
return false;
}QUIC(Quick UDP Internet Connections)最初由 Google 开发,后标准化为 RFC 9000。它在 UDP 之上实现了类似 TCP 的可靠传输,同时解决了 TCP 的几个固有问题:
TCP 的固有问题: QUIC 的解决方案:
+---------------------+ +---------------------------+
| 队头阻塞(HOL) | | 独立流,流间不阻塞 |
| 0-RTT 连接建立慢 | | TLS 1.3 集成,0-RTT 支持 |
| 中间设备 ossification| | 基于 UDP,易于部署更新 |
| 连接迁移困难 | | 连接 ID 机制,IP 变化无感 |
+---------------------+ +---------------------------+
截至本文基于的内核版本,Linux 内核尚未原生实现完整的 QUIC 协议,但提供了若干基础设施支持:
内核提供的 QUIC 基础:
+-- net/tls/ : KTLS(内核 TLS),QUIC 可复用 TLS 记录层加密
| tls_main.c : TLS ULP 框架
| tls_device.c : TLS offload(网卡硬件加密)
|
+-- net/udp/ : UDP 基础
| udp.c : UDP socket,QUIC 运行在 UDP 上
|
+-- net/handshake/ : TLS 握手框架(Linux 6.2+)
| tlshd.c : 通过 netlink 与用户态 tlshd 通信
|
+-- 用户态 QUIC 实现:
quiche (Cloudflare/Google), msquic (Microsoft),
NGINX QUIC, lsquic, mvfst (Facebook)
KTLS 将 TLS 加密/解密移入内核(或硬件),QUIC 可以利用类似机制:
/* net/tls/tls_main.c */
/* 启用 KTLS:应用层 TLS 握手后,将密钥传给内核 */
setsockopt(fd, SOL_TLS, TLS_TX, &crypto_info, sizeof(crypto_info));
setsockopt(fd, SOL_TLS, TLS_RX, &crypto_info, sizeof(crypto_info));
/* 此后 send()/recv() 自动在内核中完成加解密 */
/* 优点:
* 1. 零拷贝 sendfile 支持(sendfile + TLS 加密合并)
* 2. 网卡 TLS offload(TLS TOE: TCP Offload Engine)
* 3. 减少用户/内核切换
*/内核社区曾多次讨论是否将 QUIC 实现到内核中:
争议点:
支持方:
- 性能:内核 QUIC 可避免用户/内核边界拷贝
- 与内核集成:可利用 TSO/GRO 等硬件卸载
- 统一管理:与 TCP 一样可用 sysctl 调优
- XDP/eBPF 集成更容易
反对方:
- 复杂性:QUIC 包含 TLS、流控、多流等复杂逻辑
- 维护成本:QUIC 协议仍在快速演化(v1 -> v2 -> ...)
- 用户态更灵活:Google、Cloudflare 等已有成熟实现
- 内核旁路(DPDK/XDP)可同样实现高性能
当前主流方案:用 io_uring + 用户态 QUIC 库,通过 MSG_ZEROCOPY 减少拷贝:
应用程序
|
v
用户态 QUIC 库 (quiche/msquic)
| TLS 1.3 加密
v
io_uring sendmsg (zero-copy UDP)
|
v
内核 UDP + XDP 加速
XDP(eXpress Data Path)提供了一种绕过大部分内核协议栈的快速路径,可用于 QUIC 优化:
网卡收包
|
v
XDP 程序(eBPF,运行在驱动层)
|
|--> XDP_PASS : 正常走内核协议栈(普通包)
|
|--> XDP_REDIRECT : 直接重定向到 AF_XDP socket
用户态 QUIC 程序直接读取
绕过 IP/UDP 协议栈,极低延迟
性能对比:
普通 UDP recv(): ~3 μs/包
XDP + AF_XDP: ~0.3 μs/包(10倍提升)
Linux 6.5+ 内核中已有一些为 QUIC 预留的基础设施:
net/handshake/ TLS 握手框架(Linux 6.2)
功能:允许内核子系统(NFS、SMB)发起 TLS 握手
QUIC 可复用此框架进行 QUIC 握手
include/net/udp.h UDP early demux(fast path)
udp_v4_early_demux() 连接 socket 的快速查找
QUIC 的 Connection ID 路由可类比此机制实现
net/ipv4/udp_offload.c UDP GSO/GRO
udp_gso_segment() UDP 分段卸载
udp4_gro_receive() UDP 接收合并
QUIC 可利用 UDP GSO 实现批量发送
近期(1-2 年):
- 内核 UDP GRO/GSO 持续优化,使 QUIC 用户态实现受益
- io_uring 零拷贝 UDP 发送路径完善
- eBPF sk_reuseport 支持 QUIC Connection ID 路由
中期(2-5 年):
- 可能出现内核态 QUIC 基础框架(类似 KTLS 模式)
- 仅实现数据平面(加密/解密/确认),控制平面留在用户态
- 网卡 QUIC offload 标准化
参考实现:
Cloudflare quiche: https://github.com/cloudflare/quiche
Microsoft msquic: https://github.com/microsoft/msquic
内核 udp_gso: net/ipv4/udp_offload.c
内核 KTLS: net/tls/tls_main.c
struct tcp_sock(完整连接):
~2KB(含所有拥塞控制、重传、缓冲区字段)
struct inet_timewait_sock(TIME_WAIT 轻量版):
~200B(仅含 IP/端口/时间戳/序号等必要字段)
节省:90% 的内存
设计意义:高并发短连接服务器可能有数万个 TIME_WAIT,
轻量化设计避免 OOM
/* include/net/tcp.h */
struct tcp_skb_cb {
/* 序号信息(快路径访问) */
__u32 seq; /* 起始序号 */
__u32 end_seq; /* 结束序号 */
__u32 ack_seq; /* ACK 序号 */
__u32 tcp_tw_isn; /* TIME_WAIT -> SYN 复用时的 ISN */
/* 时间戳(RACK 使用) */
u64 tx_delivered_mstamp;
u64 first_tx_mstamp;
/* 标志 */
__u8 tcp_flags; /* TCP 头部标志的快速副本 */
__u8 sacked; /* SACK 评分板标志 */
__u8 ip_dsfield; /* TOS/DSCP */
/* TSO 信息 */
__u16 tcp_gso_size;
};
/* 存储在 skb->cb[48] 中,无额外内存分配 */ 普通 TCP TFO(第二次连接)
客户端 服务端 客户端 服务端
| | | |
|-------- SYN ----------------->| |-------- SYN+DATA+Cookie ----->|
| t=0 | | t=0 |
| | | | 验证 Cookie
| | | | 创建 socket
| | | | 数据入队
|<-------- SYN+ACK ------------| |<-------- SYN+ACK+DATA --------|
| t=RTT | | t=RTT |
| | | |
|-------- ACK ----------------->| |-------- ACK ----------------->|
| t=1.5*RTT | | t=1.5*RTT |
| | | |
|-------- DATA ---------------->| 服务端应用已在 t=RTT 收到数据!
| t=1.5*RTT |
| |
服务端应用在 t=2*RTT 才收到数据
节省:0.5~1 RTT 的首包延迟
# 查看 TCP 拥塞控制信息(需要 ss 7.x 以上)
ss -tni 'dst 192.168.1.1'
# 输出示例:
# cubic wscale:7,7 rto:204 rtt:0.5/0.25 ato:40 mss:1460
# cwnd:10 ssthresh:10 bytes_sent:123456 bytes_retrans:0
# bytes_acked:123456 bytes_received:98765 segs_out:100
# segs_in:95 data_segs_out:90 data_segs_in:80 send 233.6Mbps
# pacing_rate 280.3Mbps delivery_rate 233.6Mbps
# 查看 TIME_WAIT 数量
ss -tan state time-wait | wc -l
# 查看监听 socket 的 accept queue 状态
ss -tlnp
# Recv-Q 列 = 当前 accept queue 长度
# Send-Q 列 = 最大 accept queue 长度(listen backlog)# 查看 TCP 连接统计
cat /proc/net/netstat | grep -E "TcpExt:"
# 重要计数器:
# TCPLostRetransmit : 丢失重传次数(RACK 检测)
# TCPFastRetrans : 快速重传次数
# TCPSlowStartRetrans : 慢启动重传次数
# TCPTimeouts : RTO 超时次数
# TCPFastOpenActive : TFO 主动连接次数
# TCPFastOpenPassive : TFO 被动连接次数
# TCPFastOpenBlackhole : TFO 黑洞检测次数# 使用 bpftrace 追踪 RTO 超时事件
bpftrace -e 'kprobe:tcp_retransmit_timer {
printf("RTO: pid=%d comm=%s\n", pid, comm);
}'
# 使用 perf 分析 TCP 热点函数
perf top -e cycles:k --call-graph dwarf -- sleep 10 | grep tcp
# 使用 ftrace 追踪 TCP 状态转换
echo 'tcp_set_state' > /sys/kernel/debug/tracing/set_ftrace_filter
echo 'function_graph' > /sys/kernel/debug/tracing/current_tracer由 Claude Code 分析生成