MPTCP包调度器源码解析:数据该走哪条路
MPTCP 深度解析系列(7/20)本文基于 Linux 内核主线代码(net/mptcp/protocol.c, net/mptcp/sched.c)编写
一个数据包站在”十字路口”。
前方有两条路:4G 子流,带宽 10Mbps,当前排队 100KB;5G 子流,带宽 50Mbps,当前排队 500KB。
数据包问:”我该走哪条路?”
直觉告诉你:走 5G,因为带宽更大。但直觉错了——如果走 5G,这个数据包要排在 500KB 后面,等 5G 把前面的数据发完才轮到它。而如果走 4G,虽然带宽小,但排队少,反而更快到达。
谁来做这个选择?
不是应用程序——它只管调用 send(),根本不知道有几条子流。不是 TCP 层——每条 TCP 子流只管自己的传输,不知道其他子流的状态。
答案是 **Packet Scheduler(包调度器)**——MPTCP 的”导航系统”,每次发送数据时,都要计算一遍所有子流的状态,选出最优路径。
这篇文章,我们就跟着数据包走,从应用层的 send() 调用,到调度器的 linger_time 计算,再到 TCP 层的实际发送,完整看一遍调度器的工作流程。每个决策点,我们都会深入内核源码,看看那 10 微秒里到底发生了什么。
一、调度器的职责:数据包的”导航系统”
在深入源码之前,我们需要先理解 Scheduler 在 MPTCP 架构中的位置和职责。
Scheduler 在架构中的位置
应用层 [App: send()] |MPTCP 层 [mptcp_sendmsg] | [Scheduler] ← 为每个数据块选择子流 / \ 子流1 子流2 | |TCP 层 [tcp_sendmsg]
Scheduler 在数据路径上,每次发送数据都要调用。它的决策频率远高于 Path Manager——PM 决策频率是秒级(子流创建/销毁),Scheduler 决策频率是微秒级(每个数据块)。
打个比方:如果把 MPTCP 比作一个物流系统,那么:
-
Path Manager = 城市规划局,决定修哪些高速公路(秒级决策) -
Scheduler = 导航软件,每次出发前都要计算最优路线(微秒级决策) -
拥塞控制 = 路况监测,实时调整每条路的限速(毫秒级决策)
Scheduler 的核心职责
职责 1:发送路径选择
应用调用 send() 发送 1MB 数据,Scheduler 需要决定:
-
这 1MB 数据全走子流 1? -
还是一半走子流 1,一半走子流 2? -
还是根据子流状态动态分配?
职责 2:重传路径选择
TCP 子流丢包需要重传时,Scheduler 需要决定:
-
重传数据走原来的子流? -
还是换一条子流(可能原路径拥塞了)?
职责 3:避免 HoL 阻塞
HoL(Head-of-Line)阻塞是多路径传输的经典问题:如果一个数据包走了慢路径,即使快路径空闲,接收端也要等慢路径的数据到达才能按序交付应用。
Scheduler 的核心任务就是选择”最早能发完”的子流,而不是”带宽最大”的子流,从而避免 HoL 阻塞。
Scheduler 与 Path Manager 的分工
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ip mptcp endpoint |
/proc/sys/net/mptcp/scheduler |
实战命令:
# 查看当前调度器cat /proc/sys/net/mptcp/scheduler# 输出:default# 查看可用调度器(内核 6.x+)cat /proc/sys/net/mptcp/available_schedulers# 输出:default
二、调度器框架:可插拔的设计
Linux 内核的 Scheduler 是可插拔的,类似 TCP 拥塞控制算法。理解框架,才能理解具体算法。
struct mptcp_sched_ops:调度器接口
源码位置:net/mptcp/protocol.h 第 642-649 行
structmptcp_sched_ops {int (*get_subflow)(struct mptcp_sock *msk, struct mptcp_sched_data *data);char name[MPTCP_SCHED_NAME_MAX];structmodule *owner;structlist_headlist;} ____cacheline_aligned_in_smp;
接口解析:
-
get_subflow:为新数据选择子流,这是 Scheduler 的核心函数 -
参数 msk:MPTCP socket,包含所有子流的状态 -
参数 data:调度器可以维护的私有数据(如轮询计数器) -
返回值:选中的子流(通过设置 subflow->scheduled = true) -
name:调度器名称,用户通过 sysctl 切换调度器时用这个名字
默认调度器:mptcp_sched_default
源码位置:net/mptcp/sched.c 第 16-49 行
staticintmptcp_sched_default_get_subflow(struct mptcp_sock *msk, struct mptcp_sched_data *data){structsock *ssk; ssk = mptcp_subflow_get_send(msk); // 调用 BLEST 算法if (!ssk)return -EINVAL; mptcp_subflow_set_scheduled(mptcp_subflow_ctx(ssk), true);return0;}staticstructmptcp_sched_opsmptcp_sched_default __read_mostly = { .get_subflow = mptcp_sched_default_get_subflow, .name = "default", .owner = THIS_MODULE,};
关键点:
-
默认调度器调用 mptcp_subflow_get_send(),这是 BLEST 算法的实现 -
mptcp_subflow_set_scheduled()标记选中的子流,后续数据会发送到这个子流
调度器注册机制
源码位置:net/mptcp/sched.c 第 51-74 行
voidmptcp_register_scheduler(struct mptcp_sched_ops *sched){if (WARN_ON_ONCE(!sched->get_subflow))return; spin_lock(&mptcp_sched_list_lock); list_add_tail_rcu(&sched->list, &mptcp_sched_list); spin_unlock(&mptcp_sched_list_lock);}
可扩展性:
-
内核模块可以通过 mptcp_register_scheduler()注册自定义调度器 -
内核 6.x+ 支持 BPF 调度器,用户可以用 BPF 程序实现自定义策略 -
但目前主线内核只有 default调度器
实战命令:
# 查看可用调度器cat /proc/sys/net/mptcp/available_schedulers# 切换调度器(如果有多个)echo"default" > /proc/sys/net/mptcp/scheduler
三、BLEST 算法详解:linger_time 的魔法
现在进入全文的核心部分——BLEST 算法。这是 Linux 默认调度器的选路逻辑,也是避免 HoL 阻塞的关键。
BLEST 算法的核心思想
BLEST = Buffering Linger Estimation
核心思想:选择”最早可以发送完”的子流,而不是”带宽最大”的子流。
为什么?
假设有两条子流:
-
子流 A:带宽 10Mbps,当前排队 100KB -
子流 B:带宽 50Mbps,当前排队 500KB
直觉告诉你选子流 B(带宽大),但计算一下”发送完排队数据需要多久”:
-
子流 A:100KB / 10Mbps = 0.08 秒 -
子流 B:500KB / 50Mbps = 0.08 秒
两者耗时相同!如果子流 B 排队更多(如 600KB),则 600KB / 50Mbps = 0.096 秒,反而比子流 A 慢。
这个”发送完排队数据需要的时间”就是 linger_time。
linger_time 计算公式
源码位置:net/mptcp/protocol.c 第 1389 行
linger_time = div_u64((u64)READ_ONCE(ssk->sk_wmem_queued) << 32, pace);
公式解析:
-
sk_wmem_queued:TCP 发送缓冲区中排队的字节数(单位:字节) -
pace:子流的 pacing_rate,即发送速率(单位:字节/秒) -
linger_time:排队时间(单位: 秒,高精度时间)
为什么左移 32 位?
-
为了提高精度,避免整数除法丢失小数部分 -
结果是定点数,单位是 秒(约 0.23 纳秒) -
比较 linger_time 时不需要转换回浮点数,直接比较整数即可
通俗理解:
假设子流 A:
-
排队 100KB = 102400 字节 -
pacing_rate = 10Mbps = 1250000 字节/秒 -
linger_time = (102400 << 32) / 1250000 ≈ 3.52 × 10^11
假设子流 B:
-
排队 500KB = 512000 字节 -
pacing_rate = 50Mbps = 6250000 字节/秒 -
linger_time = (512000 << 32) / 6250000 ≈ 3.52 × 10^11
两者 linger_time 相同,随机选一个。
完整的 BLEST 算法实现
源码位置:net/mptcp/protocol.c 第 1353-1427 行
struct sock *mptcp_subflow_get_send(struct mptcp_sock *msk){structsubflow_send_infosend_info[SSK_MODE_MAX];structmptcp_subflow_context *subflow;structsock *sk = (structsock *)msk; u32 pace, burst, wmem;int i, nr_active = 0;structsock *ssk; u64 linger_time;long tout = 0;// 初始化:为 active 和 backup 子流分别记录最优选择for (i = 0; i < SSK_MODE_MAX; ++i) { send_info[i].ssk = NULL; send_info[i].linger_time = -1; // 初始化为最大值 }// 遍历所有子流 mptcp_for_each_subflow(msk, subflow) {bool backup = subflow->backup || subflow->request_bkup; ssk = mptcp_subflow_tcp_sock(subflow);if (!mptcp_subflow_active(subflow)) // 子流必须活跃continue; nr_active += !backup; pace = subflow->avg_pacing_rate;if (unlikely(!pace)) {// 首次使用时,初始化 pacing_rate subflow->avg_pacing_rate = READ_ONCE(ssk->sk_pacing_rate); pace = subflow->avg_pacing_rate;if (!pace)continue; }// 核心:计算 linger_time linger_time = div_u64((u64)READ_ONCE(ssk->sk_wmem_queued) << 32, pace);// 记录 linger_time 最小的子流(分 active 和 backup 两组)if (linger_time < send_info[backup].linger_time) { send_info[backup].ssk = ssk; send_info[backup].linger_time = linger_time; } }// 优先使用 active 子流,如果没有则使用 backupif (!nr_active) send_info[SSK_MODE_ACTIVE].ssk = send_info[SSK_MODE_BACKUP].ssk; ssk = send_info[SSK_MODE_ACTIVE].ssk;// 子流必须有可用缓冲区if (!ssk || !sk_stream_memory_free(ssk))returnNULL;return ssk;}
算法步骤:
-
初始化:为 active 和 backup 子流分别维护最优选择 -
遍历子流:遍历所有活跃子流,跳过已关闭的子流 -
获取 pacing_rate:从 TCP 层读取 sk_pacing_rate(由拥塞控制算法计算) -
计算 linger_time:使用公式 sk_wmem_queued << 32 / pace -
记录最优:在 active 组中选择 linger_time 最小的子流 -
回退策略:如果没有 active 子流,使用 backup 子流 -
缓冲区检查:确保选中的子流有可用缓冲区( sk_stream_memory_free)
HoL 阻塞的避免机制
源码注释:net/mptcp/protocol.c 第 1401-1410 行
/* According to the blest algorithm, to avoid HoL blocking for the * faster flow, we need to: * - estimate the faster flow linger time * - use the above to estimate the amount of byte transferred * by the faster flow * - check that the amount of queued data is greater than the above, * otherwise do not use the picked, slower, subflow * We select the subflow with the shorter estimated time to flush * the queued mem, which basically ensure the above. We just need * to check that subflow has a non empty cwin. */
HoL 阻塞场景:
假设有两条子流:
-
子流 A(快):带宽 50Mbps,RTT 20ms -
子流 B(慢):带宽 10Mbps,RTT 100ms
如果 Scheduler 使用轮询策略:
-
第 1 个数据块(DSN 1-1000)走子流 A,100ms 后到达 -
第 2 个数据块(DSN 1001-2000)走子流 B,500ms 后到达 -
接收端收到 DSN 1-1000 后,必须等 DSN 1001-2000 到达才能交付应用 -
即使子流 A 发送的 DSN 2001-3000 在 200ms 就到了,也要排队等待 -
这就是 HoL 阻塞:快路径被慢路径阻塞
BLEST 如何避免:
BLEST 选择 linger_time 最小的子流,确保:
-
如果子流 A 当前排队少,优先选择子流 A(快路径) -
如果子流 A 排队多,可能选择子流 B(避免在快路径上堆积更多数据) -
关键是”尽快发出”,而不是”理论最快”
pacing_rate 的来源
源码位置:net/mptcp/protocol.c 第 1383 行
subflow->avg_pacing_rate = READ_ONCE(ssk->sk_pacing_rate);
sk_pacing_rate 由 TCP 拥塞控制算法计算,考虑了:
-
拥塞窗口(cwnd) -
RTT(往返时延) -
丢包率
源码位置:net/ipv4/tcp_input.c 第 944 行(BBR 算法)
staticvoidtcp_update_pacing_rate(struct sock *sk){conststructtcp_sock *tp = tcp_sk(sk); u64 rate; rate = (u64)tp->mss_cache * ((USEC_PER_SEC / 100) * (100 - 1)); rate *= max(tp->snd_cwnd, tp->packets_out);if (likely(tp->srtt_us)) do_div(rate, tp->srtt_us); WRITE_ONCE(sk->sk_pacing_rate, min_t(u64, rate, sk->sk_max_pacing_rate));}
BLEST 不重复计算拥塞控制,而是直接使用 TCP 层的 sk_pacing_rate,实现了分层解耦。
实战命令:
# 查看子流的 pacing_rate 和排队情况ss -tin sport = :8080 | grep -E "pacing_rate|Send-Q"# 输出示例:# Send-Q: 16384# pacing_rate 12.5Mbps# 手动计算 linger_time# linger_time ≈ Send-Q / pacing_rate# = 16384 bytes / (12.5Mbps / 8)# = 16384 / 1562500 bytes/s# ≈ 0.0105 秒# 查看 MPTCP 连接的所有子流ss -tin | grep -A 20 "mptcp"
四、重传路径选择:与发送路径的区别
BLEST 算法处理的是新数据的发送路径。但当 TCP 子流丢包需要重传时,路径选择策略可以不同。
重传路径选择:mptcp_subflow_get_retrans()
源码位置:net/mptcp/protocol.c 第 2264-2298 行
struct sock *mptcp_subflow_get_retrans(struct mptcp_sock *msk){structmptcp_subflow_context *subflow;structsock *backup = NULL;// 优先选择非 backup 子流 mptcp_for_each_subflow(msk, subflow) {structsock *ssk = mptcp_subflow_tcp_sock(subflow);if (!mptcp_subflow_active(subflow))continue;if (subflow->backup || subflow->request_bkup) {if (!backup) backup = ssk;continue; }return ssk; // 返回第一个非 backup 子流 }return backup; // 如果没有非 backup 子流,返回 backup}
重传策略解析:
-
不计算 linger_time:直接返回第一个可用的非 backup 子流 -
优先非 backup:backup 子流只在没有其他选择时使用 -
简化逻辑:重传数据通常较少,不需要复杂的调度逻辑
为什么重传路径可以不同?
理由 1:DSN 已分配,不会引发 HoL 阻塞
重传数据的 DSN(Data Sequence Number)已经分配过了。接收端按 DSN 排序交付应用,重传数据的 DSN 通常比正在发送的新数据更小,不会阻塞新数据的交付。
理由 2:选择不同路径可能避开拥塞
丢包通常意味着原路径拥塞。选择不同的子流重传,可能绕开拥塞路径,提高重传成功率。
理由 3:简化实现,减少开销
重传频率远低于发送频率,不需要每次都计算 linger_time。简化逻辑可以减少 CPU 开销。
实战命令:
# 查看重传统计nstat -az | grep -i retrans# 关注:# TcpRetransSegs ← TCP 层重传次数# MPTCPRetrans ← MPTCP 层重传次数(如果有)# 查看子流丢包情况ss -tin sport = :8080 | grep -E "retrans|lost"# 输出示例:# retrans:0/5 lost:0
五、其他调度策略简介
BLEST 是 Linux 主线内核的默认(也是唯一)调度器。但在研究和实践中,还有其他调度策略值得了解。
轮询调度(Round-Robin)
原理:轮流选择子流,维护一个计数器,每次调度后计数器加 1。
伪代码:
staticint subflow_index = 0;struct sock *round_robin_scheduler(struct mptcp_sock *msk){structsock *ssk = get_nth_subflow(msk, subflow_index); subflow_index = (subflow_index + 1) % num_subflows;return ssk;}
优点:
-
实现简单 -
公平性好,每条子流获得相同的发送机会
缺点:
-
不考虑子流状态(带宽、RTT、排队) -
容易导致 HoL 阻塞 -
子流带宽差异大时性能差
适用场景:子流带宽相近、RTT 相近的场景(如数据中心内部多路径)。
冗余调度(Redundant)
原理:同一份数据在多条子流上同时发送,接收端收到第一份就确认,丢弃后续重复数据。
源码位置:内核主线不支持,但 RFC 8684 定义了 MP_REDUNDANT 选项
优点:
-
大幅降低延迟:不需要等待重传,接收端总是收到最快路径的数据 -
提高可靠性:即使一条子流完全丢包,其他子流仍能送达
缺点:
-
浪费带宽:同一份数据发送 N 次(N = 子流数量) -
增加网络负载:不适合带宽受限的场景
适用场景:
-
低延迟要求极高:如云控远程驾驶、工业控制 -
可靠性要求高:如金融交易、紧急通信 -
带宽充裕:如企业专线、5G eMBB
实战案例:
某车企的云控系统,T-Box 同时使用 4G 和 5G 上传摄像头视频流:
-
正常模式:BLEST 调度,带宽聚合约 60Mbps -
紧急模式:冗余调度,延迟降低 40%,但带宽翻倍
最低 RTT 优先(Lowest-RTT-First)
原理:选择 RTT 最小的子流,延迟敏感型应用的选择。
伪代码:
struct sock *lowest_rtt_scheduler(struct mptcp_sock *msk){structsock *best_ssk = NULL; u32 min_rtt = UINT_MAX; mptcp_for_each_subflow(msk, subflow) { u32 rtt = tcp_sk(subflow->tcp_sock)->srtt_us >> 3;if (rtt < min_rtt) { min_rtt = rtt; best_ssk = subflow->tcp_sock; } }return best_ssk;}
优点:
-
降低端到端延迟 -
适合交互式应用(SSH、VoIP)
缺点:
-
不考虑带宽,低 RTT 子流可能被打满 -
不考虑排队,可能导致 HoL 阻塞
对比 BLEST:BLEST 同时考虑排队和 pacing_rate,综合优于单纯的 Lowest-RTT-First。
BPF 调度器(内核 6.x+)
原理:用户编写 BPF 程序,在内核中动态加载,实现自定义调度策略。
示例场景:
-
根据子流的信号强度(RSSI)动态调整优先级 -
根据网络成本(WiFi 免费,5G 按流量计费)选择路径 -
根据应用类型(视频流 vs 文件下载)使用不同策略
实战命令(需要内核 6.x+):
# 加载 BPF 调度器(示例)bpftool prog load my_scheduler.o /sys/fs/bpf/mptcp_sched# 切换到 BPF 调度器echo"bpf" > /proc/sys/net/mptcp/scheduler
六、性能对比:BLEST vs 轮询
理论分析之外,我们需要实验数据来验证 BLEST 的优势。
实验场景
-
拓扑:客户端 ↔ 两条路径 ↔ 服务器 -
路径 1:带宽 10Mbps,RTT 20ms -
路径 2:带宽 50Mbps,RTT 100ms -
流量:客户端持续发送 100MB 数据 -
对比:BLEST 调度 vs 轮询调度
实验结果
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
分析:
-
BLEST 吞吐量高 35%:因为避免了 HoL 阻塞,快路径不被慢路径拖累 -
BLEST 延迟低 68%:数据包总是走”最早能发完”的路径,不会在慢路径上堆积 -
轮询产生大量 HoL 阻塞:慢路径的数据阻塞快路径的交付
真实场景:T-Box 移动切换
某车企的 T-Box,在高速公路上从 4G 切换到 5G:
-
4G:带宽 15Mbps,RTT 60ms -
5G:带宽 80Mbps,RTT 30ms
BLEST 表现:
-
5G 子流建立后,linger_time 迅速降低(排队少 + pacing_rate 高) -
Scheduler 逐步将流量切到 5G,4G 流量自然降低 -
平滑切换,无卡顿
轮询表现:
-
5G 子流建立后,仍然按 50:50 分配流量 -
4G 子流排队增加,导致 HoL 阻塞 -
延迟波动大,有明显卡顿
七、总结与展望
回到开头那个十字路口。数据包站在那里,BLEST 算法在 10 微秒内完成了计算:
-
4G 子流:linger_time = 100KB / 10Mbps = 0.08 秒 -
5G 子流:linger_time = 500KB / 50Mbps = 0.08 秒
两者相同,随机选一个。但如果 5G 子流排队更少(如 200KB),linger_time = 0.032 秒,BLEST 会选择 5G。
这就是 BLEST 的智慧:不看带宽,看”最早发完”。
这篇文章我们深入 Linux 内核源码,拆解了 MPTCP 包调度器的核心逻辑:
-
Scheduler 是数据包的”导航系统”,每次发送都要计算最优路径 -
BLEST 算法通过 linger_time = 排队长度 / pacing_rate 选择最优子流 -
重传路径可以不同,简化逻辑减少开销 -
BLEST 避免 HoL 阻塞,性能优于轮询调度
但 Scheduler 只解决了”数据走哪条路”,没有解决”每条路能走多快”。后者由拥塞控制算法决定——每条子流的 cwnd、pacing_rate 如何调整?多条子流如何协同避免过度竞争?
关于本系列
这是《MPTCP 深度解析》系列的第 7 篇,专注于 Linux 内核实现、车联网应用、云控低延迟场景。如果你在做 T-Box、网关设备、边缘计算,这个系列会持续更新实战经验和源码分析。
下一篇:MPTCP 拥塞控制算法全家桶(第 8 篇)
夜雨聆风