Linux内核MPTCP架构总览:跟着一个send()走进内核

Linux内核MPTCP架构总览:跟着一个send()走进内核 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(bHello 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那你看到的是 v0out-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 v12020 年 7 月Linux 5.8 支持多子流2021 年Linux 5.13 支持 BPF 调度器2023 年Linux 6.x 持续完善实战命令# 查看内核是否支持 MPTCP cat /proc/sys/net/mptcp/enabled # 查看 MPTCP 源码目录 ls /usr/src/linux/net/mptcp/二、数据结构MPTCP 的骨架理解架构的最好方法是理解数据结构。让我们看看内核中真实的定义。struct mptcp_sock逻辑连接的大脑源码位置net/mptcp/protocol.h第 273 行struct mptcp_sock { /* inet_connection_sock must be the first member */ struct inet_connection_sock sk; u64 local_key; // 本地密钥MP_CAPABLE 握手时生成 u64 remote_key; // 对端密钥 u64 write_seq; // 发送端 DSNData Sequence Number u64 snd_una; // 已确认的 DSN u64 ack_seq; // 接收端确认序列号 atomic64_t rcv_wnd_sent; // 接收窗口 u32 token; // 连接标识符 unsigned long flags; // 状态标志 struct sock *first; // 第一条子流主子流 struct list_head conn_list; // 子流列表 struct mptcp_pm_data pm; // Path Manager 状态 struct mptcp_sched_data sched; // 调度器数据 bool fully_established; // 连接是否完全建立 bool can_ack; // 是否可以发送 ACK // ... 更多字段 };关键字段**write_seq**全局 DSN保证字节流有序。每发送一个字节write_seq。**first**指向第一条子流主子流这是一个struct sock *实际指向底层的 TCP socket。**conn_list**子流链表。struct mptcp_subflow_context子流的身份证源码位置net/mptcp/protocol.h第 467 行struct mptcp_subflow_context { struct list_head node; // 链表节点 struct sock *tcp_sock; // 指向底层 TCP socket struct sock *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 和 SSNMPTCP 最核心的设计之一就是双层编号DSNData Sequence Number连接级序列号64 位全局唯一。SSNSubflow Sequence Number子流级序列号32 位每条子流独立复用 TCP 的 seq。类比DSN 是订单号SSN 是快递单号。发送端应用发送 1000 字节数据MPTCP 层用 DSN 给这 1000 字节编号。调度器决定前 500 字节走子流 A后 500 字节走子流 B。每条子流用自己的 SSN 传输。每个数据包携带 DSS Option告诉接收端这个包的 SSN 对应 DSN 的哪个范围。接收端从不同子流收到数据包从 DSS Option 得知 SSN → DSN 的映射。按 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(bHello MPTCP)这个调用从用户态走到网卡。源码位置net/mptcp/protocol.c第 1743 行用户态 → 内核态系统调用入口// 用户态 send(sock_fd, Hello MPTCP, 11, 0); // 内核态入口简化 SYSCALL_DEFINE4(sendto, ...) → sock_sendmsg() → sock-ops-sendmsg() // 对于 MPTCP socket这里是 mptcp_sendmsgMPTCP 层mptcp_sendmsg()真实源码简化版static int mptcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len) { struct mptcp_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 行static int mptcp_sched_default_get_send(struct mptcp_sock *msk) { struct sock *ssk; // 调用 mptcp_subflow_get_send() 选择最佳子流 ssk mptcp_subflow_get_send(msk); if (!ssk) return -EINVAL; // 标记该子流为已调度 mptcp_subflow_set_scheduled(mptcp_subflow_ctx(ssk), true); return 0; }默认调度器的逻辑选择拥塞窗口最大、RTT 最小的子流。考虑子流的可用性是否已建立、是否拥塞。调度器的详细逻辑我们在第 7 篇会深入讲解。子流层复用 TCP 代码数据进入子流后后续流程与普通 TCP 完全相同tcp_sendmsg() → tcp_push() → tcp_write_xmit() → ip_queue_xmit() // IP 层 → dev_queue_xmit() // 网卡驱动关键点每条子流独立做拥塞控制、重传、ACK 处理。MPTCP 只负责连接级的可靠性DSN 确认子流级的可靠性由 TCP 保证。数据包结构DSS Option发送出去的 TCP 包Options 字段中包含 DSSData Sequence Signal源码位置net/mptcp/options.cTCP Options: Kind30, SubType2 (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.cTCP 层调用 MPTCP 钩子解析 DSS Optionvoid mptcp_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()开始跟着调用链走。用grep或cscope搜索函数定义。参考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(bHello MPTCP)这一行在内核里走了一条漫长的路从mptcp_sendmsg()进入 MPTCP 层调度器选择子流每条子流独立传输携带 DSS Option接收端根据 DSN 重组数据最终交付给应用程序这就是 Linux 内核 MPTCP 的设计哲学对应用透明对 TCP 栈最小化侵入。三个要点双层 socket 架构用户态看到 1 个 MPTCP socket内核里是 N 个 TCP socket。双层编号系统DSN 保证连接级有序SSN 保证子流级有序。完全解耦TCP 层不知道 MPTCP 的存在MPTCP 层在上面编排TCP 连接。下一篇预告《MPTCP 路径管理器Path Manager源码解析子流何时创建、何时销毁》关注本系列我们将用 20 篇文章把 MPTCP 从协议到内核、从实战到前沿讲透彻。本文基于 Linux 内核主线代码编写源码位置net/mptcp/主要参考文件protocol.c、protocol.h、sched.c、options.c字数统计约 6200 字 代码片段基于真实内核源码简化以便理解