skaiuijing

引言

本章我们将进入TCP部分,这也是网络协议栈的精髓,可以说,TCP协议是现代网络的基石。

TCP协议非常复杂,就拿代码量来说,它通常会占据TCP/IP网络协议栈一半的代码。

我们可以使用eBPF作为调试工具。

tcp特性

tcp有非常多的特性,例如:连接管理、超时与重传、拥塞控制、保活机制等。

我们将会使用eBPF解析以下行为:

连接生命周期:三次握手、四次挥手、TIME_WAIT。

流量控制:rwnd 与接收缓存的关系。

拥塞控制:cwnd、ssthresh 的动态变化。

重传机制:RTO 超时、快速重传、SACK。

保活机制:keepalive 探测与超时。

挂载点

TCP 内核函数入口/出口挂载fentry / fexit

fentry 和 fexit 是基于 BTF 的挂载方式,比传统的 kprobe/kretprobe 更高效、更稳定。它们允许你在函数入口(fentry)和函数返回(fexit)时获取参数和返回值,非常适合调试 TCP 内部逻辑。

  • 推荐挂载的 TCP 函数
    • tcp_sendmsgtcp_recvmsg —— 应用层数据进入/离开 TCP。
    • tcp_acktcp_clean_rtx_queue —— ACK 处理、重传队列清理。
    • tcp_set_stateinet_csk_accept —— 状态迁移、连接建立。
    • tcp_retransmit_skbtcp_write_xmit —— 重传与发送逻辑。
    • tcp_keepalive_timertcp_delack_timer —— 保活与延迟 ACK 定时器。
  • 可观测内容
    • 入口参数struct sock *sk、数据长度、标志位、序列号。
    • 返回值:函数返回码、发送字节数、ACK 处理结果。
    • 派生指标:吞吐量、ACK 延迟、重传触发点、状态停留时间。

代码用例

如果我们想观察状态迁移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// fentry_tcp_set_state.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct evt {
__u64 ts;
__u32 pid;
int old_state;
int new_state;
};

struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");

