乐于分享
好东西不私藏

MPTCP包调度器源码解析:数据该走哪条路

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 的分工

维度
Path Manager
Scheduler
决策对象
子流本身(开哪些路)
数据包(数据走哪条路)
决策频率
秒级(子流创建/销毁)
微秒级(每次发送)
决策位置
控制路径(不在数据路径上)
数据路径(每个包都经过)
配置方式
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;}

算法步骤:

  1. 初始化:为 active 和 backup 子流分别维护最优选择
  2. 遍历子流:遍历所有活跃子流,跳过已关闭的子流
  3. 获取 pacing_rate:从 TCP 层读取 sk_pacing_rate(由拥塞控制算法计算)
  4. 计算 linger_time:使用公式 sk_wmem_queued << 32 / pace
  5. 记录最优:在 active 组中选择 linger_time 最小的子流
  6. 回退策略:如果没有 active 子流,使用 backup 子流
  7. 缓冲区检查:确保选中的子流有可用缓冲区(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. 第 1 个数据块(DSN 1-1000)走子流 A,100ms 后到达
  2. 第 2 个数据块(DSN 1001-2000)走子流 B,500ms 后到达
  3. 接收端收到 DSN 1-1000 后,必须等 DSN 1001-2000 到达才能交付应用
  4. 即使子流 A 发送的 DSN 2001-3000 在 200ms 就到了,也要排队等待
  5. 这就是 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(msksubflow_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 轮询调度

实验结果

调度器
总耗时
吞吐量
99th 延迟
HoL 阻塞次数
BLEST
18.2s
43.9Mbps
120ms
0
轮询
24.5s
32.6Mbps
380ms
127

分析:

  • 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 篇)

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » MPTCP包调度器源码解析:数据该走哪条路

猜你喜欢

  • 暂无文章