WiresharkTS|接收窗口的差异"

admin 2026-10-24 12:16:44 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

Wireshark TS | 接收窗口的差异

原创

7ACE
7ACE

Echo Reply

2026年10月19日 08:08
江苏

去阅读

有判断力,放大你的能力;没有判断力,放大你的幻觉。

前言

在上一篇文章中,我们分析了 write(10000) 与 write(9000) 在 ACK 3001 时刻的发送行为差异,其根本原因是最后一个 skb 的大小不同(2000B vs 1000B),导致 tso_segs 不同,从而走入 tcp_tso_should_defer 或 tcp_nagle_test 两条不同的代码路径。

本文继续探讨一个相关现象:同样写入 10000 字节,仅仅把接收窗口从 10000 改为 12000,ACK 3001 时刻的发送行为又发生了变化。这次的差异不再是 tso_segs 的分叉,而是 tcp_tso_should_defer 内部的一个更精细的判断条件。

问题现象

仍然是两个 pkt 测试脚本(其中脚本 1 与上篇文章一致),如下所示。两者的测试路由均指定了 reno 拥塞控制算法,并将初始 CWND 改为 3,同时关闭了 TSO/GSO。脚本模拟 TCP 三次握手完成后,客户端发送数据并正常 ACK,服务器随后写入数据,之后服务器收到客户端连续的几个 ACK 确认。

# cat tcp_reno_001.pkt
`ip route change 192.0.2.0/24 dev tun0 congctl reno initcwnd 3
 ethtool -K tun0 tso off
 ethtool -K tun0 gso off`

0  socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0

+0 < S 0:0(0) win 10000 <mss 1000,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.05 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4

+0.05 < P. 1:101(100) ack 1 win 10000
+0 > . 1:1(0) ack 101

+0.05 write(4,...,10000) = 10000

+0.05 < . 101:101(0) ack 1001 win 10000
+0 %{print (tcpi_snd_cwnd)}%

+0.005 < . 101:101(0) ack 2001 win 10000
+0 %{print (tcpi_snd_cwnd)}%

+0.05 < . 101:101(0) ack 3001 win 10000
+0 %{print (tcpi_snd_cwnd)}%

+0.03 < . 101:101(0) ack 4001 win 10000
+0 %{print (tcpi_snd_cwnd)}%

+0.005 < . 101:101(0) ack 5001 win 10000
+0 %{print (tcpi_snd_cwnd)}%
#
# cat tcp_reno_003.pkt
`ip route change 192.0.2.0/24 dev tun0 congctl reno initcwnd 3
 ethtool -K tun0 tso off
 ethtool -K tun0 gso off`

0  socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0

+0 < S 0:0(0) win 12000 <mss 1000,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.05 < . 1:1(0) ack 1 win 12000
+0 accept(3, ..., ...) = 4

+0.05 < P. 1:101(100) ack 1 win 12000
+0 > . 1:1(0) ack 101

+0.05 write(4,...,10000) = 10000

+0.05 < . 101:101(0) ack 1001 win 12000
+0 %{print (tcpi_snd_cwnd)}%

+0.005 < . 101:101(0) ack 2001 win 12000
+0 %{print (tcpi_snd_cwnd)}%

+0.05 < . 101:101(0) ack 3001 win 12000
+0 %{print (tcpi_snd_cwnd)}%

+0.03 < . 101:101(0) ack 4001 win 12000
+0 %{print (tcpi_snd_cwnd)}%

+0.005 < . 101:101(0) ack 5001 win 12000
+0 %{print (tcpi_snd_cwnd)}%
#

总体来看,两个 pkt 脚本写入的数据量相同(10000 字节 = 10 个 MSS),唯一的区别是接收窗口的大小:一个是 10000 字节,一个是 12000 字节——后者多了 2000 字节。

同样运行 tcpdump 捕获数据包对比看看。

# packetdrill tcp_reno_001.pkt
4
5
6
7
8
#