// fentry: 函数入口
SEC("fentry/tcp_set_state")
int BPF_PROG(on_tcp_set_state, struct sock *sk, int state)
{
struct evt e = {};
e.ts = bpf_ktime_get_ns();
e.pid = bpf_get_current_pid_tgid() >> 32;
e.old_state = sk->sk_state;
e.new_state = state;
bpf_perf_event_output(bpf_get_current_task(), &events,
BPF_F_CURRENT_CPU, &e, sizeof(e));
return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

TCP 内核事件挂载:tracepoint

tracepoint 是内核维护的稳定事件接口,ABI 固定,适合长期观测和统计。相比 fentry,它粒度稍粗,但更安全。

  • 常见 TCP tracepoint
    • sock:inet_sock_set_state —— TCP 状态迁移。
    • tcp:tcp_retransmit_skb —— 重传事件。
    • tcp:tcp_receive_reset —— 收到 RST。
    • tcp:tcp_destroy_sock —— 连接销毁。
    • tcp:tcp_probe —— 报文收发探测(包括 keepalive)。
  • 学习价值
    • 观察三次握手、四次挥手的真实时序。
    • 统计重传率、连接生命周期。
    • 分析 FIN/RESET 的触发场景。

代码用例

tracepoint下的tcp_retransmit_skb每次 TCP 重传时都会触发,适合做重传率统计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// trace_tcp_retransmit.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>

struct retrans_evt {
__u64 ts;
__u32 saddr, daddr;
__u16 sport, dport;
};

struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");

SEC("tracepoint/tcp/tcp_retransmit_skb")
int on_retransmit(struct trace_event_raw_tcp_event_sk *ctx)
{
struct retrans_evt e = {};
e.ts = bpf_ktime_get_ns();
e.saddr = ctx->saddr;
e.daddr = ctx->daddr;
e.sport = ctx->sport;
e.dport = ctx->dport;
bpf_perf_event_output(bpf_get_current_task(), &events,
BPF_F_CURRENT_CPU, &e, sizeof(e));
return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

TCP 专用回调挂载:sockops

sockops 是专门为 TCP 设计的 eBPF 挂载点,直接挂在 TCP socket 上,能捕获连接和 RTT 等关键事件。

  • 关键回调
    • BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB —— 主动连接建立完成。
    • BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB —— 被动连接建立完成。
    • BPF_SOCK_OPS_RTT_CB —— RTT 更新。
    • BPF_SOCK_OPS_STATE_CB —— TCP 状态变化。
  • 可观测内容
    • 四元组(源/目的 IP 和端口)、进程信息。
    • RTT、srtt、rto、cwnd、ssthresh 等 TCP 内部参数。
    • 状态迁移时机、握手延迟。
  • 学习价值
    • 掌握 TCP 握手/挥手的延迟与时序。
    • 观察 RTT 与 cwnd 的动态变化。
    • 分析丢包对 RTT 和窗口的影响。
  • 优点
    • TCP 专用,能直接访问 struct sock
    • 适合做 per-connection 的调试和统计。

代码用例

我们可以在这个挂载点捕获rtt更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// sockops_rtt.bpf.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>

struct rtt_evt {
__u64 ts;
__u32 saddr, daddr;
__u16 sport, dport;
__u32 srtt_us;
};

struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
} events SEC(".maps");

SEC("sockops")
int on_sockops(struct bpf_sock_ops *ops)
{
if (ops->op != BPF_SOCK_OPS_RTT_CB)
return 0;

struct rtt_evt e = {};
e.ts = bpf_ktime_get_ns();
e.saddr = ops->local_ip4;
e.daddr = ops->remote_ip4;
e.sport = bpf_ntohs(ops->local_port);
e.dport = bpf_ntohs(ops->remote_port);
e.srtt_us = ops->srtt_us;

bpf_perf_event_output(bpf_get_current_task(), &events,
BPF_F_CURRENT_CPU, &e, sizeof(e));
return 0;
}

char LICENSE[] SEC("license") = "Dual BSD/GPL";

struct_ops —— 自定义 TCP 拥塞控制

struct_ops 允许用 eBPF 实现并注册一个新的 tcp_congestion_ops,相当于在内核里写一个拥塞控制算法。

  • 常见方法
    • init —— 初始化连接状态。
    • cong_control —— 根据 RTT、丢包、ECN 调整 cwnd。
    • undo_cwnd —— 丢包恢复时撤销 cwnd 降低。
    • in_slow_start —— 判断是否处于慢启动。
  • 学习价值
    • 理解慢启动、加性增大、乘性减小的核心逻辑。
    • 学习 RTT 估计、丢包响应、窗口调整。
    • 实验 pacing、流量整形对性能的影响。
  • 优点
    • 可以亲手实现一个简化版 Reno/CUBIC/BBR。
    • 能直接验证不同算法对吞吐量、延迟、重传的影响。

eBPF

Linux 内核 5.6 版本开始支持 eBPF 程序修改 TCP 拥塞算法,可通过在用户态修改内核中拥塞函数结构指针实现。

在 5.13 版本中,增加了该类程序类型直接调用部分内核代码的能力,这避免了在 eBPF 程序中需要重复实现内核中使用的 TCP 拥塞算法相关的函数。

这可能是eBPF技术的一小步,但有人认为这是Linux向自定义内核甚至是微内核演进的一大步。

STRUCT_OPS 是如何在源码层面被支持的

STRUCT_OPS 让 eBPF 程序可以实现并替换内核中的 “ops 结构”(如 struct tcp_congestion_ops)的函数指针集合。为此,内核与 libbpf 引入了两类新能力:一种新的 BPF 程序类型用于实现各个回调函数,和一种新的 BPF map 类型用于携带“整套 ops 实例”,由内核在更新 map 时完成“解析、跳板生成、注册到子系统”的全过程。

  • 新增类型与框架:
    • BPF_PROG_TYPE_STRUCT_OPS:针对每个 ops 成员函数的 eBPF 实现(如 cong_controlssthresh 等)。这些程序在加载后会被绑定到 trampoline,实现从内核到 eBPF 的直接调用。
    • BPF_MAP_TYPE_STRUCT_OPS:用户态创建一个特殊 map,map 的 “value” 用 BTF 描述为目标 ops 结构的子集或全量布局;更新该 map 会触发内核完成成员匹配、trampoline 生成、ops 注册/注销等生命周期动作。
    • bpf_struct_ops 框架:内核端定义了 struct bpf_struct_ops,为每个支持的 ops 家族(如 tcp_congestion_ops)提供校验、初始化各成员、注册/卸载等回调;并维护每个成员的函数模型用于 verifier 与 ABI 检查。

上述机制首先用于 TCP 拥塞控制,使 eBPF 写的算法像内核内置的 Reno/CUBIC 一样被选择与运行。

用户态与 libbpf:描述 ops、加载程序、初始化 map

在用户态,使用 libbpf 的 ELF section 将 eBPF 程序与 ops 描述组织起来:

  • 程序组织:
    • 每个 ops 方法用 SEC("struct_ops/<method>") 定义一个 BPF 函数(程序类型为 STRUCT_OPS),比如 dctcp_ssthreshdctcp_init 等,用 BTF 签名匹配内核原型(参数与返回类型必须一致)。
    • 这些 BPF 程序可直接调用内核导出的 TCP 算法辅助函数(后续版本支持 direct call,以减少重复实现)。
  • ops 结构声明与绑定:
    • SEC(".struct_ops") 声明一个 struct tcp_congestion_ops 的“值”,把各成员指派到对应的 BPF 程序地址(libbpf 会在 open/load 阶段解析并建立映射)。
    • libbpf 在 open/load 过程中调用类似 bpf_map__init_kern_struct_ops 的逻辑:依据 BTF,将“bpf 程序集合 + 结构体成员布局”转译为内核预期的 struct_ops map 载荷,准备好后续的内核端注册流程。
  • 要点:
    • 结构体可为内核原型的子集,顺序不必完全一致,但成员名、类型与函数原型要匹配;不匹配会在 libbpf 或 verifier 阶段失败。
    • 通过更新该 map 的元素(插入或删除 key=0 的唯一实例)来“注册/注销”该 ops 实例,形成与内核的联动接口。

上述组织方式在示例中展示了 DCTCP/CUBIC 的 eBPF 版本如何被声明与绑定为一个“完整的 struct_ops 实例”。

内核端:验证、成员初始化、trampoline 绑定与注册

当用户态对 BPF_MAP_TYPE_STRUCT_OPS 执行更新(update_elem)时,内核 bpf_struct_ops 框架驱动以下步骤:

  • 成员匹配与校验:
    • 内核根据 BTF 的 type-id/value-id 找到目标 ops 家族(如 tcp_congestion_ops),调用该家族的 check_member() 针对每个成员进行合法性校验(函数模型、参数/返回类型、可选/必选成员)。
    • 对数据成员(非函数指针)执行 init_member(),用于将用户态提供的常量字段写入内核侧的 ops 实例(例如标志位、名字),并为函数成员记录其对应的 BPF 程序句柄,供后续生成 trampoline 使用。
  • 生成 trampoline 并填充函数指针:
    • 对每个被实现的函数成员,内核通过 BPF trampoline 生成一个“内核到 eBPF”的跳板,形成可直接调用的入口;随后把 ops 结构体中的函数指针替换为该 trampoline 地址。
    • 这一步实现了“内核像调用普通内核函数一样调用 eBPF 实现”,同时保留 verifier 与类型模型的安全性保证。
  • 注册到子系统:
    • 完成结构体实例的成员填充后,调用家族的 reg() 回调,将该 ops 实例注册到对应的内核子系统(如通过 tcp_register_congestion_control() 链接到 TCP CC 框架的算法链表与选择路径)。
    • 删除 map 元素时,调用 unreg() 完成注销与 trampoline 释放,保证生命周期与引用计数一致。

这一整套路径在 5.6 首次用于 TCP 拥塞控制,并在后续版本进一步增强 direct call 能力与可访问性。

数据访问与安全:verifier、BTF CO-RE 与可写字段限制

为了安全地让 eBPF 算法操作 TCP 内部状态(如 cwnd/sshthresh),内核在 verifier 与 struct_access 上做了多重约束:

  • 字段访问与 CO-RE:
    • eBPF 程序通过 BTF CO-RE 访问 tcp_sockinet_csk_ca() 中的拥塞私有数据;加载时进行 “preserve_access_index” 重定向,匹配当前内核布局,避免直接偏移依赖。
    • 字段与函数的签名、大小、类型都通过 BTF 校验,不匹配直接拒绝加载,避免不安全访问。
  • 可写字段白名单:
    • 在 TCP CC 家族中,仅允许写入部分安全字段(如 snd_cwndsnd_ssthresh 等);内核家族的 struct_access() 控制允许修改的成员范围,防止破坏协议不变式或安全边界。
  • 频率与开销:
    • 算法回调处于 TCP 热路径,需保持最小开销;trampoline 与 BTF 直接调用减少胶水代码,降低调用成本;但仍建议避免复杂逻辑与重型 helper。

这些约束确保 eBPF 替换 ops 的同时不越权破坏内核语义与稳定性。

拥塞控制算法

eBPF可以自定义拥塞控制算法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2019 Facebook */

/* WARNING: This implemenation is not necessarily the same
* as the tcp_dctcp.c. The purpose is mainly for testing
* the kernel BPF logic.
*/

#include <linux/bpf.h>
#include <linux/types.h>
#include <bpf/bpf_helpers.h>
#include "bpf_trace_helpers.h"
#include "bpf_tcp_helpers.h"

char _license[] SEC("license") = "GPL";

#define DCTCP_MAX_ALPHA 1024U // alpha 的最大值 (1024 = 1.0)

/* DCTCP 私有控制块 (per-connection state) */
struct dctcp {
__u32 old_delivered; // 上一次 RTT 时的 delivered 快照 (来自 tcp_sock)
__u32 old_delivered_ce; // 上一次 RTT 时的 delivered_ce 快照 (来自 tcp_sock)
__u32 prior_rcv_nxt; // 上一次接收窗口的 rcv_nxt (用于 ACK 触发)
__u32 dctcp_alpha; // 拥塞估计参数 alpha (0~1024)
__u32 next_seq; // RTT 边界的序列号 (用于判断 RTT 是否结束)
__u32 ce_state; // 上一个包的 CE 状态 (0=非拥塞, 1=拥塞)
__u32 loss_cwnd; // 丢包时的 cwnd 值 (用于恢复)
};

static unsigned int dctcp_shift_g = 4; /* g = 1/2^4 */
static unsigned int dctcp_alpha_on_init = DCTCP_MAX_ALPHA;

static __always_inline void dctcp_reset(const struct tcp_sock *tp,
struct dctcp *ca)
{
ca->next_seq = tp->snd_nxt;

ca->old_delivered = tp->delivered;
ca->old_delivered_ce = tp->delivered_ce;
}

/* 连接初始化时调用*/
SEC("struct_ops/dctcp_init")
void BPF_PROG(dctcp_init, struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
struct dctcp *ca = inet_csk_ca(sk);

ca->prior_rcv_nxt = tp->rcv_nxt;
ca->dctcp_alpha = min(dctcp_alpha_on_init, DCTCP_MAX_ALPHA);
ca->loss_cwnd = 0;
ca->ce_state = 0;

dctcp_reset(tp, ca);
}

//计算新的慢启动阈值 (ssthresh)
SEC("struct_ops/dctcp_ssthresh")
__u32 BPF_PROG(dctcp_ssthresh, struct sock *sk)
{
struct dctcp *ca = inet_csk_ca(sk);
struct tcp_sock *tp = tcp_sk(sk);

ca->loss_cwnd = tp->snd_cwnd; // 保存丢包前的 cwnd
// ssthresh = cwnd - (cwnd * alpha / 2048)
return max(tp->snd_cwnd - ((tp->snd_cwnd * ca->dctcp_alpha) >> 11U), 2U);
}

/* 更新 alpha,收到ACK时调用*/
SEC("struct_ops/dctcp_update_alpha")
void BPF_PROG(dctcp_update_alpha, struct sock *sk, __u32 flags)
{
const struct tcp_sock *tp = tcp_sk(sk);
struct dctcp *ca = inet_csk_ca(sk);

/* Expired RTT */
if (!before(tp->snd_una, ca->next_seq)) {
__u32 delivered_ce = tp->delivered_ce - ca->old_delivered_ce;
__u32 alpha = ca->dctcp_alpha;

/* alpha = (1 - g) * alpha + g * F */
alpha -= min_not_zero(alpha, alpha >> dctcp_shift_g);
if (delivered_ce) {
__u32 delivered = tp->delivered - ca->old_delivered;

/* If dctcp_shift_g == 1, a 32bit value would overflow
* after 8 M packets.
*/
delivered_ce <<= (10 - dctcp_shift_g);
delivered_ce /= max(1U, delivered);

alpha = min(alpha + delivered_ce, DCTCP_MAX_ALPHA);
}
ca->dctcp_alpha = alpha;
dctcp_reset(tp, ca);
}
}

/* 丢包时的反应:cwnd 减半 */
static __always_inline void dctcp_react_to_loss(struct sock *sk)
{
struct dctcp *ca = inet_csk_ca(sk);
struct tcp_sock *tp = tcp_sk(sk);

ca->loss_cwnd = tp->snd_cwnd;
tp->snd_ssthresh = max(tp->snd_cwnd >> 1U, 2U);
}

SEC("struct_ops/dctcp_state")
void BPF_PROG(dctcp_state, struct sock *sk, __u8 new_state)
{
if (new_state == TCP_CA_Recovery &&
new_state != BPF_CORE_READ_BITFIELD(inet_csk(sk), icsk_ca_state))
dctcp_react_to_loss(sk);
/* We handle RTO in dctcp_cwnd_event to ensure that we perform only
* one loss-adjustment per RTT.
*/
}

static __always_inline void dctcp_ece_ack_cwr(struct sock *sk, __u32 ce_state)
{
struct tcp_sock *tp = tcp_sk(sk);

if (ce_state == 1)
tp->ecn_flags |= TCP_ECN_DEMAND_CWR;
else
tp->ecn_flags &= ~TCP_ECN_DEMAND_CWR;
}

/* Minimal DCTP CE state machine:
*
* S: 0 <- last pkt was non-CE
* 1 <- last pkt was CE
*/
static __always_inline
void dctcp_ece_ack_update(struct sock *sk, enum tcp_ca_event evt,
__u32 *prior_rcv_nxt, __u32 *ce_state)
{
__u32 new_ce_state = (evt == CA_EVENT_ECN_IS_CE) ? 1 : 0;

if (*ce_state != new_ce_state) {
/* CE state has changed, force an immediate ACK to
* reflect the new CE state. If an ACK was delayed,
* send that first to reflect the prior CE state.
*/
if (inet_csk(sk)->icsk_ack.pending & ICSK_ACK_TIMER) {
dctcp_ece_ack_cwr(sk, *ce_state);
bpf_tcp_send_ack(sk, *prior_rcv_nxt);
}
inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_NOW;
}
*prior_rcv_nxt = tcp_sk(sk)->rcv_nxt;
*ce_state = new_ce_state;
dctcp_ece_ack_cwr(sk, new_ce_state);
}

SEC("struct_ops/dctcp_cwnd_event")
void BPF_PROG(dctcp_cwnd_event, struct sock *sk, enum tcp_ca_event ev)
{
struct dctcp *ca = inet_csk_ca(sk);

switch (ev) {
case CA_EVENT_ECN_IS_CE:
case CA_EVENT_ECN_NO_CE:
dctcp_ece_ack_update(sk, ev, &ca->prior_rcv_nxt, &ca->ce_state);
break;
case CA_EVENT_LOSS:
dctcp_react_to_loss(sk);
break;
default:
/* Don't care for the rest. */
break;
}
}

SEC("struct_ops/dctcp_cwnd_undo")
__u32 BPF_PROG(dctcp_cwnd_undo, struct sock *sk)
{
const struct dctcp *ca = inet_csk_ca(sk);

return max(tcp_sk(sk)->snd_cwnd, ca->loss_cwnd);
}

SEC("struct_ops/tcp_reno_cong_avoid")
void BPF_PROG(tcp_reno_cong_avoid, struct sock *sk, __u32 ack, __u32 acked)
{
struct tcp_sock *tp = tcp_sk(sk);

if (!tcp_is_cwnd_limited(sk))
return;

/* In "safe" area, increase. */
// 这里就是慢启动阶段,指数级增长
if (tcp_in_slow_start(tp)) {
acked = tcp_slow_start(tp, acked);
if (!acked)
return;
}
/* In dangerous area, increase slowly. */
//这里是拥塞避免阶段,线性增长
tcp_cong_avoid_ai(tp, tp->snd_cwnd, acked);
}

SEC(".struct_ops")
struct tcp_congestion_ops dctcp_nouse = {
.init = (void *)dctcp_init,
.set_state = (void *)dctcp_state,
.flags = TCP_CONG_NEEDS_ECN,
.name = "bpf_dctcp_nouse",
};

SEC(".struct_ops")
struct tcp_congestion_ops dctcp = {
.init = (void *)dctcp_init,
.in_ack_event = (void *)dctcp_update_alpha,
.cwnd_event = (void *)dctcp_cwnd_event,
.ssthresh = (void *)dctcp_ssthresh,
.cong_avoid = (void *)tcp_reno_cong_avoid,
.undo_cwnd = (void *)dctcp_cwnd_undo,
.set_state = (void *)dctcp_state,
.flags = TCP_CONG_NEEDS_ECN,
.name = "bpf_dctcp",
};

真正的算法挂载是最后一个结构体:

它把前面实现的函数挂接到 TCP 栈的各个回调点:

  • .init:连接初始化时调用 → dctcp_init

  • .in_ack_event:收到 ACK 时调用 → dctcp_update_alpha

  • .cwnd_event:发生 ECN 或丢包事件时调用 → dctcp_cwnd_event

  • .ssthresh:计算新的慢启动阈值 → dctcp_ssthresh

  • .cong_avoid:拥塞避免阶段的窗口增长策略 → tcp_reno_cong_avoid(复用 Reno)

  • .undo_cwnd:撤销 cwnd 收缩 → dctcp_cwnd_undo

  • .set_state:TCP 状态变化时调用 → dctcp_state

  • .flags = TCP_CONG_NEEDS_ECN 表示该算法依赖 ECN 信号。

从 BPF 程序到 ops 注册

以下片段展示了 eBPF 实现 DCTCP 的若干方法,并将它们组装为一个 tcp_congestion_ops,通过 .struct_ops 暴露给内核完成注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 每个方法都是一个 BPF 程序(STRUCT_OPS 类型)
SEC("struct_ops/dctcp_init")
void BPF_PROG(dctcp_init, struct sock *sk) {
const struct tcp_sock *tp = tcp_sk(sk);
struct dctcp *ca = inet_csk_ca(sk);
ca->prior_rcv_nxt = tp->rcv_nxt;
ca->dctcp_alpha = min(dctcp_alpha_on_init, DCTCP_MAX_ALPHA);
ca->loss_cwnd = 0;
ca->ce_state = 0;
dctcp_reset(tp, ca);
}

SEC("struct_ops/dctcp_ssthresh")
__u32 BPF_PROG(dctcp_ssthresh, struct sock *sk) {
struct dctcp *ca = inet_csk_ca(sk);
struct tcp_sock *tp = tcp_sk(sk);
ca->loss_cwnd = tp->snd_cwnd;
return max(tp->snd_cwnd - ((tp->snd_cwnd * ca->dctcp_alpha) >> 11U), 2U);
}

// 组装为一个 struct_ops 实例,供内核注册
SEC(".struct_ops")
struct tcp_congestion_ops dctcp = {
.init = (void *)dctcp_init,
.in_ack_event= (void *)dctcp_update_alpha,
.cwnd_event = (void *)dctcp_cwnd_event,
.ssthresh = (void *)dctcp_ssthresh,
.cong_avoid = (void *)tcp_reno_cong_avoid,
.undo_cwnd = (void *)dctcp_cwnd_undo,
.set_state = (void *)dctcp_state,
.flags = TCP_CONG_NEEDS_ECN,
.name = "bpf_dctcp",
};
  • 用户态用 libbpf open/load 这些程序后,创建 BPF_MAP_TYPE_STRUCT_OPS 并把 dctcp 这个 value 写入;内核端 bpf_struct_ops 家族完成成员校验、trampoline 生成、并通过 reg() 挂到 TCP 拥塞控制框架;删除该 map 元素时则执行 unreg() 注销。

这条“程序 → map → family 校验与注册”的链路,是 STRUCT_OPS 支持的核心机制。它把 eBPF 程序以 BTF 符合的方式、在 verifier 与 trampoline 保护下,变成可被内核子系统当作本地算法调用的 ops 实例。

核心文件:net/ipv4/bpf_tcp_ca.c

这个文件是 struct_ops 在 TCP 拥塞控制上的第一个实现。它把 struct tcp_congestion_ops 暴露给 eBPF,让 eBPF 程序可以实现这些回调。

定义哪些成员是可选/不支持的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static u32 optional_ops[] = {
offsetof(struct tcp_congestion_ops, init),
offsetof(struct tcp_congestion_ops, release),
offsetof(struct tcp_congestion_ops, set_state),
offsetof(struct tcp_congestion_ops, cwnd_event),
offsetof(struct tcp_congestion_ops, in_ack_event),
offsetof(struct tcp_congestion_ops, pkts_acked),
offsetof(struct tcp_congestion_ops, min_tso_segs),
offsetof(struct tcp_congestion_ops, sndbuf_expand),
offsetof(struct tcp_congestion_ops, cong_control),
};

static u32 unsupported_ops[] = {
offsetof(struct tcp_congestion_ops, get_info),
};
  • optional_ops:这些函数指针可以不实现。

  • unsupported_ops:暂时不支持的接口(如 get_info)。

  • 意义:保证 eBPF 实现的 ops 至少覆盖必需的函数,避免缺失导致 TCP 栈崩溃。

初始化 BTF 类型信息

1
2
3
4
5
6
7
8
9
10
11
12
13
static int bpf_tcp_ca_init(struct btf *btf)
{
s32 type_id;

type_id = btf_find_by_name_kind(btf, "sock", BTF_KIND_STRUCT);
sock_id = type_id;

type_id = btf_find_by_name_kind(btf, "tcp_sock", BTF_KIND_STRUCT);
tcp_sock_id = type_id;
tcp_sock_type = btf_type_by_id(btf, tcp_sock_id);

return 0;
}
  • 找到内核里 struct sockstruct tcp_sock 的 BTF 类型 ID。
  • 后续 verifier 校验时用来判断 eBPF 程序访问的结构体是否合法。

控制 eBPF 程序对 tcp_sock 的访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static int bpf_tcp_ca_btf_struct_access(struct bpf_verifier_log *log,
const struct btf_type *t, int off, int size,
enum bpf_access_type atype, u32 *next_btf_id)
{
if (atype == BPF_READ)
return btf_struct_access(log, t, off, size, atype, next_btf_id);

if (t != tcp_sock_type) {
bpf_log(log, "only read is supported\n");
return -EACCES;
}

switch (off) {
case offsetof(struct tcp_sock, snd_cwnd):
case offsetof(struct tcp_sock, snd_cwnd_cnt):
case offsetof(struct tcp_sock, snd_ssthresh):
case offsetof(struct tcp_sock, ecn_flags):
return NOT_INIT;
default:
bpf_log(log, "no write support to tcp_sock at off %d\n", off);
return -EACCES;
}
}
  • 默认只允许 读访问

  • 特殊字段(snd_cwndsnd_ssthresh 等)允许写。

  • 意义:保证 eBPF 拥塞控制算法能调整窗口,但不会随意破坏 TCP 内部状态。

初始化 struct_ops 成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int bpf_tcp_ca_init_member(const struct btf_type *t,
const struct btf_member *member,
void *kdata, const void *udata)
{
const struct tcp_congestion_ops *utcp_ca;
struct tcp_congestion_ops *tcp_ca;
size_t tcp_ca_name_len;
int prog_fd;
u32 moff;

utcp_ca = (const struct tcp_congestion_ops *)udata;
tcp_ca = (struct tcp_congestion_ops *)kdata;

moff = btf_member_bit_offset(t, member) / 8;
switch (moff) {
case offsetof(struct tcp_congestion_ops, flags):
if (utcp_ca->flags & ~TCP_CONG_MASK)
return -EINVAL;
tcp_ca->flags = utcp_ca->flags;
return 1;
case offsetof(struct tcp_congestion_ops, name):
tcp_ca_name_len = strnlen(utcp_ca->name, sizeof(utcp_ca->name));
memcpy(tcp_ca->name, utcp_ca->name, sizeof(tcp_ca->name));
return 1;
}

/* compulsory func ptr 必须有对应的 bpf_prog */
prog_fd = (int)(*(unsigned long *)(udata + moff));
if (!prog_fd && !is_optional(moff) && !is_unsupported(moff))
return -EINVAL;

return 0;
}
  • 把用户态传入的 struct tcp_congestion_ops(带有 BPF 程序 fd)转化为内核态的 ops 实例。
  • 特殊处理 flagsname 字段。
  • 对函数指针成员,检查是否提供了对应的 BPF 程序。

注册/注销 TCP 拥塞控制算法

1
2
3
4
5
6
7
8
9
static int bpf_tcp_ca_reg(void *kdata)
{
return tcp_register_congestion_control(kdata);
}

static void bpf_tcp_ca_unreg(void *kdata)
{
tcp_unregister_congestion_control(kdata);
}
  • 当用户态 bpf_map_update_elem() 注册一个 struct_ops 实例时,调用 tcp_register_congestion_control() 把它挂到 TCP 栈。
  • 删除时调用 tcp_unregister_congestion_control()

定义 bpf_struct_ops 描述符

1
2
3
4
5
6
7
8
9
struct bpf_struct_ops bpf_tcp_congestion_ops = {
.verifier_ops = &bpf_tcp_ca_verifier_ops,
.reg = bpf_tcp_ca_reg,
.unreg = bpf_tcp_ca_unreg,
.check_member = bpf_tcp_ca_check_member,
.init_member = bpf_tcp_ca_init_member,
.init = bpf_tcp_ca_init,
.name = "tcp_congestion_ops",
};
  • 把 verifier、init、注册、注销等逻辑绑定到 tcp_congestion_ops 这个家族。
  • 内核的 bpf_struct_ops.c 框架会用这个描述符来驱动整个生命周期。

总结

源码层面,支持 struct_ops 的关键点是:

  1. 定义一个 bpf_struct_ops 描述符bpf_tcp_congestion_ops),告诉内核如何校验、初始化、注册/注销。
  2. 限制访问:通过 bpf_tcp_ca_btf_struct_access 控制 eBPF 程序对 tcp_sock 的读写权限。
  3. 成员初始化bpf_tcp_ca_init_member 把用户态传入的 BPF 程序 fd 绑定到内核的函数指针。
  4. 注册/注销:调用 TCP 栈的 tcp_register_congestion_control() / tcp_unregister_congestion_control()
  5. 最终效果:用户态写一个 .struct_ops 段的 eBPF 程序,加载后就能作为一个新的 TCP 拥塞控制算法被内核使用。

