当前位置:首页>Linux>Linux内核MPTCP架构总览:跟着一个send()走进内核

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

  • 2026-09-06 12:30:37
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(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 调度器
  • 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 行

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。
  • **conn_list**:子流链表。

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 是"快递单号"。

发送端:

  1. 应用发送 1000 字节数据,MPTCP 层用 DSN 给这 1000 字节编号。
  2. 调度器决定:前 500 字节走子流 A,后 500 字节走子流 B。
  3. 每条子流用自己的 SSN 传输。
  4. 每个数据包携带 DSS Option,告诉接收端:"这个包的 SSN 对应 DSN 的哪个范围"。

接收端:

  1. 从不同子流收到数据包,从 DSS Option 得知 SSN → DSN 的映射。
  2. 按 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;
}

默认调度器的逻辑:

  • 选择拥塞窗口最大、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 字段中包含 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 行):调度器框架,比较精简。

如何阅读源码

  1. 从 mptcp_sendmsg() 开始,跟着调用链走。
  2. 用 grep 或 cscope 搜索函数定义。
  3. 参考 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 层
  • 调度器选择子流
  • 每条子流独立传输,携带 DSS Option
  • 接收端根据 DSN 重组数据
  • 最终交付给应用程序

这就是 Linux 内核 MPTCP 的设计哲学:对应用透明,对 TCP 栈最小化侵入。

三个要点:

  1. 双层 socket 架构:用户态看到 1 个 MPTCP socket,内核里是 N 个 TCP socket。
  2. 双层编号系统:DSN 保证连接级有序,SSN 保证子流级有序。
  3. 完全解耦:TCP 层不知道 MPTCP 的存在,MPTCP 层在上面"编排"TCP 连接。

下一篇预告:《MPTCP 路径管理器(Path Manager)源码解析:子流何时创建、何时销毁》

关注本系列,我们将用 20 篇文章,把 MPTCP 从协议到内核、从实战到前沿,讲透彻。


本文基于Linux6.x内核主线代码编写源码位置:net/mptcp/主要参考文件:protocol.c、protocol.h、sched.c、options.c基于真实内核源码(简化以便理解)

最新文章

随机文章