# tcpdump -i any -nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
10:14:43.103586 tun0  In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [S], seq 0, win 10000, options [mss 1000,nop,nop,sackOK], length 0
10:14:43.103646 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [S.], seq 4103320984, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
10:14:43.153896 tun0  In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [.], ack 1, win 10000, length 0
10:14:43.203986 tun0  In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [P.], seq 1:101, ack 1, win 10000, length 100: HTTP
10:14:43.204135 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [.], ack 101, win 64140, length 0
10:14:43.254368 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [.], seq 1:1001, ack 101, win 64140, length 1000: HTTP
10:14:43.254420 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [.], seq 1001:2001, ack 101, win 64140, length 1000: HTTP
10:14:43.254445 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [.], seq 2001:3001, ack 101, win 64140, length 1000: HTTP
10:14:43.304644 tun0  In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [.], ack 1001, win 10000, length 0
10:14:43.304870 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [.], seq 3001:4001, ack 101, win 64140, length 1000: HTTP
10:14:43.304873 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [P.], seq 4001:5001, ack 101, win 64140, length 1000: HTTP
10:14:43.310247 tun0  In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [.], ack 2001, win 10000, length 0
10:14:43.310439 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [.], seq 5001:6001, ack 101, win 64140, length 1000: HTTP
10:14:43.310442 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [P.], seq 6001:7001, ack 101, win 64140, length 1000: HTTP
10:14:43.361452 tun0  In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [.], ack 3001, win 10000, length 0
10:14:43.361923 tun0  Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [P.], seq 7001:8001, ack 101, win 64140, length 1000: HTTP
10:14:43.392152 ?     In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [.], ack 4001, win 10000, length 0
10:14:43.392658 ?     Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [.], seq 8001:9001, ack 101, win 64140, length 1000: HTTP
10:14:43.392663 ?     Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [P.], seq 9001:10001, ack 101, win 64140, length 1000: HTTP
10:14:43.398015 ?     In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [.], ack 5001, win 10000, length 0
10:14:43.427999 ?     Out IP 192.168.4.128.8080 > 192.0.2.1.52445: Flags [R.], seq 10001, ack 101, win 64140, length 0
10:14:43.428028 ?     In  IP 192.0.2.1.52445 > 192.168.4.128.8080: Flags [R.], seq 101, ack 5001, win 10000, length 0
#
# packetdrill tcp_reno_003.pkt
4
5
6
7
8
#

# tcpdump -i any -nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
15:17:40.222963 tun0  In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [S], seq 0, win 12000, options [mss 1000,nop,nop,sackOK], length 0
15:17:40.223053 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [S.], seq 1477131195, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
15:17:40.273783 tun0  In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [.], ack 1, win 12000, length 0
15:17:40.323958 tun0  In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [P.], seq 1:101, ack 1, win 12000, length 100: HTTP
15:17:40.324039 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [.], ack 101, win 64140, length 0
15:17:40.375175 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [.], seq 1:1001, ack 101, win 64140, length 1000: HTTP
15:17:40.375261 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [.], seq 1001:2001, ack 101, win 64140, length 1000: HTTP
15:17:40.375298 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [.], seq 2001:3001, ack 101, win 64140, length 1000: HTTP
15:17:40.425623 tun0  In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [.], ack 1001, win 12000, length 0
15:17:40.425724 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [.], seq 3001:4001, ack 101, win 64140, length 1000: HTTP
15:17:40.425725 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [P.], seq 4001:5001, ack 101, win 64140, length 1000: HTTP
15:17:40.430779 tun0  In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [.], ack 2001, win 12000, length 0
15:17:40.430897 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [.], seq 5001:6001, ack 101, win 64140, length 1000: HTTP
15:17:40.430899 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [P.], seq 6001:7001, ack 101, win 64140, length 1000: HTTP
15:17:40.481729 tun0  In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [.], ack 3001, win 12000, length 0
15:17:40.481985 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [.], seq 7001:8001, ack 101, win 64140, length 1000: HTTP
15:17:40.481995 tun0  Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [P.], seq 8001:9001, ack 101, win 64140, length 1000: HTTP
15:17:40.512183 ?     In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [.], ack 4001, win 12000, length 0
15:17:40.512286 ?     Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [P.], seq 9001:10001, ack 101, win 64140, length 1000: HTTP
15:17:40.517573 ?     In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [.], ack 5001, win 12000, length 0
15:17:40.530264 ?     Out IP 192.168.14.1.8080 > 192.0.2.1.49599: Flags [R.], seq 10001, ack 101, win 64140, length 0
15:17:40.530277 ?     In  IP 192.0.2.1.49599 > 192.168.14.1.8080: Flags [R.], seq 101, ack 5001, win 12000, length 0
#

通过对比数据包,同样有些许不同,简单总结如下:

  1. 服务端发送数据后,在收到客户端 ACK 确认时 cwnd 均逐步增加,从初始值 3 增至 8;
  2. 两个测试脚本从 ACK num 3001 之后开始出现不一样的情况:

  3. 测试脚本 1 发送了一个数据包 Seq 7001:8001;

  4. 测试脚本 2 发送了两个数据包 Seq 7001:8001 和 Seq 8001:9001。

换句话说,虽然 write 写入数据段的数量一样,但后者脚本在修改了接收窗口为 12000 字节后,也会多发出 1 个数据段。

tcpdump 抓包的差异点:

# tcp_reno_001.pkt (win=10000)
22:30:02.963398 tun0  In  IP ... ack 3001
22:30:02.963422 tun0  Out IP ... Flags [P.], seq 7001:8001    ← 仅1个段
22:30:02.993550 tun0  In  IP ... ack 4001
22:30:02.993580 tun0  Out IP ... Flags [.], seq 8001:9001
22:30:02.993582 tun0  Out IP ... Flags [P.], seq 9001:10001

# tcp_reno_003.pkt (win=12000)
15:17:40.481729 tun0  In  IP ... ack 3001
15:17:40.481985 tun0  Out IP ... Flags [.], seq 7001:8001    ← 发2个段
15:17:40.481995 tun0  Out IP ... Flags [P.], seq 8001:9001
15:17:40.512183       In  IP ... ack 4001
15:17:40.512286       Out IP ... Flags [P.], seq 9001:10001

那么,疑问来了:

  • 问题:测试脚本 2 为什么也能发出两个数据段?两者唯一的区别是接收窗口大小,为何这一差异会引发截然不同的发送行为?

问题分析

测试环境

两个 packetdrill 脚本共享以下关键配置:

参数 值
拥塞控制算法 Reno
初始拥塞窗口 (initcwnd) 3
MSS 1000
发送数据 10000
TSO/GSO ethtool -K tun0 tso off gso off
TCP Nagle 默认(未设置 TCP\_NODELAY)
tcp_tso_win_divisor 3

两者唯一区别:

// Script 1: 接收窗口 win = 10000
+0 < S 0:0(0) win 10000 <mss 1000,nop,nop,sackOK>

// Script 2: 接收窗口 win = 12000
+0 < S 0:0(0) win 12000 <mss 1000,nop,nop,sackOK>

write() 创建的 skb 队列

要理解后续行为,首先需要弄清楚接收窗口的差异如何影响 write() 创建的 skb 结构。

xmit\_size\_goal 的计算

与上篇文章相同,tcp_sendmsg_locked() 通过 tcp_xmit_size_goal() 决定每个 skb 的最大尺寸,其中关键一步是 tcp_bound_to_half_wnd(tp, sk->sk_gso_max_size)。该函数(net/ipv4/tcp.h)的核心逻辑:

/* Bound MSS / TSO packet size with the half of the window */
static inline int tcp_bound_to_half_wnd(struct tcp_sock *tp, int pktsize)
{
int cutoff;

/* When peer uses tiny windows, there is no use in packetizing
  * to sub-MSS pieces for the sake of SWS or making sure there
  * are enough packets in the pipe for fast recovery.
  *
  * On the other hand, for extremely large MSS devices, handling
  * smaller than MSS windows in this way does make sense.
  */
if (tp->max_window > TCP_MSS_DEFAULT)
  cutoff = (tp->max_window >> 1);
else
  cutoff = tp->max_window;

if (cutoff && pktsize > cutoff)
returnmax_t(int, cutoff, 68U - tp->tcp_header_len);
else
return pktsize;
}

max_window 是连接过程中观察到的最大接收窗口。由于 sk_gso_max_size 远大于 max_window,tcp_bound_to_half_wnd 实际上将 size_goal 限制为 max_window >> 1:

脚本 max\_window cutoff = max\_window >> 1 size\_goal
Script 1 (win=10000) 10000 5000 5000
Script 2 (win=12000) 12000 6000 6000

接收窗口从 10000 增至 12000,size_goal 相应从 5000 增至 6000。

write() 返回时的队列状态

两个脚本均为 write(10000),cwnd=3,mss=1000。由于 size_goal 不同,tcp_sendmsg_locked() 主循环创建的 skb 队列也不同:

