这一关回答:发多快才"刚好"。答案比直觉更激进:CUBIC 把丢包当拥塞信号,BBR 把网络本身当对象建模——瓶颈带宽(BtlBw)× 最小 RTT(RTprop)= 最优在途量(BDP)。丢包是拥塞的滞后指标,模型是前瞻指标。带宽是物理的,延迟是设计的。
四个机制支点:CUBIC 的丢包驱动(对照组)、BBR 的模型与四状态机(net/ipv4/tcp_bbr.c)、TSO/GRO/GSO 分段卸载(per-packet 开销才是小包线速的瓶颈)、XDP 驱动层 hook(skb 分配之前就动手)。M13 的 tcp_write_xmit 里 cwnd/pacing 就是这里算出来的;M17 的 DMA 描述符环是 TSO 大包的物理载体。
CUBIC(net/ipv4/tcp_cubic.c)是经典的丢包驱动算法,逻辑是"丢包 = 拥塞":慢启动指数涨,丢包则 cwnd 乘 beta(源码常量 β=717/1024≈0.7),然后进拥塞避免。它的窗口增长曲线是三次函数 W(t)=C(t-K)³+Wmax——快速回到上次丢包点附近,再缓慢向上探测。在悬崖边反复试探,就是 CUBIC 的全部哲学。
问题不在算法笨,而在信号滞后且噪声大:深缓冲(bufferbloat)下延迟先爆炸、迟迟不丢包,CUBIC 会把队列灌满——你的包在排队,不在传输;浅缓冲 + 随机丢包下,丢包根本不是拥塞,CUBIC 却被误伤降速。丢包这个信号,又迟又吵。
BBR(net/ipv4/tcp_bbr.c)的注释开宗明义:"The core algorithm does not react directly to packet losses or delays"(核心算法不直接响应丢包或延迟)。它维护两个模型参数:BtlBw = 10 个 RTT 内测到的最大 delivery rate(windowed_max(delivered / elapsed, 10 round trips)),RTprop = 10 秒内测到的最小 RTT(windowed_min(rtt, 10 seconds))。最优在途量 BDP = BtlBw × RTprop——管道有多粗、管道有多长,两个数就够了。
"不看丢包"要读准:它是核心控制信号层面的说法。同一段注释紧接着说:观察到丢包时,BBR 可能调整下一次 ACK 对应的发送量;估计到链路上有 traffic policer 时,也可能调发送速率以控制 drop rate。丢包仍被处理、仍带来重传开销,只是不再是"降速"的直接判据——用模型代替症状,而不是假装症状不存在。
发送速率 pacing_rate = pacing_gain × BtlBw,窗口 cwnd = max(cwnd_gain × BDP, 4)(cwnd_gain=2,源码常量)。模型算出速率,pacing 负责均匀执行,见 14.4。
四状态机(tcp_bbr.c 头部注释的状态图):STARTUP 以约 2.89 的 gain(源码 bbr_high_gain = 2/ln(2),注释说明目的是让平滑 pacing rate 每 RTT 翻倍)指数探测;连续 3 个 round 带宽没有显著增长(阈值 1.25×,bbr_full_bw_thresh=5/4、bbr_full_bw_cnt=3)则判定管道已满,退出 STARTUP → DRAIN(排空启动期堆起来的队列)→ PROBE_BW(稳态)⇄ PROBE_RTT。
PROBE_BW 的 gain 按 8 拍循环:5/4、3/4、然后六个 1.0。源码注释写明了每拍的用途:5/4 探测还有没有多余带宽(probe for more available bw),3/4 排空队列并给其他流让路(drain queue and/or yield bw to other flows),1.0 巡航。平均 pacing 目标比估计带宽低约 1%(bbr_pacing_margin_percent=1)。
PROBE_RTT 触发:流在整个 min_rtt 窗口内持续发送、且 10 秒没见到匹配或更小的 RTT 样本——动作是降到 4 个在途包(bbr_cwnd_min_target=4)、持续 200ms(bbr_probe_rtt_mode_ms=200)重测传播延迟,然后回 PROBE_BW。降到 4 个包是为了把队列排空,测到的是"空管道"的 RTT。长连接绝大部分时间在 PROBE_BW 里打转(源码注释原话:"spends the vast majority of its time … in PROBE_BW")。
pacing 是 BBR 的另一半:模型假设"发送速率 = pacing_rate"是均匀的,突发发送会在瓶颈处瞬间制造排队,ACK 到达速率被队列扭曲,BtlBw 估计失真——模型的前提是行为配合。实现上靠 sk_pacing_rate + fq qdisc 做微突发整形;注释原话:没有 fq qdisc 时,TCP 退回每 socket 一个高精度 timer 的内部 pacing——能用,但更耗资源。
TSO 的攒批大小也不是拍脑袋的固定值:bbr_tso_segs_goal() 按当前 pacing rate、MSS 等动态计算目标 segment 数(上限 0x7F)。pacing 决定"多快",TSO 决定"多大的块"——两个旋钮都听模型的。
小包线速的瓶颈不在"切包"这个动作,而在 per-packet 固定开销:skb 分配、协议栈逐层遍历、中断/轮询次数。10G 线速 64 字节包每秒 1488 万包——CPU 先死在次数上,不死在字节上。卸载的思路:让"次数"降下来。
TSO(发方向):协议栈把数据攒成一个大切片(super-skb),skb_shared_info 里的 gso_size 记 MSS,网卡硬件按 MSS 切片。典型上限是 64K(GSO_LEGACY_MAX_SIZE=65536),具体看设备能力(tso_max_size)。协议栈只遍历一次——几十个 MSS 的数据,"过路费"只交一遍。GSO 是软件版 TSO:网卡不支持时内核自己切(tcp_gso_segment() 按 gso_size 切分,net/ipv4/tcp_offload.c)。重传仍按 MSS 粒度——大切片丢了不用重传整个 64K,故障域没有放大。
GRO(收方向):tcp_gro_receive()(net/ipv4/tcp_offload.c)在 NAPI poll 里把相容的包尝试合并:同流(端口匹配)、序号连续、标志/ack_seq/选项一致,任一不符就触发 flush 把暂存的包上送——PSH/FIN/RST/SYN/URG 这类标志位会强制 flush。GRO 不是"攒够一批再上送",而是"来一个、看能不能跟暂存的拼上,拼不上就送走"。代价是暂存等待引入的延迟:吞吐优先的场景(文件传输)GRO 是赚的;延迟敏感场景要权衡——每个 workload 的延迟/CPU 账本不一样,没有"应该关"的通用答案。
XDP 的 hook 点在驱动收包之后、skb 分配之前——内核里最早的可编程点。M13 的信封(skb)本身就不便宜:分配、初始化、逐层解析都是钱。DDoS 清洗要在"花钱之前"把包扔掉——hook 越早,沉没成本越低。
五个动作(include/uapi/linux/bpf.h 的 enum xdp_action):XDP_ABORTED(异常丢弃)、XDP_DROP(丢弃)、XDP_PASS(放行进协议栈)、XDP_TX(从收包网卡直接发回)、XDP_REDIRECT(重定向到另一网卡/CPU)。XDP 程序是 eBPF 程序的一种类型(BPF_PROG_TYPE_XDP,include/uapi/linux/bpf.h 的 enum bpf_prog_type),由 bpf_prog_run_xdp() 执行(定义在 include/net/xdp.h),驱动 XDP hook 在单个 NAPI poll cycle 内被调用。
eBPF 和 XDP 的关系:eBPF 是指令集 + 验证器 + JIT 的执行体系,XDP 是网络数据路径上的程序类型/hook 点。官方 sysctl 文档(Documentation/admin-guide/sysctl/net.rst)原话:BPF 是"在多种 hook 点执行字节码"的基础设施,LLVM 后端把受限 C 编译成 BPF 指令,bpf(2) 加载、过内核 verifier,JIT 再翻译成机器码。同一个 eBPF 生态,还可以挂 kprobe、tracepoint、tc——XDP 只是其中离网卡最近的一个。
两种挂载模式(include/uapi/linux/if_link.h):XDP_FLAGS_DRV_MODE 是 native 模式——驱动在收包路径直接调 XDP 程序,需要驱动支持;XDP_FLAGS_SKB_MODE 是 generic 模式——不要求驱动实现 native hook 的软件路径,性能不如 native。选哪种,看驱动和内核配置,没有"任何网卡都能跑 native"这种事。
为什么 BBR 不看丢包?
因为丢包是拥塞的滞后、噪声大的信号。深缓冲下延迟爆炸了还不丢包——CUBIC 按丢包行事,会把队列灌到延迟上天(bufferbloat 的本质:排队延迟被当成了带宽);浅缓冲下随机丢包根本不是拥塞,CUBIC 却被误伤。BBR 直接估计"管道有多粗(BtlBw)、有多长(RTprop)",一步算出最优在途量——用模型代替症状。(注意:"不看丢包"是核心控制信号层面的说法,实现上 BBR 仍处理丢包、仍会因 policer 调速,见 14.2。)
为什么分段卸载能省 CPU?
因为瓶颈在 per-packet 固定开销,不在字节数。skb 分配、协议栈遍历、中断/轮询——每个包都要交一次"过路费",和包大小无关。几十个 MSS 的数据一次遍历 vs 几十次遍历,省的是次数。TSO/GRO 把"次数"降下来,CPU 才追得上线速。优化的第一性原理:先找到按次收费的环节。
为什么 XDP 要挂在驱动层,而不是协议栈里?
因为 skb 分配本身就是一笔开销:分配、初始化、逐层解析,包还没被处理就已经花了钱。DDoS 场景下绝大多数包都是要扔的——在协议栈里扔,等于先付全款再退货;在驱动层 XDP_DROP,等于在收银台前就拦下。越早的 hook,沉没成本越低。eBPF 在这里是"怎么写这个 hook"(指令集 + verifier + JIT),XDP 是"hook 挂在哪"(驱动收包后、skb 分配前)——一个是执行体系,一个是挂载位置,别混成一个概念。
tcp_write_xmit 里 cwnd/pacing 就是本关算出来的——BBR 的 sk_pacing_rate + fq qdisc 在这里门控发送。出题原则:10 题中记忆性题目不超过 3 道,其余为应用/推理/权衡类——场景预测、设计 trade-off 比较、从代码路径找问题、解释"为什么这样设计而不是那样"。难度递进,最后两道需要想一会儿。
BtlBw(瓶颈带宽)= 10 个 RTT 内测到的最大 delivery rate(windowed_max(delivered / elapsed, 10 round trips));RTprop(最小 RTT)= 10 秒内测到的最小 RTT(windowed_min(rtt, 10 seconds))。BDP(最优在途量)= BtlBw × RTprop。窗口设计有深意:带宽用短窗口(10 RTT)快速跟随变化,RTT 用长窗口(10s)取传播延迟的下确界——排队只会增大 RTT,不会减小它,所以最小值就是"管道本身的长度"。
STARTUP → DRAIN → PROBE_BW ⇄ PROBE_RTT。STARTUP 以约 2.89 的 gain(bbr_high_gain = 2/ln(2))指数探测,连续 3 个 round 带宽没有显著增长(阈值 1.25×,bbr_full_bw_thresh=5/4、bbr_full_bw_cnt=3)则判定管道已满、退出。PROBE_RTT 触发:流在整个 min_rtt 窗口内持续发送、且 10 秒没见到匹配或更小的 RTT 样本——动作是降到 4 个在途包(bbr_cwnd_min_target=4)、持续 200ms 重测传播延迟,然后回 PROBE_BW。降到 4 个包是为了把队列排空,测到的是"空管道"的 RTT。
CUBIC 按丢包行事:深缓冲迟迟不丢包,它就一直涨 cwnd——队列灌满,延迟随缓冲深度膨胀,灌满才丢包降速,然后重复。BBR 按模型行事:在途量 = BDP,多一个包都是排队——它主动把在途量钳在 BDP 附近,队列保持接近空,延迟就是 RTprop 本身(物理传播延迟)。深缓冲下 CUBIC 的延迟是"缓冲有多深、延迟有多高",BBR 的延迟是"物理传播延迟"。
CUBIC 会被饿死:1% 随机丢包让它频繁触发"丢包=拥塞",cwnd 反复乘 0.7,长期跑不满带宽——丢包不是拥塞,却被当成了拥塞。BBR 的核心算法不直接以丢包为降速判据,delivery rate 模型只看 ACK 的到达速率,所以不会像 CUBIC 那样被误伤;但"不受影响"是错的——丢包仍要重传(开销),注释还明确说观察到丢包时会调整下一次 ACK 的发送量。信号错了,算法越努力越错;信号对了,代价也只是按实际损失付。
省的是 per-packet 固定开销:skb 分配次数、协议栈逐层遍历次数、中断/轮询次数。"切包"本身(按 MSS 的数据切分)相对便宜,贵的是每个包独立走一遍的"过路费"——分配、遍历、上下文切换都和包大小无关。几十个 MSS 的数据一次遍历 vs 几十次遍历,省的是次数不是字节。先找到按次收费的环节,再谈优化。注意大切片不是固定 64K:GSO_LEGACY_MAX_SIZE=65536 只是典型上限,具体看设备能力(tso_max_size);重传仍按 MSS 粒度,故障域没有放大。
代价是延迟:GRO 把包暂存、尝试与后续同流包合并(tcp_gro_receive()),暂存等待就是人为延迟;且合并有严格相容条件(同流、序号连续、标志/选项一致),乱序流、带 PSH/FIN/RST 这类标志的包根本合并不了。决策看 workload 的账本:吞吐优先(文件传输、视频)GRO 是赚的;延迟敏感场景要实测权衡——没有"高频交易就该关"的通用答案,只有"你的 p99 和 CPU 账本"。批量化永远是吞吐和延迟的 trade-off,GRO 只是把它搬到了收包路径。
因为 BBR 的模型假设"发送速率 = pacing_rate"是均匀的——突发发送会在瓶颈处瞬间制造排队,ACK 到达速率被队列扭曲,BtlBw 估计失真,模型白建。更糟的是微突发会自我实现:突发造成排队 → RTT 样本变大 → 模型以为管道变长。pacing(sk_pacing_rate + fq qdisc)把突发摊平成均匀流,模型的前提是行为配合。
差在三个环节:(1) skb 分配——XDP 在 skb 分配之前动手,iptables/netfilter hook 触发时 skb 早已分配初始化;(2) 协议栈遍历——XDP_DROP 的包没进过 IP/TCP 层,iptables 的包已经走过路由、连接跟踪等处理;(3) hook 位置——XDP 在驱动收包路径(NAPI poll 内)直接处理,是内核里最早的可编程点。DDoS 场景下要扔的包占绝大多数,在协议栈里扔等于先付全款再退货——扔得越早,沉没成本越低。(不给"差几个数量级"的固定数字:差距随包大小、规则复杂度、网卡能力变,定量要实测。)
5/4 阶段故意超发 25%——探测"还有没有多余带宽"(如果有,delivery rate 会涨;源码注释:probe for more available bw);3/4 阶段主动降速——排空 5/4 阶段制造的队列,也给其他流让路(源码注释原话:drain queue and/or yield bw to other flows);1×6 阶段按估计带宽巡航。公平来自"超发-让出"的循环设计:每个流都周期性地超发探测、再主动让出,长期看各流估计的 BtlBw 趋向公平份额——但这是设计意图,不是数学保证,实际收敛还依赖瓶颈队列深度、RTT 差异等现实条件。公平不是算出来的,是探出来、让出来的。
数据路径:网卡 DMA → 驱动收包 → XDP 程序(BPF_PROG_TYPE_XDP)在 skb 分配前先看 → 查 BPF map(SYN 速率统计/黑名单)→ 超限则 XDP_DROP,不超则 XDP_PASS 进协议栈。不能在 TCP 层做:SYN flood 打的就是连接建立路径的资源——在 TCP 层处理,包已经走完驱动、skb 分配、IP/TCP 解析,攻击者要耗尽的资源已经被消耗了,等于"先付全款再退货"。BPF map 是分工的关键:XDP 程序本身无状态,靠 map 在包之间共享状态(计数、黑名单),map 经 bpf(2) 创建、用户态可查改——数据面线速执行,控制面动态下发,这正是 XDP+eBPF 的分工。(具体用哪些 map 类型、半连接队列的细节超出本关源码核实范围,不展开。)
bpf_prog_run_xdp,XDP 程序执行入口(JIT 分发)BBR 状态机(长连接绝大部分时间在 PROBE_BW 里打转,源码注释原话):
3 轮无显著带宽增长
┌─────────────────────────┐
▼ │
STARTUP ──→ DRAIN ──→ PROBE_BW ←──→ PROBE_RTT
(2.89x 指数 (排空启动期 (5/4 探带宽 (10s 无更小 RTT:
探测带宽) 堆的队列) 3/4 让带宽 降到 4 包/200ms
1x6 巡航) 重测传播延迟)
BDP 与队列的关系(BBR 模型的核心直觉):
在途量 ──→
| 空转区 | 最优点(BDP) | 排队区
| 发少了, | 在途 = BtlBw | 发多了,
| 带宽空转 | × RTprop | 全在排队
| | | 延迟爆炸、毫无收益
▲
BBR 把在途量钳在这里
CUBIC 的问题:靠丢包找"最优点",而丢包发生在排队区深处
发包路径上的门控(M13 的路,M14 的闸):
应用 write()
│
▼
tcp_sendmsg() ──进 sk_write_queue(M13)
│
▼ ┌─────────────────────────────────┐
tcp_write_xmit() │ 第一道闸:cwnd(CUBIC/BBR 算)│
│ │ 第二道闸:pacing_rate(BBR) │
▼ └─────────────────────────────────┘
dev_queue_xmit() ──qdisc(fq 按 pacing 整形)
│
▼
网卡 TSO 切片 ──大切片按 gso_size(MSS) 切(M14)
在页面里每题写下你的答案(关键词即可),卡住的题点一下"卡住了"标记,
点"生成作答汇总"后复制、贴到聊天里——对错由我来判定,我逐题批改并据此
调整下一关的深浅;准备好了说"下一关"。
点下面按钮生成汇总文本,复制后贴到「内核二十关」侧聊里,对错由我判定。