MPTCP 深度解析系列(5/20)本文基于 Linux 内核主线代码(net/mptcp/)编写
一个 5 行 Python 脚本:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_MPTCP)
sock.connect(("203.0.113.1", 8080))
sock.send(b"Hello MPTCP")
sock.close()
5 行代码,1 秒钟执行完。但在这 1 秒钟里,Linux 内核做了什么?
send() 这一行,在内核里走了一条漫长的路:从用户态陷入内核,进入 MPTCP 层,调度器决定数据走哪条子流,每条子流独立传输,接收端根据 DSN 重组数据,最终交付给对端应用。
这条路,涉及 10+ 个内核函数,操作多种数据结构,协调 N 条子流。但对应用程序来说,它只是一个普通的 send() 调用。
这就是 Linux 内核 MPTCP 的设计哲学:对应用透明,对 TCP 栈最小化侵入。
这篇文章,我们就跟着这个 send() 调用,从用户态走到网卡,完整看一遍 MPTCP 在 Linux 内核中是如何实现的。
一、v0 vs v1:一次彻底的重构
在讲代码路径之前,我们需要先理解一个关键背景:Linux 内核的 MPTCP 实现经历了一次彻底的重构。
如果你在 2020 年之前接触过 MPTCP,那你看到的是 v0(out-of-tree patch)。如果你现在用 Linux 5.6+ 内核,那你用的是 v1(主线内核)。两者的架构完全不同。
v0 的历史包袱
2013-2020 年,MPTCP 以 out-of-tree 补丁形式存在。什么意思?就是你要手动下载补丁,打到内核源码上,重新编译内核,才能用 MPTCP。
v0 的架构很"暴力":直接修改 struct tcp_sock,在 TCP 层"硬塞"多路径逻辑。
问题在哪?
- 代码侵入性强:修改了 TCP 核心数据结构,与主线内核耦合严重。
- 升级困难:每次 Linux 内核升级,都要重新适配补丁。
- 难以合入主线:Linus Torvalds 不接受这种侵入式修改。
v1 的核心设计:双层 socket 架构
v1 的架构可以用一句话概括:
用户态看到一个 MPTCP socket,内核里是 N 个 TCP socket。
应用程序
↓
mptcp_sock(逻辑连接)
↓
tcp_sock × N(物理子流)
↓
网卡
这个设计的精妙之处在于:MPTCP 层和 TCP 层完全解耦。 TCP 层不知道自己是 MPTCP 的一部分,它只是在正常工作。MPTCP 层在上面"编排"这些 TCP 连接。
v1 合入主线的里程碑
- 2020 年 3 月:Linux 5.6 合入 MPTCP v1
- 2020 年 7 月:Linux 5.8 支持多子流
- 2021 年:Linux 5.13 支持 BPF 调度器
实战命令:
# 查看内核是否支持 MPTCP
cat /proc/sys/net/mptcp/enabled
# 查看 MPTCP 源码目录
ls /usr/src/linux/net/mptcp/
二、数据结构:MPTCP 的"骨架"
理解架构的最好方法,是理解数据结构。让我们看看内核中真实的定义。
struct mptcp_sock:逻辑连接的"大脑"
源码位置:net/mptcp/protocol.h 第 273 行
structmptcp_sock {
/* inet_connection_sock must be the first member */
structinet_connection_socksk;
u64 local_key; // 本地密钥(MP_CAPABLE 握手时生成)
u64 remote_key; // 对端密钥
u64 write_seq; // 发送端 DSN(Data Sequence Number)
u64 snd_una; // 已确认的 DSN
u64 ack_seq; // 接收端确认序列号
atomic64_t rcv_wnd_sent; // 接收窗口
u32 token; // 连接标识符
unsignedlong flags; // 状态标志
structsock *first;// 第一条子流(主子流)
structlist_headconn_list;// 子流列表
structmptcp_pm_datapm;// Path Manager 状态
structmptcp_sched_datasched;// 调度器数据
bool fully_established; // 连接是否完全建立
bool can_ack; // 是否可以发送 ACK
// ... 更多字段
};
关键字段:
- **
write_seq**:全局 DSN,保证字节流有序。每发送一个字节,write_seq++。 - **
first**:指向第一条子流(主子流),这是一个 struct sock *,实际指向底层的 TCP socket。
struct mptcp_subflow_context:子流的"身份证"
源码位置:net/mptcp/protocol.h 第 467 行
structmptcp_subflow_context {
structlist_headnode;// 链表节点
structsock *tcp_sock;// 指向底层 TCP socket
structsock *conn;// 指向 MPTCP socket
u64 map_seq; // DSN 映射起点
u32 map_subflow_seq; // 子流 SSN
u16 map_data_len; // 映射数据长度
u8 local_id; // 本地 Address ID
u8 remote_id; // 对端 Address ID
u32 token; // 连接 Token
u64 thmac; // Truncated HMAC
u8 request_join:1, // 是否正在 JOIN
request_bkup:1, // 是否为备份子流
fully_established:1,
// ... 更多标志位
};
关键字段:
map_seq 和 map_subflow_seq:DSN ↔ SSN 的映射关系,存储在 DSS Option 中。local_id / remote_id:逻辑接口标识(对应 ADD_ADDR 中的 Address ID)。
双层编号:DSN 和 SSN
MPTCP 最核心的设计之一,就是双层编号:
- DSN(Data Sequence Number):连接级序列号,64 位,全局唯一。
- SSN(Subflow Sequence Number):子流级序列号,32 位,每条子流独立(复用 TCP 的 seq)。
类比:DSN 是"订单号",SSN 是"快递单号"。
发送端:
- 应用发送 1000 字节数据,MPTCP 层用 DSN 给这 1000 字节编号。
- 调度器决定:前 500 字节走子流 A,后 500 字节走子流 B。
- 每个数据包携带 DSS Option,告诉接收端:"这个包的 SSN 对应 DSN 的哪个范围"。
接收端:
- 从不同子流收到数据包,从 DSS Option 得知 SSN → DSN 的映射。
实战命令:
# 查看 MPTCP 连接的子流信息
ss -tin | grep -A 5 mptcp
# 输出示例:
# ESTAB 0 0 10.0.0.2:54321 203.0.113.1:8080
# mptcp subflows:2 add_addr_signal:0 ...
三、发送路径:send() 的旅程
现在,我们跟着 sock.send(b"Hello MPTCP") 这个调用,从用户态走到网卡。
源码位置:net/mptcp/protocol.c 第 1743 行
用户态 → 内核态:系统调用入口
// 用户态
send(sock_fd, "Hello MPTCP", 11, 0);
// 内核态入口(简化)
SYSCALL_DEFINE4(sendto, ...)
→ sock_sendmsg()
→ sock->ops->sendmsg() // 对于 MPTCP socket,这里是 mptcp_sendmsg
MPTCP 层:mptcp_sendmsg()
真实源码(简化版):
staticintmptcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
structmptcp_sock *msk = mptcp_sk(sk);
size_t copied = 0;
lock_sock(sk);
// 1. 检查连接状态
if ((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) {
// 等待连接建立
ret = sk_stream_wait_connect(sk, &timeo);
}
// 2. 将数据从用户态拷贝到内核缓冲区
while (msg_data_left(msg)) {
// ... 拷贝数据到 MPTCP 发送队列
copied += copy;
}
// 3. 调用 __mptcp_push_pending() 触发实际发送
__mptcp_push_pending(sk);
release_sock(sk);
return copied;
}
关键函数:
- 数据首先进入 MPTCP 层的发送队列,此时还没有分配到具体子流。
__mptcp_push_pending() 负责调用调度器,把数据分配到子流。
调度器决策:数据该走哪条子流?
源码位置:net/mptcp/sched.c 第 19 行
staticintmptcp_sched_default_get_send(struct mptcp_sock *msk)
{
structsock *ssk;
// 调用 mptcp_subflow_get_send() 选择最佳子流
ssk = mptcp_subflow_get_send(msk);
if (!ssk)
return -EINVAL;
// 标记该子流为已调度
mptcp_subflow_set_scheduled(mptcp_subflow_ctx(ssk), true);
return0;
}
默认调度器的逻辑:
(调度器的详细逻辑,我们在第 7 篇会深入讲解。)
子流层:复用 TCP 代码
数据进入子流后,后续流程与普通 TCP 完全相同:
tcp_sendmsg()
→ tcp_push()
→ tcp_write_xmit()
→ ip_queue_xmit() // IP 层
→ dev_queue_xmit() // 网卡驱动
关键点:
- MPTCP 只负责连接级的可靠性(DSN 确认),子流级的可靠性由 TCP 保证。
数据包结构:DSS Option
发送出去的 TCP 包,Options 字段中包含 DSS(Data Sequence Signal):
源码位置:net/mptcp/options.c
TCP Options:
Kind=30, SubType=2 (DSS)
Data Sequence Number (DSN): 0x64
Subflow Sequence Number (SSN): 0x1
Data Length: 11
Checksum: 0xabcd (可选)
四、接收路径:数据如何重组
接收端从不同子流收到数据包,需要根据 DSN 重新排序。
网卡 → IP → TCP:子流独立接收
每条子流独立接收数据,与普通 TCP 完全相同:
tcp_v4_rcv()
→ tcp_rcv_established()
→ tcp_data_queue() // 数据进入 TCP 接收队列
MPTCP 层:解析 DSS Option
源码位置:net/mptcp/options.c
TCP 层调用 MPTCP 钩子,解析 DSS Option:
voidmptcp_incoming_options(struct sock *sk, struct sk_buff *skb)
{
// 从 TCP Options 中提取 DSN、SSN、Data Length
u64 dsn = mp_opt->data_seq;
u32 ssn = mp_opt->subflow_seq;
// 将数据插入 MPTCP 接收队列(按 DSN 排序)
// 实际实现更复杂,涉及乱序处理、重复检测等
}
DSN 排序与重组
关键点:
- 即使子流 A 的数据先到,但如果 DSN 不连续,也要等子流 B 的数据到达。
- 这会引入 HoL 阻塞(Head-of-Line Blocking):一条慢速子流会拖慢整个连接的吞吐量。
(HoL 阻塞是 MPTCP 性能的核心问题,我们在第 12 篇会详细讨论。)
五、源码目录导航
如果你想深入阅读源码,这里是一个"地图"。
Linux 内核源码目录:net/mptcp/
net/mptcp/
├── protocol.c # 核心协议逻辑(mptcp_sendmsg、mptcp_recvmsg)
├── protocol.h # 数据结构定义(mptcp_sock、mptcp_subflow_context)
├── subflow.c # 子流管理(MP_JOIN、ADD_ADDR 处理)
├── pm.c # Path Manager 核心逻辑
├── pm_kernel.c # 内核态 Path Manager 实现
├── pm_netlink.c # Netlink 接口(ip mptcp 命令后端)
├── pm_userspace.c # 用户态 Path Manager 支持
├── sched.c # 调度器框架
├── options.c # TCP Options 解析(MP_CAPABLE、DSS 等)
├── token.c # Token 管理(连接标识)
├── crypto.c # HMAC-SHA256 实现
├── sockopt.c # Socket 选项处理
└── bpf.c # BPF 扩展接口(6.x 新增)
关键文件说明
protocol.c(4171 行):最大的文件,包含发送/接收路径的主入口。subflow.c(2190 行):子流生命周期管理,MP_JOIN 握手逻辑。pm_kernel.c(1411 行):内核态 Path Manager 的默认实现。sched.c(215 行):调度器框架,比较精简。
如何阅读源码
- 从
mptcp_sendmsg() 开始,跟着调用链走。 - 参考
Documentation/networking/mptcp-sysctl.rst。
实战命令:
# 进入内核源码目录
cd /usr/src/linux/net/mptcp
# 搜索函数定义
grep -rn "^static int mptcp_sendmsg" .
# 查看数据结构定义
grep -A 50 "^struct mptcp_sock" protocol.h
六、总结:透明与解耦
回到开头那 5 行 Python 代码。sock.send(b"Hello MPTCP") 这一行,在内核里走了一条漫长的路:
- 从
mptcp_sendmsg() 进入 MPTCP 层
这就是 Linux 内核 MPTCP 的设计哲学:对应用透明,对 TCP 栈最小化侵入。
三个要点:
- 双层 socket 架构:用户态看到 1 个 MPTCP socket,内核里是 N 个 TCP socket。
- 双层编号系统:DSN 保证连接级有序,SSN 保证子流级有序。
- 完全解耦:TCP 层不知道 MPTCP 的存在,MPTCP 层在上面"编排"TCP 连接。
下一篇预告:《MPTCP 路径管理器(Path Manager)源码解析:子流何时创建、何时销毁》
关注本系列,我们将用 20 篇文章,把 MPTCP 从协议到内核、从实战到前沿,讲透彻。
本文基于Linux6.x内核主线代码编写源码位置:net/mptcp/主要参考文件:protocol.c、protocol.h、sched.c、options.c基于真实内核源码(简化以便理解)