当我们在Linux内核中部署eBPF程序时,经常会遇到这样的需求:同一个eBPF字节码需要附加到多个不同的网络钩子上,例如多个tc队列、多个cgroup或者不同的netfilter挂载点。程序逻辑基本一致,但在某些分支上需要根据具体的附加位置执行不同策略。传统做法是编写多个几乎相同的eBPF程序,或者通过映射(map)来传递上下文信息,但这些方法要么增加维护成本,要么引入额外的运行时开销。内核从5.15版本开始为部分程序类型提供了attach cookie机制,允许用户在加载程序时指定一个自定义的整数值,程序运行期间通过bpf_get_attach_cookie辅助函数读取该值,从而轻松识别自己所在的附加点。

attach cookie本质上是一个用户态可控的元数据,它被保存在内核的bpf_prog_aux结构中,与程序实例绑定。对于支持该特性的程序类型,同一个bpf_prog可以多次附加到不同位置,每次附加时都允许携带不同的cookie值。这样,eBPF程序在运行时调用bpf_get_attach_cookie就能获取到当前执行上下文对应的cookie,而不需要修改程序本身或者额外查询映射。下面我们深入探讨这一机制的工作原理和具体用法。
理解attach cookie的工作机制
bpf_get_attach_cookie是内核提供的eBPF辅助函数,其函数签名为__u64 bpf_get_attach_cookie(void *ctx)。对于大多数程序类型,参数ctx可以传入当前程序的上下文指针,例如tc程序的struct __sk_buff *,也可以传入空指针。该函数返回一个64位无符号整数,表示用户态在附加程序时设置的cookie值。如果调用者不支持attach cookie或者用户态没有显式设置,返回值通常为0。
设置cookie的方式因程序类型而异。对于基于cgroup的BPF程序(如BPF_PROG_TYPE_CGROUP_SKB),用户态通过BPF_PROG_ATTACH命令附加程序时,可以在union bpf_attr的attach_cookie字段中指定值。对于tc BPF程序,由于使用netlink接口附加,早期内核并不直接支持cookie,但从5.15版本开始,可以通过tc工具的新选项或者使用libbpf的封装来设置。libbpf提供了bpf_program__set_attach_cookie函数,在加载对象前设置程序级别的默认cookie,也可以在bpf_prog_attach调用时通过struct bpf_prog_attach_opts中的attach_cookie字段覆盖。
与bpf_get_attach_type相比,attach cookie提供了更大的灵活性。bpf_get_attach_type返回的是内核预定义的枚举值(如BPF_CGROUP_INET_EGRESS),它只能区分大类的附加类型,而无法区分同一类型下的不同实例。attach cookie则完全由用户自定义,可以是一个容器ID、接口索引、策略版本号等任意语义化数值。内核本身并不关心cookie的具体含义,它只负责存储和返回,这为上层应用提供了极大的设计空间。
从内核实现角度看,每次bpf_prog_attach操作都会创建一个bpf_prog_aux的引用,并在这个引用中保存attach_cookie字段。当eBPF程序运行并调用辅助函数时,内核通过当前正在执行的bpf_prog实例找到对应的bpf_prog_aux,读取其中的cookie值。需要注意的是,如果一个程序被附加到多个位置且每个位置的cookie不同,内核在运行时会自动选择与当前执行上下文匹配的那一个,这得益于程序运行时携带的bpf_prog对象引用是附加时创建的副本。
使用bpf_get_attach_cookie的完整示例
下面通过一个tc egress方向的BPF程序来演示cookie的实际用法。假设我们有两个网络接口eth0和eth1,它们都挂载同一个tc BPF程序用于流量统计,但希望根据接口不同将统计结果写入不同的map条目。传统做法需要分别加载两个程序或者通过外部传入接口信息,而使用attach cookie后,内核态代码只需一次判断即可区分。
首先编写内核态eBPF程序tc_egress.c:
#include <linux/bpf.h>
#include <bpf/bpf_helpers.h>
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 2);
__type(key, __u32);
__type(value, __u64);
} pkt_count SEC(".maps");
SEC("tc")
int tc_egress(struct __sk_buff *skb) {
__u64 cookie = bpf_get_attach_cookie(skb);
__u32 idx;
if (cookie == 100) {
idx = 0; /* eth0 */
} else if (cookie == 200) {
idx = 1; /* eth1 */
} else {
return TC_ACT_OK;
}
__u64 *counter = bpf_map_lookup_elem(&pkt_count, &idx);
if (counter) {
__sync_fetch_and_add(counter, 1);
}
return TC_ACT_OK;
}
char LICENSE[] SEC("license") = "GPL";
在这个程序中,bpf_get_attach_cookie(skb)返回附加时设置的cookie值。如果cookie等于100,说明数据包来自eth0的egress方向;如果等于200则来自eth1。程序使用一个percpu数组map pkt_count,根据cookie选择不同的索引来累加计数。这样同一个程序就能同时服务于两个接口,而无需重复加载。
接下来是用户态加载逻辑,使用libbpf来设置cookie并附加程序。以下代码片段展示了核心步骤:
#include <bpf/libbpf.h>
#include <linux/bpf.h>
#include <net/if.h>
int main() {
struct bpf_object *obj = bpf_object__open_file("tc_egress.o", NULL);
if (!obj) return 1;
struct bpf_program *prog = bpf_object__find_program_by_name(obj, "tc_egress");
if (!prog) {
bpf_object__close(obj);
return 1;
}
/* 设置程序默认附加cookie,也可以后续在attach时覆盖 */
bpf_program__set_attach_cookie(prog, 100);
if (bpf_object__load(obj)) {
bpf_object__close(obj);
return 1;
}
int prog_fd = bpf_program__fd(prog);
int ifindex_eth0 = if_nametoindex("eth0");
int ifindex_eth1 = if_nametoindex("eth1");
/* 附加到eth0,cookie=100 */
DECLARE_LIBBPF_OPTS(bpf_tc_opts, opts0,
.handle = 1,
.priority = 1,
.prog_fd = prog_fd,
);
if (bpf_tc_attach(ifindex_eth0, BPF_TC_EGRESS, &opts0)) {
return 1;
}
/* 附加到eth1,cookie=200 */
DECLARE_LIBBPF_OPTS(bpf_tc_opts, opts1,
.handle = 2,
.priority = 1,
.prog_fd = prog_fd,
.attach_cookie = 200,
);
if (bpf_tc_attach(ifindex_eth1, BPF_TC_EGRESS, &opts1)) {
return 1;
}
/* 保持运行 */
pause();
return 0;
}
在这个例子中,程序在bpf_object__open_file之后、bpf_object__load之前通过bpf_program__set_attach_cookie设置了默认cookie为100。当附加到eth1时,通过bpf_tc_opts中的attach_cookie字段覆盖为200。libbpf会在内部调用BPF_PROG_ATTACH系统调用时传递相应的cookie值。注意tc BPF的附加机制从5.15内核开始才支持cookie,libbpf需要较新版本(建议1.0以上)。
完成加载后,我们可以通过查看map内容来验证cookie是否生效。在内核态程序中,bpf_get_attach_cookie返回的确实是用户态设置的值,程序据此正确地将计数累加到不同索引。这个示例展示了attach cookie在网络钩子场景中的实际效用——同一个程序体,不同附加点呈现不同行为。
适用场景与注意事项
attach cookie特别适合那些需要在多个实例间共享同一份eBPF字节码,但每个实例又有细微差异的场景。除了上述的接口区分外,常见的应用还包括:在cgroup级别实施网络策略时,为每个容器或pod分配一个唯一cookie,eBPF程序根据cookie查找对应的规则集;在XDP程序中,为不同的网卡队列设置不同cookie以实施差异化限速;在socket过滤器中,根据附加的socket归属来执行不同的审计逻辑。由于cookie是一个静态的整数值,它比动态查询map更加高效,避免了额外的查表开销。
使用attach cookie时需要注意几个关键点。第一,并非所有程序类型都支持attach cookie,目前支持的主要包括cgroup系列、tc、sk_lookup、flow dissector等,XDP程序在较新内核中也可以通过bpf_xdp_attach的选项设置cookie。第二,cookie的位宽在不同程序类型中可能不同,有些早期实现只支持32位,但bpf_get_attach_cookie统一返回64位,高位补零。第三,如果用户态忘记设置cookie,返回值默认为0,这可能导致程序误判,因此建议在代码中显式处理cookie为0的情况。第四,同一bpf_prog附加到多个位置时,每次附加的cookie是独立存储的,修改其中一个附加点的cookie不会影响其他附加点。
另一个常见的误区是将attach cookie与map索引直接关联。虽然示例中我们使用cookie来索引map,但这要求cookie值必须是连续的或者预先定义的。如果cookie值动态变化且范围很大,直接作为map索引会浪费内存,此时更好的做法是将cookie作为key的一部分,搭配hash map存储精细化的上下文信息。此外,attach cookie在程序运行期间是只读的,无法通过eBPF程序修改,如果需要动态调整不同附加点的行为,仍然需要借助map或全局变量。
总的来说,bpf_get_attach_cookie为eBPF网络钩子程序提供了一种轻量级的附加点标识方案,有效减少了程序重复加载的负担,提高了代码复用率。结合libbpf的现代封装,开发者可以很方便地在用户态控制每个附加点的行为差异。随着内核的持续演进,这一机制有望在更多程序类型中得到支持,成为eBPF程序多实例部署的标准实践之一。
eBPFbpf_get_attach_cookie网络钩子修改时间:2026-08-25 21:35:27