源码层面的注册

数据结构:struct tcp_congestion_ops

每个拥塞控制算法都用一个 struct tcp_congestion_ops 来描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct tcp_congestion_ops {
struct list_head list; // 链表节点
unsigned long flags;
void (*init)(struct sock *sk);
void (*release)(struct sock *sk);
u32 (*ssthresh)(struct sock *sk);
void (*cong_avoid)(struct sock *sk, u32 ack, u32 in_flight);
void (*set_state)(struct sock *sk, u8 new_state);
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
u32 (*undo_cwnd)(struct sock *sk);
void (*pkts_acked)(struct sock *sk, u32 num_acked, s32 rtt_us);
void (*get_info)(struct sock *sk, u32 ext, struct sk_buff *skb);
char name[TCP_CA_NAME_MAX]; // 算法名字
struct module *owner;
};
  • 这是一个 函数指针表,定义了 TCP 拥塞控制算法的所有回调。
  • 内核维护一个全局链表 tcp_cong_list,保存所有注册的算法。

注册函数:tcp_register_congestion_control()

源码(简化版)大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static LIST_HEAD(tcp_cong_list);
static DEFINE_SPINLOCK(tcp_cong_list_lock);

int tcp_register_congestion_control(struct tcp_congestion_ops *ca)
{
spin_lock(&tcp_cong_list_lock);

// 检查是否重名
list_for_each_entry(tmp, &tcp_cong_list, list) {
if (!strcmp(tmp->name, ca->name)) {
spin_unlock(&tcp_cong_list_lock);
return -EEXIST;
}
}

// 插入全局链表
list_add_tail(&ca->list, &tcp_cong_list);

spin_unlock(&tcp_cong_list_lock);
return 0;
}
  • 作用:把新的算法(ca)挂到 tcp_cong_list 链表里。
  • 检查:如果名字重复(比如已有 cubic,你再注册一个同名),会失败。
  • 结果:算法就成为系统可用的拥塞控制算法之一。