脚本 skb1 skb2
Script 1 (size\_goal=5000) 5000B (seq 3001-8000) 2000B (seq 8001-10000)
Script 2 (size\_goal=6000) 6000B (seq 3001-9000) 1000B (seq 9001-10000)

此时已有 3 个 MSS(seq 1-3000)在途。两者的差异在于:Script 1 的 skb1 较小(5000B),剩余数据较多地分到了 skb2(2000B);Script 2 的 skb1 较大(6000B),skb2 只有 1000B。

ACK 3001 时刻的队列演变

初始发送的 3 个 MSS(seq 1-3000)依次收到 ACK,每收到一个 ACK,慢启动使 cwnd 加 1,同时 tcp_write_xmit() 从队头 skb 切出对应字节数发送。

以 Script 2 为例(skb1=6000B, skb2=1000B):

事件 cwnd in\_flight 操作 队列状态
write 返回 3 3 — skb1:6000B, skb2:1000B
ACK 1001 4 2 skb1 切 2000B (3001-5000) 发出 skb1:4000B(5001-9000), skb2:1000B
ACK 2001 5 3 skb1 切 2000B (5001-7000) 发出 skb1:2000B(7001-9000), skb2:1000B
ACK 3001 6 4 见下文分析

Script 1 的演变(skb1=5000B, skb2=2000B):

事件 cwnd in\_flight 操作 队列状态
write 返回 3 3 — skb1:5000B, skb2:2000B
ACK 1001 4 2 skb1 切 2000B (3001-5000) 发出 skb1:3000B(5001-8000), skb2:2000B
ACK 2001 5 3 skb1 切 2000B (5001-7000) 发出 skb1:1000B(7001-8000), skb2:2000B
ACK 3001 6 4 见下文分析

到 ACK 3001 到达时,两个脚本发送队列中剩余 skb 的结构:

位置 Script 1 (win=10000) Script 2 (win=12000)
第 1 个待发 skb 1000B (seq 7001-8000) 2000B (seq 7001-9000)
最后一个 skb 2000B (seq 8001-10000) 1000B (seq 9001-10000)

这就是两个脚本的关键差异:在 ACK 3001 时刻,Script 1 的队头 skb 是 1000 字节(1 个 MSS),而 Script 2 的队头 skb 是 2000 字节(2 个 MSS)。这一差异源于 size_goal 的不同,进而影响了后续 tcp_write_xmit() 的处理路径。


tcp\_write\_xmit 的路径分叉

tcp_write_xmit() 的核心循环中,每个 skb 首先通过 tcp_set_skb_tso_segs() 确定 tso_segs,然后根据 tso_segs 的值走入不同的判断路径:tso\_segs == 1 走 tcp_nagle_test(),tso\_segs > 1 走 tcp_tso_should_defer()。

第一个 skb 的处理

Script 1(1000B, tso\_segs=1)→ tcp_nagle_test 路径 → 放行 → 刷新 tcp\_wstamp\_ns

skb1: len=1000, tso_segs=1, cwnd_quota=2
→ tso_segs==1 → tcp_nagle_test TRUE -> __tcp_transmit_skb()
→ 发送 7001:8001 (1000 bytes),in_flight: 4→5
→ 关键副作用:tcp_wstamp_ns 被刷新为\"现在\"

1000 字节的 skb 不小于 MSS(1000),不满足 Nagle 的\"小包\"条件,直接允许发送。发送这一动作会把 tp->tcp_wstamp_ns 更新为当前时刻——这是后续判断的关键伏笔。发送后 in_flight 从 4 增至 5。

Script 2(2000B, tso\_segs=2)→ tcp_tso_should_defer 路径(未先发送)

skb1: len=2000, tso_segs=2, cwnd_quota=2
→ tso_segs > 1 → tcp_tso_should_defer()
→ 注意:未发生发送动作,tcp_wstamp_ns 保持旧值

此时 in_flight=4,没有先发送任何 skb,直接进入 tcp_tso_should_defer() 判断。tcp_wstamp_ns 保留着 ACK 2001 时刻发送 5001:6001/6001:7001 时的时间戳,距现在约 51ms。

tcp\_tso\_should\_defer 的 delta 判断

