Linux 内核中 eBPF 程序的挂载点已经从最初的 socket filter 扩展到覆盖网络栈的几乎每一层。一个包从网卡进入到最终被应用读取,中间可能经过 XDP、TC ingress、cgroup ingress、socket filter、sk_skb verdict 等多个 eBPF 钩子。每个钩子有不同的上下文结构体、可用的 helper 集合、返回值语义。理解这些钩子在内核中的精确位置和执行时机,是构建高性能网络数据面(Cilium、Katran、Calico eBPF 模式)的基础。
本文从内核 6.6/6.8 源码出发,逐个拆解七类网络 eBPF 钩子的内核实现、数据结构和典型应用场景。
一、eBPF 网络钩子全景图
eBPF 在网络子系统中的挂载点可以按数据流方向和层次分为七类:
| 钩子类型 | 程序类型 | 挂载点 | 上下文结构体 | 内核上下文 |
|---|---|---|---|---|
| XDP | BPF_PROG_TYPE_XDP |
驱动 NAPI poll | struct xdp_md |
struct xdp_buff |
| TC BPF | BPF_PROG_TYPE_SCHED_CLS |
TC ingress/egress | struct __sk_buff |
struct sk_buff |
| cgroup SKB | BPF_PROG_TYPE_CGROUP_SKB |
cgroup ingress/egress | struct __sk_buff |
struct sk_buff |
| cgroup sock | BPF_PROG_TYPE_CGROUP_SOCK |
socket 创建/绑定/连接 | struct bpf_sock |
struct sock |
| socket ops | BPF_PROG_TYPE_SOCK_OPS |
TCP 生命周期事件 | struct bpf_sock_ops |
struct bpf_sock_ops_kern |
| sk_skb | BPF_PROG_TYPE_SK_SKB |
socket 收包裁决 | struct __sk_buff |
struct sk_buff |
| sk_msg | BPF_PROG_TYPE_SK_MSG |
sendmsg 路径裁决 | struct sk_msg_md |
struct sk_msg |
| struct_ops | BPF_PROG_TYPE_STRUCT_OPS |
内核子系统回调 | 由子系统定义 | 由子系统定义 |
下图展示了 eBPF 网络钩子在收包/发包路径和 socket 层的完整挂载点:
一个典型的收包路径上,eBPF 钩子的执行顺序为:
NIC → XDP → TC ingress → Netfilter → cgroup ingress → socket filter → sk_skb verdict
发包路径:
sendmsg → sk_msg verdict → socket filter → cgroup egress → TC egress → XDP(egress, 6.6+)
每个钩子的返回值决定了包的命运——通过、丢弃、重定向,或者修改后继续。
二、TC BPF:direct-action 与 bpf_mprog 多程序链
2.1 TC BPF 在发包/收包路径中的位置
TC BPF 挂载在 __dev_queue_xmit()(egress)和
__netif_receive_skb_core()(ingress)中的 TC
分类器调用点。与传统 TC qdisc 不同,TC BPF 使用
direct-action(DA)模式——程序既是分类器又是动作,返回值直接决定包的处理方式:
/* include/uapi/linux/pkt_cls.h */
#define TC_ACT_UNSPEC (-1)
#define TC_ACT_OK 0 /* 继续正常处理 */
#define TC_ACT_RECLASSIFY 1 /* 重新分类 */
#define TC_ACT_SHOT 2 /* 丢弃 */
#define TC_ACT_PIPE 3 /* 传递给下一个 action */
#define TC_ACT_STOLEN 4 /* 已被消费,不再处理 */
#define TC_ACT_REDIRECT 7 /* 重定向 */在 net/core/dev.c 的收包路径中,TC ingress
的调用发生在 GRO 之后、Netfilter 之前:
/* net/core/dev.c - __netif_receive_skb_core() 简化 */
static int __netif_receive_skb_core(struct sk_buff **pskb, ...)
{
skb = *pskb;
...
/* TC ingress 钩子 */
skb = sch_handle_ingress(skb, &pt_prev, &ret, orig_dev, &another);
if (!skb)
goto out; /* TC BPF 返回 TC_ACT_SHOT 或 TC_ACT_STOLEN */
...
/* 继续 Netfilter、协议栈分发 */
}在 sch_handle_ingress() 中,内核调用
tcf_classify() 执行 TC BPF 程序:
/* net/core/dev.c - sch_handle_ingress() 简化 */
static struct sk_buff *sch_handle_ingress(struct sk_buff *skb, ...)
{
struct mini_Qdisc *miniq = rcu_dereference_bh(skb->dev->miniq_ingress);
...
qdisc_skb_cb(skb)->mru = 0;
qdisc_skb_cb(skb)->post_ct = false;
mini_qdisc_bstats_cpu_update(miniq, skb);
switch (tcf_classify(skb, miniq->block, miniq->filter_list, &cl_res, false)) {
case TC_ACT_OK:
case TC_ACT_RECLASSIFY:
skb->tc_index = TC_H_MIN(cl_res.classid);
break;
case TC_ACT_SHOT:
mini_qdisc_qstats_cpu_update(miniq, skb);
kfree_skb_reason(skb, SKB_DROP_REASON_TC_INGRESS);
return NULL;
case TC_ACT_STOLEN:
case TC_ACT_QUEUED:
case TC_ACT_TRAP:
consume_skb(skb);
return NULL;
case TC_ACT_REDIRECT:
skb_do_redirect(skb); /* bpf_redirect() 的目标 */
return NULL;
...
}
return skb;
}2.2 bpf_mprog:6.6+ 多程序链
Linux 6.6 引入了
bpf_mprog(multi-prog)机制,允许在一个 TC
挂载点上链式执行多个 BPF 程序,取代了早期通过
tc filter add
多次添加的方式。核心数据结构定义在
include/linux/bpf_mprog.h:
/* include/linux/bpf_mprog.h */
struct bpf_mprog_fp {
struct bpf_prog *prog;
};
struct bpf_mprog_bundle {
struct bpf_mprog_entry a; /* double-buffer:活跃 entry 和备用 entry */
struct bpf_mprog_entry b;
struct bpf_mprog_cp cp_items[BPF_MPROG_MAX]; /* 最多 64 个程序 */
struct bpf_prog *ref;
atomic64_t revision;
u32 count;
};
struct bpf_mprog_entry {
struct bpf_mprog_fp fp_items[BPF_MPROG_MAX];
struct bpf_mprog_bundle *parent;
};bpf_mprog 使用 double-buffer
设计——修改在备用 entry 上进行,完成后通过
rcu_assign_pointer
原子切换。这使得在不中断数据面的情况下添加、删除、替换 BPF
程序成为可能。
多程序链的执行逻辑在 tcx_run()
中(net/sched/sch_ingress.c),每个程序按优先级顺序执行,前一个程序的返回值决定是否继续:
/* 简化的 tcx_run 逻辑 */
static int tcx_run(struct tcx_entry *entry, struct sk_buff *skb, bool ingress)
{
struct bpf_mprog_entry *active = rcu_dereference_bh(entry->active);
struct bpf_mprog_fp *fp;
struct bpf_prog *prog;
int ret = TC_ACT_UNSPEC;
bpf_mprog_foreach_prog(active, fp, prog) {
ret = bpf_prog_run(prog, skb);
if (ret != TC_ACT_UNSPEC)
break; /* 非 UNSPEC 则中止链式执行 */
}
return ret;
}关键语义:TC_ACT_UNSPEC(-1)表示”我不关心这个包,交给链中的下一个程序”;其他返回值(OK、SHOT、REDIRECT
等)则终止链式执行。
2.3 TC BPF 与 XDP 的选择
| 维度 | XDP | TC BPF |
|---|---|---|
| 执行位置 | 驱动 NAPI poll,sk_buff 分配前 |
sk_buff 分配后,GRO 之后 |
| 上下文 | xdp_buff(线性缓冲区) |
__sk_buff(完整 sk_buff) |
| 可访问元数据 | L2/L3/L4 头部 | 完整 sk_buff 字段(mark、priority、cgroup 等) |
| 重定向目标 | devmap、cpumap、xskmap | ifindex(任意设备)、Netfilter conntrack |
| egress 支持 | 有限(6.6+ BPF_XDP_DEVMAP) | 完整 egress 支持 |
| 性能 | 最高(跳过协议栈) | 较高(经过 GRO,有 sk_buff 开销) |
| 典型场景 | DDoS 过滤、负载均衡 | 容器网络策略、NAT、带状态过滤 |
Cilium 同时使用两者:XDP 做无状态的 DDoS 过滤和负载均衡预处理,TC BPF 做有状态的 conntrack 和策略执行。
三、cgroup BPF:基于进程层级的网络策略
3.1 cgroup SKB 过滤
BPF_PROG_TYPE_CGROUP_SKB 程序挂载在 cgroup
的 ingress 和 egress
路径上,可以按进程组(cgroup)执行网络策略。内核在
include/linux/bpf-cgroup.h 中定义了调用宏:
/* include/linux/bpf-cgroup.h */
#define BPF_CGROUP_RUN_PROG_INET_INGRESS(sk, skb)
({
int __ret = 0;
if (cgroup_bpf_enabled(CGROUP_INET_INGRESS) &&
cgroup_bpf_sock_enabled(sk, CGROUP_INET_INGRESS))
__ret = __cgroup_bpf_run_filter_skb(sk, skb,
CGROUP_INET_INGRESS);
__ret;
})
#define BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb)
({
int __ret = 0;
if (cgroup_bpf_enabled(CGROUP_INET_EGRESS) && sk) {
typeof(sk) __sk = sk_to_full_sk(sk);
if (sk_fullsock(__sk) && __sk == skb_to_full_sk(skb) &&
cgroup_bpf_sock_enabled(__sk, CGROUP_INET_EGRESS))
__ret = __cgroup_bpf_run_filter_skb(__sk, skb,
CGROUP_INET_EGRESS);
}
__ret;
})cgroup ingress 钩子在 IP 层的
ip_local_deliver() 之后、socket
分发之前执行;cgroup egress 钩子在 ip_output()
路径上执行。
关键特性:cgroup BPF 程序继承层级——父
cgroup 的程序对所有子 cgroup 生效。内核通过
cgrp->bpf.effective[type]
数组存储有效程序列表,层级合并在
cgroup_bpf_inherit()
中完成。cgroup_bpf_sock_enabled()
先做快速检查——如果该 cgroup 没有挂载任何 BPF
程序(effective 指向
bpf_empty_prog_array),则跳过整个 BPF
执行路径,零开销。
3.2 cgroup socket 控制
除了 SKB 过滤,cgroup BPF 还提供了 socket 生命周期控制:
/* 各挂载点对应的宏 */
BPF_CGROUP_RUN_PROG_INET_SOCK(sk) /* socket 创建 */
BPF_CGROUP_RUN_PROG_INET_SOCK_RELEASE(sk) /* socket 关闭 */
BPF_CGROUP_RUN_PROG_INET4_POST_BIND(sk) /* bind() 之后 */
BPF_CGROUP_RUN_PROG_INET4_CONNECT(sk, ...) /* connect() */
BPF_CGROUP_RUN_PROG_INET_BIND_LOCK(sk, ...) /* bind() 带 CAP 检查绕过 */这些钩子允许 BPF 程序在 socket 创建时拒绝连接、在
connect() 时修改目标地址(透明代理)、在
bind() 时覆盖端口绑定(实现
connect-time 负载均衡)。
典型应用场景:
- Kubernetes NetworkPolicy:按 Pod cgroup 限制出入流量
- 透明代理:在
connect()时将目标 IP 替换为代理地址 - bind()
端口覆盖:允许非特权进程绑定低端口(
BPF_RET_BIND_NO_CAP_NET_BIND_SERVICE)
/* include/linux/bpf-cgroup.h - bind() CAP 绕过机制 */
#define BPF_CGROUP_RUN_PROG_INET_BIND_LOCK(sk, uaddr, uaddrlen, atype, bind_flags) \
({ \
u32 __flags = 0; \
int __ret = 0; \
if (cgroup_bpf_enabled(atype)) { \
lock_sock(sk); \
__ret = __cgroup_bpf_run_filter_sock_addr(sk, uaddr, ... \
atype, NULL, &__flags); \
release_sock(sk); \
if (__flags & BPF_RET_BIND_NO_CAP_NET_BIND_SERVICE) \
*bind_flags |= BIND_NO_CAP_NET_BIND_SERVICE; \
} \
__ret; \
})3.3 cgroup setsockopt/getsockopt 拦截
cgroup BPF 还可以拦截 setsockopt() 和
getsockopt() 调用:
/* include/linux/bpf-cgroup.h */
int __cgroup_bpf_run_filter_setsockopt(struct sock *sock, int *level,
int *optname, sockptr_t optval,
int *optlen, char **kernel_optval);
int __cgroup_bpf_run_filter_getsockopt(struct sock *sk, int level,
int optname, sockptr_t optval,
sockptr_t optlen, int max_optlen,
int retval);BPF 程序可以修改 setsockopt
的参数(例如强制设置 TCP_CONGESTION
为指定算法),也可以拦截 getsockopt
返回值(例如隐藏敏感的 socket
选项)。这在多租户环境中用于统一 TCP 调优策略。
四、socket ops:TCP 生命周期的 eBPF 回调
4.1 bpf_sock_ops_kern 结构体
BPF_PROG_TYPE_SOCK_OPS 程序在 TCP
连接的各个生命周期事件中被调用。内核上下文是
struct bpf_sock_ops_kern:
/* include/linux/filter.h:1340 */
struct bpf_sock_ops_kern {
struct sock *sk;
union {
u32 args[4]; /* 事件参数 */
u32 reply; /* BPF 程序返回值 */
u32 replylong[4]; /* 多返回值 */
};
struct sk_buff *syn_skb; /* SYN 包(被动连接) */
struct sk_buff *skb; /* 当前处理的包 */
void *skb_data_end; /* 包数据结束指针 */
u8 op; /* 当前操作码 */
u8 is_fullsock; /* 是否完整 socket */
u8 remaining_opt_len; /* 剩余 TCP 选项空间 */
u64 temp; /* 临时存储(不会初始化为 0) */
};4.2 操作码:TCP 生命周期事件
内核定义了 15+
个操作码(include/uapi/linux/bpf.h:6801),覆盖
TCP 连接的完整生命周期:
enum {
BPF_SOCK_OPS_VOID,
BPF_SOCK_OPS_TIMEOUT_INIT, /* SYN-RTO 初始值 */
BPF_SOCK_OPS_RWND_INIT, /* 初始通告窗口 */
BPF_SOCK_OPS_TCP_CONNECT_CB, /* 主动连接初始化前 */
BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB, /* 主动连接建立 */
BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB,/* 被动连接建立 */
BPF_SOCK_OPS_NEEDS_ECN, /* 拥塞控制是否需要 ECN */
BPF_SOCK_OPS_BASE_RTT, /* 基线 RTT */
BPF_SOCK_OPS_RTO_CB, /* RTO 超时触发 */
BPF_SOCK_OPS_RETRANS_CB, /* 重传事件 */
BPF_SOCK_OPS_STATE_CB, /* TCP 状态变化 */
BPF_SOCK_OPS_TCP_LISTEN_CB, /* listen() 后 */
BPF_SOCK_OPS_RTT_CB, /* 每次 RTT 采样 */
BPF_SOCK_OPS_PARSE_HDR_OPT_CB, /* 解析 TCP 头部选项 */
BPF_SOCK_OPS_HDR_OPT_LEN_CB, /* 预留头部选项空间 */
BPF_SOCK_OPS_WRITE_HDR_OPT_CB, /* 写入自定义头部选项 */
};4.3 tcp_call_bpf() 调用链
所有 sock_ops 回调通过 tcp_call_bpf()
触发,定义在 include/net/tcp.h:2618:
/* include/net/tcp.h:2618 */
static inline int tcp_call_bpf(struct sock *sk, int op, u32 nargs, u32 *args)
{
struct bpf_sock_ops_kern sock_ops;
int ret;
memset(&sock_ops, 0, offsetof(struct bpf_sock_ops_kern, temp));
if (sk_fullsock(sk)) {
sock_ops.is_fullsock = 1;
sock_owned_by_me(sk); /* 必须持有 socket 锁 */
}
sock_ops.sk = sk;
sock_ops.op = op;
if (nargs > 0)
memcpy(sock_ops.args, args, nargs * sizeof(*args));
ret = BPF_CGROUP_RUN_PROG_SOCK_OPS(&sock_ops);
if (ret == 0)
ret = sock_ops.reply; /* BPF 程序的返回值 */
else
ret = -1;
return ret;
}注意 BPF_CGROUP_RUN_PROG_SOCK_OPS
宏——sock_ops BPF 程序通过 cgroup
路径执行,这意味着它与 cgroup
层级绑定。内核在以下调用点触发
tcp_call_bpf():
tcp_init_transfer():ACTIVE_ESTABLISHED_CB/PASSIVE_ESTABLISHED_CBtcp_set_state():STATE_CBtcp_retransmit_skb():RETRANS_CBtcp_rto_timeout():RTO_CBtcp_rcv_rtt_update():RTT_CBtcp_connect():TCP_CONNECT_CBtcp_v4_init_sock():TIMEOUT_INIT、RWND_INIT
4.4 自定义 TCP 头部选项
Linux 5.10+ 引入了三个操作码用于 BPF 程序读写自定义 TCP 头部选项:
HDR_OPT_LEN_CB:在构建 TCP 头部时被调用,BPF 程序通过bpf_reserve_hdr_opt()预留选项空间WRITE_HDR_OPT_CB:在写入 TCP 头部时被调用,BPF 程序通过bpf_store_hdr_opt()写入自定义选项PARSE_HDR_OPT_CB:在收到包时被调用,BPF 程序通过bpf_load_hdr_opt()解析自定义选项
典型用途:在 TCP 选项中携带自定义元数据(如负载均衡器的连接 ID、MPTCP 子流标识),实现端到端的信息传递而不依赖应用层协议。
4.5 sock_ops 参数调优
sock_ops BPF 程序可以通过 bpf_setsockopt()
helper 在内核中直接调整 TCP 参数:
/* uapi/linux/bpf.h - BPF 专用 TCP 选项 */
enum {
TCP_BPF_IW = 1001, /* 初始拥塞窗口 */
TCP_BPF_SNDCWND_CLAMP = 1002, /* 发送窗口上限 */
TCP_BPF_DELACK_MAX = 1003, /* 最大延迟 ACK(微秒) */
TCP_BPF_RTO_MIN = 1004, /* 最小 RTO(微秒) */
};这些参数只能通过 BPF 设置,不对用户态
setsockopt()
暴露。典型场景:在数据中心内网络设置较小的 RTO_MIN(如
5ms)以加速故障恢复,同时对公网流量保持默认值。
五、sk_msg/sk_skb 与 sockmap:socket 层的可编程重定向
5.1 sockmap 核心数据结构
sockmap(BPF_MAP_TYPE_SOCKMAP /
BPF_MAP_TYPE_SOCKHASH)是一种特殊的 BPF
map,存储 socket 引用。结合 sk_msg 和
sk_skb 程序类型,它实现了在 socket
层的数据重定向——绕过整个 TCP/IP 协议栈。
核心数据结构定义在
include/linux/skmsg.h:
/* include/linux/skmsg.h:43 */
struct sk_msg {
struct sk_msg_sg sg; /* scatter-gather 数据 */
void *data; /* 数据起始 */
void *data_end; /* 数据结束 */
u32 apply_bytes; /* 裁决应用字节数 */
u32 cork_bytes; /* cork 聚合字节数 */
u32 flags;
struct sk_buff *skb;
struct sock *sk_redir; /* 重定向目标 socket */
struct sock *sk; /* 源 socket */
struct list_head list;
};sk_msg 操作的是 sendmsg()
路径的消息数据(scatter-gather 页面),而非
sk_buff。这使得内核到内核的数据传递完全绕过 TCP
分段、IP 路由、Netfilter 等开销。
5.2 sk_psock:socket 的 BPF 附属体
每个参与 sockmap 重定向的 socket 都有一个
sk_psock 结构体:
/* include/linux/skmsg.h:79 */
struct sk_psock {
struct sock *sk; /* 所属 socket */
struct sock *sk_redir; /* 重定向目标 */
u32 apply_bytes;
u32 cork_bytes;
u32 eval; /* 裁决结果 */
bool redir_ingress;/* 重定向到 ingress? */
struct sk_msg *cork; /* cork 聚合缓冲区 */
struct sk_psock_progs progs; /* BPF 程序集 */
struct strparser strp; /* stream parser */
struct sk_buff_head ingress_skb; /* ingress 队列 */
struct list_head ingress_msg; /* ingress 消息列表 */
spinlock_t ingress_lock;
unsigned long state;
struct list_head link; /* sockmap 链接列表 */
spinlock_t link_lock;
refcount_t refcnt;
/* 保存的原始回调 */
void (*saved_unhash)(struct sock *sk);
void (*saved_destroy)(struct sock *sk);
void (*saved_close)(struct sock *sk, long timeout);
void (*saved_write_space)(struct sock *sk);
void (*saved_data_ready)(struct sock *sk);
int (*psock_update_sk_prot)(struct sock *sk, struct sk_psock *psock,
bool restore);
struct proto *sk_proto; /* 原始协议操作 */
...
};sk_psock_progs 存储了四种 BPF 程序引用:
/* include/linux/skmsg.h:56 */
struct sk_psock_progs {
struct bpf_prog *msg_parser; /* sk_msg 裁决 */
struct bpf_prog *stream_parser; /* sk_skb stream parser */
struct bpf_prog *stream_verdict; /* sk_skb stream verdict */
struct bpf_prog *skb_verdict; /* sk_skb skb verdict */
};5.3 sk_msg 重定向:sendmsg 路径拦截
BPF_PROG_TYPE_SK_MSG 程序挂载在
tcp_sendmsg() 路径上。当应用调用
sendmsg() 时,内核不走正常的 TCP
发送路径,而是将消息交给 sk_msg BPF
程序裁决:
/* sk_msg 程序可用的动作 */
enum __sk_action {
__SK_DROP = 0, /* 丢弃消息 */
__SK_PASS, /* 正常发送 */
__SK_REDIRECT, /* 重定向到另一个 socket */
__SK_NONE,
};重定向通过 bpf_msg_redirect_map() 或
bpf_msg_redirect_hash() helper 实现:
sendmsg() → tcp_bpf_sendmsg() → sk_msg BPF 程序
├── __SK_PASS → 正常 TCP 发送
├── __SK_DROP → 丢弃
└── __SK_REDIRECT → sk_psock_msg_verdict()
→ 从 sockmap 查找目标 socket
→ tcp_bpf_sendmsg_redir()
→ 直接写入目标 socket 的 ingress 队列
关键优化:重定向的数据不经过 TCP 分段、校验和计算、IP 路由、Netfilter。在同一主机的两个进程间通信(如 sidecar 代理与应用),这可以将延迟降低 50% 以上。
5.4 sk_skb 裁决:收包路径拦截
BPF_PROG_TYPE_SK_SKB 程序在 socket
收到数据时被调用,有两种模式:
- stream_parser + stream_verdict:先由
stream_parser确定消息边界(返回消息长度),再由stream_verdict做裁决。基于strparser(stream parser)框架 - skb_verdict:直接对收到的
sk_buff做裁决,不需要消息边界解析
skb_verdict 模式更简单也更常用。当 socket
收到数据时,sk_psock 的
saved_data_ready 回调被触发,执行 BPF
裁决程序:
tcp_rcv_established() → tcp_data_ready() → sk->sk_data_ready()
→ sk_psock_verdict_data_ready()
→ sk_psock_verdict_apply()
├── __SK_PASS → sk_psock_skb_ingress_enqueue() → 正常接收
├── __SK_DROP → kfree_skb()
└── __SK_REDIRECT → skb_send_sock() → 目标 socket
5.5 典型应用:Cilium socket-level 负载均衡
Cilium 的 socket-level 负载均衡利用 sockmap 实现服务网格中的 sidecar-free 流量代理:
- sock_ops BPF 程序在
ACTIVE_ESTABLISHED_CB和PASSIVE_ESTABLISHED_CB时,将 socket 加入 sockhash map - sk_msg BPF 程序在
sendmsg()时查找 sockhash,找到对端 socket 后直接重定向 - 数据完全在内核中从一个 socket 传递到另一个 socket,绕过整个 TCP/IP 栈
Pod A → sendmsg() → sk_msg BPF → sockhash 查找 → Pod B 的 ingress 队列
(无需经过 TCP/IP/Netfilter/TC)
六、struct_ops:用 eBPF 实现内核子系统回调
6.1 struct_ops 机制概述
BPF_PROG_TYPE_STRUCT_OPS 是 Linux 5.6
引入的最强大的 eBPF 扩展机制。它允许 BPF
程序替换内核子系统的操作函数表——如
tcp_congestion_ops
中的各个回调。传统上,自定义拥塞控制需要编写内核模块;struct_ops
使得用 eBPF 实现完整的拥塞控制算法成为可能。
/* include/linux/bpf.h:1700 */
struct bpf_struct_ops {
const struct bpf_verifier_ops *verifier_ops;
int (*init)(struct btf *btf);
int (*check_member)(const struct btf_type *t,
const struct btf_member *member,
const struct bpf_prog *prog);
int (*init_member)(const struct btf_type *t,
const struct btf_member *member,
void *kdata, const void *udata);
int (*reg)(void *kdata); /* 注册 */
void (*unreg)(void *kdata); /* 注销 */
int (*update)(void *kdata, void *old_kdata); /* 热更新 */
int (*validate)(void *kdata); /* 验证 */
const struct btf_type *type;
const struct btf_type *value_type;
const char *name;
struct btf_func_model func_models[BPF_STRUCT_OPS_MAX_NR_MEMBERS];
u32 type_id;
u32 value_id;
void *cfi_stubs;
};6.2 BPF 拥塞控制:tcp_congestion_ops
最典型的 struct_ops 应用是实现自定义 TCP
拥塞控制。tcp_congestion_ops
定义了拥塞控制算法的所有回调:
/* include/net/tcp.h - tcp_congestion_ops 简化 */
struct tcp_congestion_ops {
/* 必须实现 */
u32 (*ssthresh)(struct sock *sk); /* 慢启动阈值 */
void (*cong_avoid)(struct sock *sk, u32 ack, u32 acked); /* 拥塞避免 */
/* 可选实现 */
void (*init)(struct sock *sk); /* 连接初始化 */
void (*release)(struct sock *sk); /* 连接关闭 */
void (*set_state)(struct sock *sk, u8 new_state);
void (*cwnd_event)(struct sock *sk, enum tcp_ca_event ev);
void (*in_ack_event)(struct sock *sk, u32 flags);
void (*pkts_acked)(struct sock *sk, const struct ack_sample *sample);
u32 (*min_tso_segs)(struct sock *sk);
void (*cong_control)(struct sock *sk, const struct rate_sample *rs);
u32 (*undo_cwnd)(struct sock *sk);
u32 (*sndbuf_expand)(struct sock *sk);
char name[TCP_CA_NAME_MAX];
struct module *owner;
};使用 struct_ops 实现 BPF 拥塞控制的流程:
- 用 BPF C 代码实现
tcp_congestion_ops的各个回调 - 通过
BPF_MAP_TYPE_STRUCT_OPSmap 加载到内核 - 内核验证器检查每个回调的类型签名和安全性
- 通过
bpf_struct_ops_get()/bpf_struct_ops_put()管理生命周期 - 用
setsockopt(TCP_CONGESTION, "bpf_cubic")为 socket 选择 BPF 拥塞控制
struct_ops 的验证极其严格——verifier 不仅检查内存安全,还通过 BTF 类型信息验证回调函数签名匹配。
6.3 struct_ops 的热更新
Linux 6.4 引入了 struct_ops 的 update
回调,允许在不断开连接的情况下替换拥塞控制算法。这在大规模部署中极其有用——可以在线灰度更新拥塞控制策略,而不需要重启连接。
七、eBPF 网络 helper 函数速查
不同钩子类型可用的 helper 函数差异巨大。以下是各钩子的关键 helper:
7.1 TC BPF 关键 helper
| helper | 功能 | 源码位置 |
|---|---|---|
bpf_skb_store_bytes() |
修改包数据 | net/core/filter.c |
bpf_l3_csum_replace() |
L3 校验和增量更新 | net/core/filter.c |
bpf_l4_csum_replace() |
L4 校验和增量更新 | net/core/filter.c |
bpf_redirect() |
重定向到指定 ifindex | net/core/filter.c |
bpf_redirect_peer() |
重定向到 veth 对端(跳过 netns) | net/core/filter.c |
bpf_redirect_neigh() |
重定向并执行邻居查找 | net/core/filter.c |
bpf_skb_change_head() |
扩展/缩减头部空间 | net/core/filter.c |
bpf_skb_adjust_room() |
调整 room(封装/解封装) | net/core/filter.c |
bpf_ct_lookup() |
conntrack 查找(6.1+) | net/netfilter/nf_bpf_link.c |
bpf_fib_lookup() |
FIB 路由查找 | net/core/filter.c |
7.2 socket ops 关键 helper
| helper | 功能 |
|---|---|
bpf_setsockopt() |
设置 socket 选项(含 BPF 专用选项) |
bpf_getsockopt() |
读取 socket 选项 |
bpf_sock_map_update() |
将 socket 加入 sockmap |
bpf_sock_hash_update() |
将 socket 加入 sockhash |
bpf_reserve_hdr_opt() |
预留 TCP 头部选项空间 |
bpf_store_hdr_opt() |
写入自定义 TCP 选项 |
bpf_load_hdr_opt() |
读取 TCP 头部选项 |
7.3 sk_msg/sk_skb 关键 helper
| helper | 功能 |
|---|---|
bpf_msg_redirect_map() |
sk_msg 重定向到 sockmap 中的 socket |
bpf_msg_redirect_hash() |
sk_msg 重定向到 sockhash 中的 socket |
bpf_sk_redirect_map() |
sk_skb 重定向到 sockmap |
bpf_sk_redirect_hash() |
sk_skb 重定向到 sockhash |
bpf_msg_apply_bytes() |
设置裁决应用的字节数 |
bpf_msg_cork_bytes() |
设置 cork 聚合字节数 |
bpf_msg_pull_data() |
将数据拉入线性区域 |
八、可观测性实战
8.1 追踪 TC BPF 程序执行
# 查看当前系统所有 TC BPF 程序
tc filter show dev eth0 ingress
tc filter show dev eth0 egress
# bpftrace:追踪 TC 分类器调用和返回值
bpftrace -e '
kprobe:tcf_classify {
@start[tid] = nsecs;
}
kretprobe:tcf_classify /@ start[tid]/ {
@tc_latency_ns = hist(nsecs - @start[tid]);
delete(@start[tid]);
}
'8.2 追踪 sock_ops 回调
# bpftrace:追踪所有 sock_ops 操作码
bpftrace -e '
kprobe:__cgroup_bpf_run_filter_sock_ops {
$sock_ops = (struct bpf_sock_ops_kern *)arg1;
@ops[str($sock_ops->op)] = count();
}
'
# 追踪 TCP 建连时的 sock_ops 调用
bpftrace -e '
kprobe:tcp_call_bpf {
$op = arg1;
@tcp_bpf_ops[$op] = count();
}
'8.3 追踪 sockmap 重定向
# bpftrace:追踪 sk_msg 重定向事件
bpftrace -e '
kprobe:sk_psock_msg_verdict {
@msg_verdict = count();
}
kprobe:tcp_bpf_sendmsg_redir {
@msg_redir = count();
}
'
# 查看 sockmap 中的 socket 数量
bpftool map dump id <MAP_ID>8.4 查看已加载的 BPF 程序
# 列出所有已加载的 BPF 程序
bpftool prog list
# 查看 TC BPF 程序详情
bpftool prog show id <PROG_ID>
# 查看 cgroup BPF 程序
bpftool cgroup tree /sys/fs/cgroup/
# 查看 struct_ops 程序
bpftool struct_ops list
# 查看 sockmap 关联的程序
bpftool map show id <MAP_ID>九、性能考量与常见陷阱
9.1 钩子选择决策树
需要最高包处理性能?
├── 是 → XDP
│ ├── 需要访问 L4 以上?→ TC BPF
│ └── 纯 L2/L3 操作?→ XDP
└── 否 → 需要 socket 感知?
├── 是 → 需要 TCP 生命周期控制?
│ ├── 是 → sock_ops
│ └── 否 → 需要 socket 间重定向?
│ ├── 是 → sk_msg / sk_skb + sockmap
│ └── 否 → cgroup BPF
└── 否 → TC BPF
9.2 常见性能陷阱
TC BPF
尾调用开销:每次尾调用(bpf_tail_call())有约
20-30ns 的开销。如果在关键路径上使用超过 3
级尾调用,累积延迟可能超过直接实现的优势。
sockmap 锁竞争:在高并发场景下,sockmap
的查找和重定向涉及 sk_psock 的
ingress_lock。如果大量连接同时重定向到同一个目标
socket,锁竞争会成为瓶颈。
cgroup BPF 层级遍历:cgroup BPF 程序需要遍历 cgroup 层级执行所有有效程序。深层嵌套的 cgroup(如 Kubernetes 中 pod cgroup 嵌套 3-4 级)会增加每包的 BPF 执行次数。
bpf_fib_lookup
与路由缓存:bpf_fib_lookup() helper 在
TC BPF 中做 FIB
查找,但结果不会缓存。对每个包都调用
bpf_fib_lookup()
的性能远低于让包走正常的路由路径(有 nexthop 缓存)。应该在
BPF map 中手动缓存查找结果。
9.3 关键 sysctl 与配置
| 参数 | 默认值 | 说明 |
|---|---|---|
/proc/sys/net/core/bpf_jit_enable |
1 | BPF JIT 编译(必须开启) |
/proc/sys/net/core/bpf_jit_harden |
0 | JIT hardening(2=全开,性能有损) |
/proc/sys/kernel/unprivileged_bpf_disabled |
2 | 非特权 BPF(1=禁止,生产推荐) |
RLIMIT_MEMLOCK / memcg |
因发行版而异 | BPF map 内存限额 |
十、总结
eBPF 在 Linux 网络子系统中的钩子体系已经形成了完整的可编程层:
- XDP:驱动层,最高性能,无
sk_buff开销 - TC BPF:
sk_buff层,功能最全面,支持 conntrack 和复杂策略 - cgroup BPF:基于进程层级的策略控制,适合多租户环境
- sock_ops:TCP 生命周期控制,适合连接级调优和 sockmap 注册
- sk_msg/sk_skb:socket 层重定向,实现 sidecar-free 的服务网格
- struct_ops:最强扩展机制,可替换内核子系统回调(如拥塞控制)
这些钩子的组合使用构成了现代云原生网络数据面的基础。下一篇将从实践角度出发,深入内核网络追踪工具箱——bpftrace、perf 和 ftrace 的实战技巧。
参考文献
- Linux 内核源码
include/linux/filter.h:struct bpf_sock_ops_kern定义 - Linux 内核源码
include/linux/skmsg.h:struct sk_msg、struct sk_psock定义 - Linux 内核源码
include/linux/bpf-cgroup.h:cgroup BPF 宏定义 - Linux 内核源码
include/linux/bpf_mprog.h:TC bpf_mprog 多程序链 - Linux 内核源码
include/linux/bpf.h:1700:struct bpf_struct_ops定义 - Linux 内核源码
include/uapi/linux/bpf.h:6801:BPF_SOCK_OPS_*操作码枚举 - Linux 内核源码
include/net/tcp.h:2618:tcp_call_bpf()实现
下一篇:内核网络追踪工具箱:bpftrace/perf/ftrace 实战
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Linux 网络子系统深度拆解】XDP 内核实现:在驱动层重编程网络栈
从内核源码拆解 XDP 的完整实现:xdp_buff 数据结构、驱动级钩子、五种动作路径、AF_XDP 零拷贝通道、devmap/cpumap/xskmap 重定向机制、多缓冲区支持,以及 bpftrace 可观测实战。
【Kubernetes 网络深度系列】eBPF 网络子系统的演进:从 hook 到可编程网络操作系统
从 2014 年 socket filter 到可编程拥塞控制和 SmartNIC offload,eBPF 网络能力的完整演化史
【Linux 网络子系统深度拆解】网络丢包定位:从 drop_monitor 到 kfree_skb 追踪
从内核源码拆解 Linux 网络丢包追踪的完整体系:kfree_skb tracepoint 与 80+ 种 drop_reason 枚举、drop_monitor netlink 子系统、dropwatch 工具、perf 丢包记录、bpftrace 丢包聚合脚本,以及生产环境常见丢包点速查表。
【Linux 网络子系统深度拆解】内核网络追踪工具箱:bpftrace/perf/ftrace 实战
从内核 tracepoint 定义出发,系统讲解 bpftrace、perf、ftrace 三大工具在网络诊断中的实战用法:TCP 重传根因分析、softirq 延迟定位、收发包路径延迟剖析、conntrack 表满监控、per-function 火焰图,以及各工具的适用场景与性能开销对比。