算法选择:tcp_set_congestion_control()

注册只是“加入候选池”。真正替换算法是在 套接字层 通过 tcp_set_congestion_control()

1
2
3
4
5
6
7
8
9
10
11
12
13
int tcp_set_congestion_control(struct sock *sk, const char *name)
{
struct tcp_congestion_ops *ca;

// 在链表里查找名字匹配的算法
ca = tcp_ca_find(name);
if (!ca)
return -ENOENT;

// 替换 sk->sk_ca_ops
sk->sk_ca_ops = ca;
return 0;
}
  • 每个 TCP socket (struct sock) 有一个指针 sk->sk_ca_ops,指向当前使用的拥塞控制算法。
  • 当你通过 sysctlsetsockopt(TCP_CONGESTION) 设置算法时,内核就会调用这个函数,把 sk_ca_ops 指向新的算法。

默认算法

  • 系统启动时,tcp_init() 会调用 tcp_register_congestion_control(&tcp_reno),把 Reno 注册为默认算法。

  • 你可以通过:

    1
    sysctl -w net.ipv4.tcp_congestion_control=bpf_cubic

    来切换默认算法。这样新建的 socket 会用bpf_cubic

eBPF struct_ops 的结合点

当你用 eBPF 定义一个 .struct_opstcp_congestion_ops,内核会在 bpf_tcp_ca_reg() 里调用:

1
2
3
4
static int bpf_tcp_ca_reg(void *kdata)
{
return tcp_register_congestion_control(kdata);
}
  • kdata 就是 eBPF 程序生成的 struct tcp_congestion_ops 实例。
  • 它被挂到 tcp_cong_list,和内核内置的 Reno、CUBIC 一起管理。
  • 之后你就能通过 sysctlsetsockopt 把 socket 的算法切换到这个 eBPF 实现。

总结

  • **tcp_register_congestion_control()**:把算法挂到全局链表 tcp_cong_list
  • **tcp_set_congestion_control()**:把 socket 的 sk->sk_ca_ops 指针切换到指定算法。
  • 替换原算法的本质:不是“覆盖”,而是“注册多个算法 → 选择其中一个”。
  • eBPF struct_ops:通过 bpf_tcp_ca_reg() 调用 tcp_register_congestion_control(),让 eBPF 算法和内核算法一样进入候选池。

调用链流程图(从 eBPF .struct_opstcp_register_congestion_controltcp_cong_listtcp_set_congestion_controlsk->sk_ca_ops