土法炼钢兴趣小组的算法知识备份

【Linux 网络子系统深度拆解】eBPF 网络钩子全景:TC/XDP/socket/cgroup

文章导航

分类入口
linuxnetworking
标签入口
#kernel#eBPF#TC-BPF#XDP#socket-ops#cgroup-BPF#sockmap#sk_msg#struct_ops#bpftrace

目录

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 网络钩子全景

一个典型的收包路径上,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 负载均衡)。

典型应用场景:

/* 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()

4.4 自定义 TCP 头部选项

Linux 5.10+ 引入了三个操作码用于 BPF 程序读写自定义 TCP 头部选项:

  1. HDR_OPT_LEN_CB:在构建 TCP 头部时被调用,BPF 程序通过 bpf_reserve_hdr_opt() 预留选项空间
  2. WRITE_HDR_OPT_CB:在写入 TCP 头部时被调用,BPF 程序通过 bpf_store_hdr_opt() 写入自定义选项
  3. 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_msgsk_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 收到数据时被调用,有两种模式:

  1. stream_parser + stream_verdict:先由 stream_parser 确定消息边界(返回消息长度),再由 stream_verdict 做裁决。基于 strparser(stream parser)框架
  2. skb_verdict:直接对收到的 sk_buff 做裁决,不需要消息边界解析

skb_verdict 模式更简单也更常用。当 socket 收到数据时,sk_psocksaved_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 流量代理:

  1. sock_ops BPF 程序在 ACTIVE_ESTABLISHED_CBPASSIVE_ESTABLISHED_CB 时,将 socket 加入 sockhash map
  2. sk_msg BPF 程序在 sendmsg() 时查找 sockhash,找到对端 socket 后直接重定向
  3. 数据完全在内核中从一个 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 拥塞控制的流程:

  1. 用 BPF C 代码实现 tcp_congestion_ops 的各个回调
  2. 通过 BPF_MAP_TYPE_STRUCT_OPS map 加载到内核
  3. 内核验证器检查每个回调的类型签名和安全性
  4. 通过 bpf_struct_ops_get() / bpf_struct_ops_put() 管理生命周期
  5. 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_psockingress_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 网络子系统中的钩子体系已经形成了完整的可编程层:

这些钩子的组合使用构成了现代云原生网络数据面的基础。下一篇将从实践角度出发,深入内核网络追踪工具箱——bpftrace、perf 和 ftrace 的实战技巧。

参考文献

  1. Linux 内核源码 include/linux/filter.hstruct bpf_sock_ops_kern 定义
  2. Linux 内核源码 include/linux/skmsg.hstruct sk_msgstruct sk_psock 定义
  3. Linux 内核源码 include/linux/bpf-cgroup.h:cgroup BPF 宏定义
  4. Linux 内核源码 include/linux/bpf_mprog.h:TC bpf_mprog 多程序链
  5. Linux 内核源码 include/linux/bpf.h:1700struct bpf_struct_ops 定义
  6. Linux 内核源码 include/uapi/linux/bpf.h:6801BPF_SOCK_OPS_* 操作码枚举
  7. Linux 内核源码 include/net/tcp.h:2618tcp_call_bpf() 实现

上一篇XDP 内核实现:在驱动层重编程网络栈

下一篇内核网络追踪工具箱:bpftrace/perf/ftrace 实战

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .