自 5.6 kernel bpf dispatcher 被引入到 Linux 内核中。

到目前 6.9 kernel,仍然只有 XDP 程序在使用 bpf dispatcher。

bpf dispatcher 的引入,是为了解决间接调用叠加 retpoline 带来的性能损耗问题。

bpf dispatcher 简介

bpf dispatcher 是一个多路分支代码生成器,用于生成多个代码分支的跳转表。

用 C 代码来描述,bpf dispatcher 的代码逻辑大概如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct bpf_dispatcher {
    struct mutex lock;
    int num;
    void *jump_table[48];
};

static struct bpf_dispatcher xdp_dispatcher = {};

static __always_inline void
bpf_dispatcher(struct bpf_prog *prog)
{
    for (int i = 0; i < xdp_dispatcher.num; i++) {
        if (prog == xdp_dispatcher.jump_table[i]) {
            jump(xdp_dispatcher.jump_table[i]);
        }
    }
}

其中:

  1. xdp_dispatcher 是一个全局变量,用于保存所有 XDP 程序的跳转表。
  2. 在向网络设备挂载 XDP 程序时,会将 XDP 程序添加到 xdp_dispatcher 的跳转表中。
  3. jump 对应 JMP 这个机器指令。

bpf dispatcher 源码解析

在 kernel 里,bpf dispatcher 的实现比上面的代码片段复杂一些,但是原理是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ${KERNEL}/include/linux/bpf.h

#define BPF_DISPATCHER_MAX 48 /* Fits in 2048B */

struct bpf_dispatcher_prog {
    struct bpf_prog *prog;
    refcount_t users;
};

struct bpf_dispatcher {
    /* dispatcher mutex */
    struct mutex mutex;
    void *func;
    struct bpf_dispatcher_prog progs[BPF_DISPATCHER_MAX];
    int num_progs;
    void *image;
    void *rw_image;
    u32 image_off;
    struct bpf_ksym ksym;
#ifdef CONFIG_HAVE_STATIC_CALL
    struct static_call_key *sc_key;
    void *sc_tramp;
#endif
};

在挂载了一个 XDP 程序后,使用 drgn 工具可以看到 bpf_dispatcher_xdp 的结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
>>> prog.variable('bpf_dispatcher_xdp')
(struct bpf_dispatcher){
    // ...
    .num_progs = (int)1,
    .image = (void *)0xffffffffc037aa00,
    .rw_image = (void *)0xffffffffc104c000,
    .image_off = (u32)0,
    // ...
    .sc_key = (struct static_call_key *)__SCK__bpf_dispatcher_xdp_call+0x0 = 0xffffffff86788010,
    .sc_tramp = (void *)__SCT__bpf_dispatcher_xdp_call+0x0 = 0xffffffff8535a9b0,
}

关注其中的 image 字段:

1
2
3
4
5
6
7
8
root@leon-vm ~# gdb -q -c /proc/kcore -ex 'x/18i 0xffffffffc037aa00' -ex 'quit'
   0xffffffffc037aa00:  cmp    $0xffffffffc0360788,%rdx
   0xffffffffc037aa07:  je     0xffffffffc0360788
   0xffffffffc037aa0d:  jmp    *%rdx
   0xffffffffc037aa0f:  int3

root@leon-vm ~# grep ffffffffc0360788 /proc/kallsyms
ffffffffc0360788 t bpf_prog_3b185187f1855c4c_dummy  [bpf] # 正是刚才运行起来的 XDP bpf prog

static call?

这里为什么会用到 STATIC_CALL?

这是为了解决 bpf dispatcher 滥用 ftrace __fentry__ 的问题;即不能同时使用 ftrace 了。

最终,kernel 在调用 bpf_dispatcher_xdp_func() 时,实际调用的是其 STATIC_CALL 对应的 trampoline。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
root@leon-vm ~/kernel-bpf (bpf-next_base)# grep dispatcher /proc/kallsyms
ffffffff8505a510 t bpf_dispatcher_nop_func
ffffffff85063630 T bpf_dispatcher_xdp_func

root@leon-vm ~# gdb -q -c /proc/kcore -ex 'x/18i 0xffffffff85063630' -ex 'quit'
   0xffffffff85063630:  nopl   0x0(%rax,%rax,1)
   0xffffffff85063635:  push   %rbp
   0xffffffff85063636:  mov    %rsp,%rbp
   0xffffffff85063639:  call   0xffffffffc037aa00 # 对应 xdp dispatcher 的 trampoline image
   0xffffffff8506363e:  pop    %rbp
   0xffffffff8506363f:  xor    %edx,%edx
   0xffffffff85063641:  xor    %esi,%esi
   0xffffffff85063643:  xor    %edi,%edi
   0xffffffff85063645:  ret

总结

bpf dispatcher 是一个多路分支代码生成器,用于生成多个代码分支的跳转表,用来解决间接调用叠加 retpoline 带来的性能损耗问题。

在 kernel 里,实际采用了 2 层 trampoline,即 STATIC_CALL trampoline + bpf dispatcher trampoline。

可以通过 drgngdb 工具来查看 bpf dispatcher 的实现细节。


后面,将会在《XDP 进阶手册》里详细讲解 bpf dispatcher 的源码细节。

想要更多 XDP 资料,请加入「eBPF Talk」知识星球来学习《XDP 进阶手册》吧。

eBPF Talk 知识星球