tcp_tso_should_defer() 入口处第一道关卡是 delta 检查(net/ipv4/tcp_output.c):

 /* Avoid bursty behavior by allowing defer
  * only if the last write was recent (1 ms).
  * Note that tp->tcp_wstamp_ns can be in the future if we have
  * packets waiting in a qdisc or device for EDT delivery.
  */
 delta = tp->tcp_clock_cache - tp->tcp_wstamp_ns - NSEC_PER_MSEC;
 if (delta > 0)
  goto send_now;

send_now:
 return false;
}

含义是:如果距离上次发送已超过 1ms,就不允许延迟,直接发送(避免突发)。tcp_wstamp_ns 是上次发送 skb 时更新的时间戳;tcp_clock_cache 是当前 TCP 时钟。

Script 2(未先发送,tcp\_wstamp\_ns 是 50ms 前的旧值)→ delta > 0 → 直接 send\_now

tcp_wstamp_ns ≈ ACK 2001 时刻发送 6001:7001 的时间
tcp_clock_cache ≈ ACK 3001 时刻(50ms 后)
delta = tcp_clock_cache - tcp_wstamp_ns - 1ms = 51ms - 1ms ≈ +50ms
delta > 0 → goto send_now → return false(不延迟)

Script 2 在第一道关卡就被放行,直接发送 2000B(seq 7001:8001 和 8001:9001)。

Script 1(刚发送过 skb1,tcp\_wstamp\_ns 已被刷新为\"现在\")→ delta < 0 → 继续判断

Script 1 的第一个 skb(1000B)通过 tcp_nagle_test 发送后,tcp_wstamp_ns 被刷新为当前时刻。随后处理第二个 skb(2000B, tso\_segs=2)时进入 tcp_tso_should_defer:

tcp_wstamp_ns ≈ 刚发送 skb1 的时刻(与 tcp_clock_cache 同一微秒)
delta = tcp_clock_cache - tcp_wstamp_ns - 1ms ≈ 0 - 1ms = -1ms
delta > 0 为假 → 不 goto send_now,继续后续判断

delta 精确等于 -1ms 是必然结果:内核在发送 skb1 后立即处理下一个 skb 进入 tcp_tso_should_defer,两个动作在同一微秒内完成,tcp_clock_cache - tcp_wstamp_ns ≈ 0,故 delta ≈ -1ms。

继续往下走,最终走到 cong_win <= skb->len 判断:

skb2:\tsend_win = 5000,\tcong_win = 1000,\tskb->len = 2000
→ cong_win <= skb->len
→ DEFER (return true)

Script 1 的 skb2 在第一道关卡没被放行,又因为 cong_win 剩余配额不足以容纳完整 skb,最终延迟发送。

结果对比

Script 1 (win=10000) Script 2 (win=12000)
size\_goal 5000 6000
ACK 3001 队头 skb 1000B (tso\_segs=1) 2000B (tso\_segs=2)
走的判断路径 tcp_nagle_test (放行) tcp_tso_should_defer
进入 tso\_should\_defer 前是否发送过 skb 是 (刚发了 skb1) 否 (直接判断)
tcp\_wstamp\_ns 状态 刚被刷新为\"现在\" 51ms 前的旧值
delta -1ms +50ms
delta > 0? 否(继续判断) 是 → send\_now
后续判断到达 cong_win<=skb->len → DEFER
ACK 3001 后发送 1 段 (7001:8001) 2 段 (7001:8001 + 8001:9001)

结论

问题解答

为什么接收窗口从 10000 改为 12000 后,ACK 3001 时刻能多发出一个数据段?

接收窗口的差异通过 tcp_bound_to_half_wnd() 影响了 size_goal:win=10000 时 size_goal=5000,win=12000 时 size_goal=6000。这导致 ACK 3001 时刻队头 skb 的大小不同(1000B vs 2000B),进而 tso_segs 不同(1 vs 2),走了不同的判断路径:

  • Script 1(size\_goal=5000):队头 skb 是 1000B(tso\_segs=1),走 tcp_nagle_test 放行,先发送 skb1,把 tcp_wstamp_ns 刷新为\"现在\";随后 2000B 的 skb2 走 tcp_tso_should_defer,此时 delta = -1ms(刚发送,时间戳未老化),不满足 delta > 0,继续往下走,最终因 cong_win=1000 <= skb->len=2000 而延迟。
  • Script 2(size\_goal=6000):队头 skb 是 2000B(tso\_segs=2),直接走 tcp_tso_should_defer(未先发送任何 skb),tcp_wstamp_ns 保留 51ms 前的旧值,delta = +50ms > 0,在第一道关卡就被放行 send_now,直接发送 2000B。

根本原因

两个测试脚本行为差异的根本原因是 tcp_tso_should_defer()入口的 delta 检查与 tso_segs 路径分叉的副作用联合作用:

  1. max_window 决定 size_goal(size_goal = max_window >> 1),进而决定 ACK 3001 时刻队头 skb 的大小
  2. 队头 skb 大小决定 tso_segs(1000B→1, 2000B→2),决定走 tcp_nagle_test 还是 tcp_tso_should_defer
  3. 关键差异:走 tcp_nagle_test 放行时会先发送 skb,副作用是刷新 tcp_wstamp_ns;走 tcp_tso_should_defer 时直接判断,不刷新 tcp_wstamp_ns
  4. Script 1 的 skb2 进入 tcp_tso_should_defer 时 tcp_wstamp_ns 刚被刷新,delta < 0,继续判断后因 cong_win <= skb->len 延迟
  5. Script 2 的 skb1 进入 tcp_tso_should_defer 时 tcp_wstamp_ns 是 51ms 前的旧值,delta > 0,直接 send_now

仅仅因为接收窗口多了 2000 字节,就使得 size_goal 从 5000 增至 6000,改变了 ACK 3001 时刻队头 skb 的大小,进而改变了走 tcp_nagle_test 还是 tcp_tso_should_defer。前者会先发送刷新时间戳,后者直接判断保留旧时间戳——这一差异导致两者进入 tcp_tso_should_defer 时 delta 的取值截然不同(-1ms vs +50ms),最终决定了是否被 delta > 0 关卡放行。

往期推荐

1. Wireshark 提示和技巧 | 捕获点之 TCP 三次握手

2. Wireshark 提示和技巧 | a == ${a} 显示过滤宏

3. Wireshark TS | 当超时或快速重传遇到零窗口

4. Wireshark TS | 防火墙空闲会话超时问题

5. 网络设备 MTU MSS Jumboframe 全解

后台回复「TT」获取 Wireshark 提示和技巧系列 合集

后台回复「TS」获取 Wireshark Troubleshooting系列 合集

如需交流,可后台直接留言,我会在第一时间回复,谢谢!

WiresharkTS|接收窗口的差异" 网络安全文章

WiresharkTS|接收窗口的差异"

本文通过packetdrill脚本对比实验,分析LinuxTCP协议栈中接收窗口大小从10000字节改为12000字节时,相同写入10000字节数据在ACK3001时刻的发送行为差异。核心发现是差异源于tcp_tso_should_defer函数内部更精细的判断条件,而非此前分析的tso_segs分叉。文章提供了详细抓包对比数据,对理解TCP拥塞控制与窗口管理机制具有实践参考价值。
今日速递|CryptSn0w开源、InfinityCore安卓root、opa334劝停27.2" 网络安全文章

今日速递|CryptSn0w开源、InfinityCore安卓root、opa334劝停27.2"

本文为2026年10月18日安全资讯速递,汇总看雪论坛热点与X平台二进制安全动态。核心内容包括:z0bdbg调试器AIagent、NVIDIAFECS固件逆向注释、代码审计流水线xNext、SDC2026议题预告、HypervisorDMA对抗讨论;CryptSn0w开源(已打补丁,非可用越狱)、opa334建议暂缓升级iOS27.2、Frida17.24.1发布、KernelSU主线preinit支持、InfinityCore披露六个Linux内核漏洞(含可root三星GalaxyS26Ultra的CV
智算网络技术白皮书(2026,100页)" 网络安全文章

智算网络技术白皮书(2026,100页)"

该白皮书系统阐述面向AI大模型时代的智算网络技术体系,针对万亿参数训练带来的网络瓶颈,提出智能无损网络、智能流量调度、高可用保障及智能运维平台四大核心技术,涵盖PFC/ECN/IPCC、SprayLink/FGLB、DPSH/ISDF及RDMATelemetry等方案,构建无丢包低时延高吞吐易运维的端到端解决方案,支撑大规模GPU集群高效稳定运行。
评论:0   参与:  0