背景

1992 年的 USENIX 会议上,Steven McCanne 和 Van Jacobson 发布的论文

BPF的特性

  • 内核态引入一个新的虚拟机,所有指令都在内核虚拟机中运行
  • 用户态使用 BPF 字节码来定义过滤表达式,然后传递给内核,由内核虚拟机解释执行。

eBPF 的诞生是 BPF 技术的一个转折点,使得 BPF 不再仅限于网络栈,而是成为内核的一个顶级子系统

开发过程

  1. 使用 C 语言开发一个 eBPF 程序;
  2. 借助 LLVM 把 eBPF 程序编译成 BPF 字节码
  3. 通过 bpf 系统调用,把 BPF 字节码提交给内核
  4. 内核验证并运行 BPF 字节码,并把相应的状态保存到 BPF 映射中
  5. 用户程序通过 BPF 映射查询 BPF 字节码的运行状态。

BCC(BPF Compiler Collection)

  • BCC 是一个 BPF 编译器集合,包含了用于构建 BPF 程序的编程框架和库,并提供了大量可以直接使用的工具
  • 使用 BCC 的好处是,它把上述的 eBPF 执行过程通过内置框架抽象了起来
  • 并提供了 Python、C++ 等编程语言接口
  • 可以直接通过 Python 语言去跟 eBPF 的各种事件和数据进行交互

BPF程序可以利用 BPF 映射(map)进行存储,而用户程序通常也需要通过 BPF 映射同运行在内核中的 BPF 程序进行交互
在性能观测中,BPF 程序收集内核运行状态存储在映射中,用户程序再从映射中读出这些状态

内核版本对应的特性

例子

hello-world

安装 Ubuntu 24.04.2 LTS
系统内核

1
2
3
Linux version 6.11.0-25-generic (buildd@lcy02-amd64-027) (x86_64-linux-gnu-gcc-13 
(Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0, GNU ld (GNU Binutils for Ubuntu) 2.42) 
#25~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 15 17:20:50 UTC 2

安装相关的工具

1
2
3
sudo apt update
sudo apt install -y libelf-dev libbpf-dev python3-bpfcc bpfcc-tools \
clang llvm linux-headers-$(uname -r) build-essential

​​Key packages​​:

  • libelf-dev and libbpf-dev: For eBPF program compilation.
  • python3-bpfcc and bpfcc-tools: BCC framework tools for eBPF development.
  • linux-headers-$(uname -r): Kernel headers matching your kernel version

c 代码

1
2
3
4
int hello_world(void *ctx) {
    bpf_trace_printk("Hello, World!");
    return 0;
}

python 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/usr/bin/env python3
# 1) import bcc library
from bcc import BPF

# 2) load BPF program
b = BPF(src_file="hello.c")
# 3) attach kprobe
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
# 4) read and print /sys/kernel/debug/tracing/trace_pipe
b.trace_print()

以上代码工作过程:

  1. 处导入了 BCC  库的 BPF 模块,以便接下来调用
  2. 处调用 BPF() 加载第一步开发的 BPF 源代码
  3. 处将 BPF 程序挂载到内核探针(简称 kprobe),其中 do_sys_openat2() 是系统调用 openat() 在内核中的实现
  4. 处则是读取内核调试文件 /sys/kernel/debug/tracing/trace_pipe 的内容,并打印到标准输出中。

当然,也可以将 c 代码嵌入到 python 中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
from bcc import BPF

# eBPF program in C
program = r"""
int hello(void *ctx) {
    bpf_trace_printk("Hello eBPF World!");
    return 0;
}
"""

# Load the eBPF program
b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

# Print trace output
b.trace_print()

运行

1
python3 hello.py

结果

 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
b'           <...>-3726    [000] ...21  1383.099335: bpf_trace_printk: Hello, World!'
b'           <...>-558     [001] ...21  1383.215927: bpf_trace_printk: Hello, World!'
b'           <...>-558     [001] ...21  1383.216014: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928281: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928385: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928393: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928399: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928404: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928409: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928431: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928469: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928475: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928480: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928485: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1383.928491: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.178974: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179075: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179082: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179088: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179093: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179098: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179118: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179156: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179161: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179167: bpf_trace_printk: Hello, World!'
b'    systemd-oomd-549     [000] ...21  1385.179172: bpf_trace_printk: Hello, World!'

输出的格式可由 /sys/kernel/debug/tracing/trace_options  来修改。前面这个默认的输出中,每个字段的含义如下所示:

  • <…>-3726 表示进程的名字和 PID;
  • [000] 表示 CPU 编号;
  • … 表示一系列的选项;
  • 1383.099335 表示时间戳;
  • bpf_trace_printk 表示函数名;
  • 最后的 “Hello, World!” 就是调用 bpf_trace_printk() 传入的字符串

改进

trace-open.c 代码

 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
// 包含头文件
#include <uapi/linux/openat2.h>
#include <linux/sched.h>

// 定义数据结构
struct data_t {
  u32 pid;
  u64 ts;
  char comm[TASK_COMM_LEN];
  char fname[NAME_MAX];
};

// 定义性能事件映射
BPF_PERF_OUTPUT(events);

// 定义kprobe处理函数
int hello_world(struct pt_regs *ctx, int dfd, const char __user * filename, struct open_how *how) {
  struct data_t data = { };

  // 获取PID和时间
  data.pid = bpf_get_current_pid_tgid();
  data.ts = bpf_ktime_get_ns();

  // 获取进程名
  if (bpf_get_current_comm(&data.comm, sizeof(data.comm)) == 0) {
    bpf_probe_read(&data.fname, sizeof(data.fname), (void *)filename);
  }

  // 提交性能事件
  events.perf_submit(ctx, &data, sizeof(data));
  return 0;
}

reference_guide

trace-open.py 代码

 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
from bcc import BPF

# 1) load BPF program
b = BPF(src_file="trace-open.c")
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")

# 2) print header
print("%-18s %-16s %-6s %-16s" % ("TIME(s)", "COMM", "PID", "FILE"))

# 3) define the callback for perf event
start = 0
def print_event(cpu, data, size):
    global start
    event = b["events"].event(data)
    if start == 0:
            start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-16s %-6d %-16s" % (time_s, event.comm, event.pid, event.fname))

# 4) loop with callback to print_event
b["events"].open_perf_buffer(print_event)
while 1:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

执行结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
TIME(s)            COMM             PID    FILE
0.000000000        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-120.slice/[email protected]/memory.pressure'
0.000118287        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-120.slice/[email protected]/memory.current'
0.000126786        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-120.slice/[email protected]/memory.min'
0.000133319        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-120.slice/[email protected]/memory.low'
0.000139539        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-120.slice/[email protected]/memory.swap.current'
0.000145635        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-120.slice/[email protected]/memory.stat'
0.000166540        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-1000.slice/[email protected]/memory.pressure'
0.000205800        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-1000.slice/[email protected]/memory.current'
0.000212132        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-1000.slice/[email protected]/memory.min'
0.000219351        b'systemd-oomd'  549    b'/sys/fs/cgroup/user.slice/user-1000.slice/[email protected]/memory.low'

c 代码执行

完整的例子

 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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kprobes.h>

static int total_count = 0;

static int handler_pre(struct kprobe *p, struct pt_regs *regs) {
    total_count++;
    printk(KERN_INFO "kernel_clone called %d times\n", total_count);
    return 0;
}

static struct kprobe kp = {
    .symbol_name = "kernel_clone",
    .pre_handler = handler_pre,
};

static int __init kprobe_init(void) {
    return register_kprobe(&kp);
}

static void __exit kprobe_exit(void) {
    unregister_kprobe(&kp);
}

module_init(kprobe_init);
module_exit(kprobe_exit);
MODULE_LICENSE("GPL");

Makefile

1
2
3
4
5
6
7
8
obj-m += hello.o  # Rename to match your source file (hello.c)
KDIR := /lib/modules/$(shell uname -r)/build

all:
        $(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
        $(MAKE) -C $(KDIR) M=$(PWD) clean

安装必要的组件

1
2
3
sudo add-apt-repository ppa:ubuntu-toolchain-r/test
sudo apt update
sudo apt install gcc-13 g++-13

执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Compile
make

# Load module
sudo insmod kprobe_example.ko

# Verify logs
dmesg | grep "kernel_clone called"

# Unload module
sudo rmmod kprobe_example

输出结果

1
2
3
4
5
6
[ 9373.247008] kernel_clone called 1 times
[ 9380.299096] kernel_clone called 2 times
[ 9380.299303] kernel_clone called 3 times
[ 9404.546115] kernel_clone called 4 times
[ 9404.551322] kernel_clone called 5 times
[ 9404.551700] kernel_clone called 6 times

Comparison with BCC​

Aspect This Code BCC - Based Approach
Complexity Manual kernel module setup. Uses Python/BPF scripts for simplicity.
Execution Context Pure kernel space. Combines user - space (Python) and kernel - space (BPF).
Overhead Higher (requires module compilation). Lower (dynamic loading via BPF).
Flexibility Direct control over kprobes. Limited by BPF’s sandboxed environment

安全性

  • bcc是编译了限制的字节码
  • 之后又有校验,所以更安全
  • 而直接 kprobe 方式绕过了上述限制,直接注入到内核所以不安全

原理

执行过程

下图,来自 BPF Internals 解释

  • 第一个模块是  eBPF 辅助函数。它提供了一系列用于 eBPF 程序与内核其他模块进行交互的函数。这些函数并不是任意一个 eBPF 程序都可以调用的,具体可用的函数集由 BPF 程序类型决定
  • 第二个模块是  eBPF 验证器。它用于确保 eBPF 程序的安全。验证器会将待执行的指令创建为一个有向无环图(DAG),确保程序中不包含不可达指令;接着再模拟指令的执行过程,确保不会执行无效指令
  • 第三个模块是由  11 个 64 位寄存器、一个程序计数器和一个 512 字节的栈组成的存储模块。这个模块用于控制 eBPF 程序的执行。其中,R0 寄存器用于存储函数调用和 eBPF 程序的返回值,这意味着函数调用最多只能有一个返回值;R1-R5 寄存器用于函数调用的参数,因此函数调用的参数最多不能超过 5 个;而 R10 则是一个只读寄存器,用于从栈中读取数据
  • 第四个模块是即时编译器,它将 eBPF 字节码编译成本地机器指令,以便更高效地在内核中执行
  • 第五个模块是  BPF 映射(map),它用于提供大块的存储。这些存储可被用户空间程序用来进行访问,进而控制 eBPF 程序的运行状态。

指令

BPF 指令
查询 hello world 程序

1
2
bpftool prog list | grep hello
60: kprobe  name hello_world  tag 38dd440716c4900f  gpl

这里的 60 就是 id,然后查询

1
sudo bpftool prog dump xlated id 60

结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int hello_world(void * ctx):
; int hello_world(void *ctx)
   0: (b7) r1 = 33
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
   1: (6b) *(u16 *)(r10 -4) = r1
   2: (b7) r1 = 1684828783
   3: (63) *(u32 *)(r10 -8) = r1
   4: (18) r1 = 0x57202c6f6c6c6548
   6: (7b) *(u64 *)(r10 -16) = r1
   7: (bf) r1 = r10
;
   8: (07) r1 += -16
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
   9: (b7) r2 = 14
  10: (85) call bpf_trace_printk#-118688
; return 0;
  11: (b7) r0 = 0
  12: (95) exit

解释

  • 第一部分,冒号前面的数字 0-12 ,代表 BPF 指令行数
  • 第二部分,括号中的 16 进制数值,表示 BPF 指令码。,比如第 0 行的 0xb7 表示为 64 位寄存器赋值
  • 第三部分,括号后面的部分,就是 BPF 指令的伪代码。

具体含义参考 IOVisor BPF 文档

这些 BPF 指令的含义:

  • 第 0-8 行,借助 R10 寄存器从栈中把字符串 “Hello, World!” 读出来,并放入 R1 寄存器中
  • 第 9 行,向 R2 寄存器写入字符串的长度 14(即代码注释里面的 sizeof(_fmt) )
  • 第 10 行,调用 BPF 辅助函数 bpf_trace_printk 输出字符串
  • 第 11 行,向 R0 寄存器写入 0,表示程序的返回值是 0
  • 最后一行,程序执行成功退出。

总结

  • 这些指令先通过 R1 和 R2 寄存器设置了 bpf_trace_printk 的参数
  • 然后调用 bpf_trace_printk 函数输出字符串,最后再通过 R0 寄存器返回成功

BPF 指令

BPF指令加载到内核后,jit 会编译成机器指令,x86的类似于

 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
# bpftool prog dump jited id 89
int hello_world(void * ctx):
bpf_prog_38dd440716c4900f_hello_world:
; int hello_world(void *ctx)
   0:  nopl   0x0(%rax,%rax,1)
   5:  xchg   %ax,%ax
   7:  push   %rbp
   8:  mov    %rsp,%rbp
   b:  sub    $0x10,%rsp
  12:  mov    $0x21,%edi
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
  17:  mov    %di,-0x4(%rbp)
  1b:  mov    $0x646c726f,%edi
  20:  mov    %edi,-0x8(%rbp)
  23:  movabs $0x57202c6f6c6c6548,%rdi
  2d:  mov    %rdi,-0x10(%rbp)
  31:  mov    %rbp,%rdi
;
  34:  add    $0xfffffffffffffff0,%rdi
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
  38:  mov    $0xe,%esi
  3d:  call   0xffffffffd8c7e834
; return 0;
  42:  xor    %eax,%eax
  44:  leave
  45:  ret

strace 跟踪一下

1
strace -f -e trace=bpf python3 hello.py

打印的结果

 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
bpf(BPF_PROG_LOAD, 
  {
    prog_type=BPF_PROG_TYPE_SOCKET_FILTER, 
    insn_cnt=2, 
    insns=[
      {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}, 
      {code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}
      ], 
    license="GPL", 
    log_level=0, 
    log_size=0, 
    log_buf=NULL, 
    kern_version=KERNEL_VERSION(0, 0, 0), 
    prog_flags=0, prog_name="libbpf_nametest"}, 
    64) = 4

bpf(BPF_PROG_LOAD, 
  {
    prog_type=BPF_PROG_TYPE_KPROBE, 
    insn_cnt=14, 
    insns=[
      {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x21}, 
      {code=BPF_STX|BPF_H|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-16, imm=0}, 
      {code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x57204650}, 
      {code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x646c726f}, 
      {code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-24, imm=0},
      {code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x6c6c6548},
      {code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x4265206f},
      {code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-32, imm=0},
      {code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_10, off=0, imm=0},
      {code=BPF_ALU64|BPF_K|BPF_ADD, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0xffffffe0},
      {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_2, src_reg=BPF_REG_0, off=0, imm=0x12},
      {code=BPF_JMP|BPF_K|BPF_CALL, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x6},
      {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0},
      {code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}
    ], 
    license="GPL", 
    log_level=0, 
    log_size=0, 
    log_buf=NULL, 
    kern_version=KERNEL_VERSION(6, 11, 11), 
    prog_flags=0, 
    prog_name="hello", 
    prog_ifindex=0, 
    expected_attach_type=BPF_CGROUP_INET_INGRESS, 
    prog_btf_fd=3, 
    func_info_rec_size=8, 
    func_info=0x1b385950, 
    func_info_cnt=1, 
    line_info_rec_size=16, 
    line_info=0x1b385970, 
    line_info_cnt=5, 
    attach_btf_id=0, 
    attach_prog_fd=0, 
    fd_array=NULL}, 144) = 4

编程接口

bpf 系统调用

用户态负责 eBPF 程序的加载、事件绑定以及 eBPF 程序运行结果的汇总输出;内核态运行在 eBPF 虚拟机中,负责定制和控制系统的运行状态

用户态和内核态交互就必须用bpf系统调用

1
2
3
#include <linux/bpf.h>

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

bfp系统调用

BPF 系统调用接受三个参数:

  • cmd ,代表操作命令,比如上一讲中我们看到的 BPF_PROG_LOAD 就是加载 eBPF 程序
  • attr,代表 bpf_attr 类型的 eBPF 属性指针,不同类型的操作命令需要传入不同的属性参数
  • size ,代表属性的大小

6.14内核的 bpf

6.14支持的 command

 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
enum bpf_cmd {
	BPF_MAP_CREATE,
	BPF_MAP_LOOKUP_ELEM,
	BPF_MAP_UPDATE_ELEM,
	BPF_MAP_DELETE_ELEM,
	BPF_MAP_GET_NEXT_KEY,
	BPF_PROG_LOAD,
	BPF_OBJ_PIN,
	BPF_OBJ_GET,
	BPF_PROG_ATTACH,
	BPF_PROG_DETACH,
	BPF_PROG_TEST_RUN,
	BPF_PROG_RUN = BPF_PROG_TEST_RUN,
	BPF_PROG_GET_NEXT_ID,
	BPF_MAP_GET_NEXT_ID,
	BPF_PROG_GET_FD_BY_ID,
	BPF_MAP_GET_FD_BY_ID,
	BPF_OBJ_GET_INFO_BY_FD,
	BPF_PROG_QUERY,
	BPF_RAW_TRACEPOINT_OPEN,
	BPF_BTF_LOAD,
	BPF_BTF_GET_FD_BY_ID,
	BPF_TASK_FD_QUERY,
	BPF_MAP_LOOKUP_AND_DELETE_ELEM,
	BPF_MAP_FREEZE,
	BPF_BTF_GET_NEXT_ID,
	BPF_MAP_LOOKUP_BATCH,
	BPF_MAP_LOOKUP_AND_DELETE_BATCH,
	BPF_MAP_UPDATE_BATCH,
	BPF_MAP_DELETE_BATCH,
	BPF_LINK_CREATE,
	BPF_LINK_UPDATE,
	BPF_LINK_GET_FD_BY_ID,
	BPF_LINK_GET_NEXT_ID,
	BPF_ENABLE_STATS,
	BPF_ITER_CREATE,
	BPF_LINK_DETACH,
	BPF_PROG_BIND_MAP,
	BPF_TOKEN_CREATE,
	__MAX_BPF_CMD,
};

常用的命令:

BPF 命令 功能描述
BPF_MAP_CREATE 创建一个 BPF 映射
BPF_MAP_LOOKUP_ELEM BPF_MAP_UPDATE_ELEMBPF_MAP_DELETE_ELEM BPF_MAP_LOOKUP_AND_DELETE_ELEM BPF_MAP_GET_NEXT_KEY BPF 映射相关的操作命令,包括查找、更新、删除以及遍历等
BPF_PROG_LOAD 验证并加载 BPF 程序
BPF_PROG_ATTACHBPF_PROG_DETACH 把 BPF 程序挂载到内核事件上把 BPF 程序从内核事件上卸载
BPF_OBJ_PIN 把 BPF 程序或映射挂载到 sysfs 中的 /sys/fs/bpf 目录中(常用于保持 BPF 程序在内核中贮存)
BPF_OBJ_GET /sys/fs/bpf 目录中查找 BPF 程序
BPF_BTF_LOAD 验证并加载 BTF 信息

辅助函数

辅助函数

  • eBPF 程序并不能随意调用内核函数
  • 内核定义了一系列的辅助函数,用于 eBPF 程序与内核其他模块进行交互

通过bpftool feature probe 可以查询当前系统支持的 辅助函数列表

include/uapi/linux/bpf.h

辅助函数非常多,6.11 内核打印的结果如下:

 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
bpftool feature probe | grep "eBPF helpers supported for program"
eBPF helpers supported for program type socket_filter:
eBPF helpers supported for program type kprobe:
eBPF helpers supported for program type sched_cls:
eBPF helpers supported for program type sched_act:
eBPF helpers supported for program type tracepoint:
eBPF helpers supported for program type xdp:
eBPF helpers supported for program type perf_event:
eBPF helpers supported for program type cgroup_skb:
eBPF helpers supported for program type cgroup_sock:
eBPF helpers supported for program type lwt_in:
eBPF helpers supported for program type lwt_out:
eBPF helpers supported for program type lwt_xmit:
eBPF helpers supported for program type sock_ops:
eBPF helpers supported for program type sk_skb:
eBPF helpers supported for program type cgroup_device:
eBPF helpers supported for program type sk_msg:
eBPF helpers supported for program type raw_tracepoint:
eBPF helpers supported for program type cgroup_sock_addr:
eBPF helpers supported for program type lwt_seg6local:
eBPF helpers supported for program type lirc_mode2:
eBPF helpers supported for program type sk_reuseport:
eBPF helpers supported for program type flow_dissector:
eBPF helpers supported for program type cgroup_sysctl:
eBPF helpers supported for program type raw_tracepoint_writable:
eBPF helpers supported for program type cgroup_sockopt:
eBPF helpers supported for program type tracing:
eBPF helpers supported for program type struct_ops:
eBPF helpers supported for program type ext:
eBPF helpers supported for program type lsm:
eBPF helpers supported for program type sk_lookup:
eBPF helpers supported for program type syscall:
eBPF helpers supported for program type netfilter:

通过man bpf-helpers 可以查看每个辅助函数的细节

常用的辅助函数

辅助函数 功能描述
bpf_trace_printk(fmt, fmt_size, ...) 向调试文件系统写入调试信息
bpf_map_lookup_elem(map, key)bpf_map_update_elem(map, key, value, flags)bpf_map_delete_elem(map, key) BPF 映射操作函数,分别是查找、更新和删除元素
bpf_probe_read(dst, size, ptr)bpf_probe_read_user(dst, size, ptr)bpf_probe_read_kernel(dst, size, ptr) 从内存指针中读取数据从用户空间内存指针中读取数据从内核空间内存指针中读取数据
bpf_probe_read_str(dst, size, ptr)bpf_probe_read_user_str(dst, size, ptr)bpf_probe_read_kernel_str(dst, size, ptr) 从内存指针中读取字符串从用户空间内存指针中读取字符串从内核空间内存指针中读取字符串
bpf_ktime_get_ns() 获取系统启动以来的时长,单位纳秒
bpf_get_current_pid_tgid() 获取当前线程的 TGID(高32位)和 PID(低32位)
bpf_get_current_comm(buf, size) 获取当前线程的任务名称
bpf_get_current_task() 获取当前任务的 task 结构体
bpf_perf_event_output(ctx, map, flags, data, size) 向性能事件缓冲区中写入数据
bpf_get_stackid(ctx, map, flags) 获取内核态和用户态调用栈

bpf_probe_read 函数

  • eBPF 内部的内存空间只有寄存器和栈。
  • 要访问其他的内核空间或用户空间地址,就需要借助  bpf_probe_read 这一系列的辅助函数
  • 这些函数会进行安全性检查,并禁止缺页中断的发生

映射

介绍

  • BPF 映射用于提供大块的键值存储
  • 这些存储可被用户空间程序访问,进而获取 eBPF 程序的运行状态
  • eBPF 程序最多可以访问 64 个不同的 BPF 映射
  • 并且不同的 eBPF 程序也可以通过相同的 BPF 映射来共享它们的状态

参考 docs.cilium.io

BPF 辅助函数中并没有 BPF 映射的创建函数,BPF 映射只能通过用户态程序的系统调用来创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int bpf_create_map(enum bpf_map_type map_type,
       unsigned int key_size,
       unsigned int value_size, unsigned int max_entries) {
  union bpf_attr attr = {
    .map_type = map_type,
    .key_size = key_size,
    .value_size = value_size,
    .max_entries = max_entries
  };
  return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}

支持的映射类型

 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
bpftool feature probe | grep map_type

eBPF map_type hash is available
eBPF map_type array is available
eBPF map_type prog_array is available
eBPF map_type perf_event_array is available
eBPF map_type percpu_hash is available
eBPF map_type percpu_array is available
eBPF map_type stack_trace is available
eBPF map_type cgroup_array is available
eBPF map_type lru_hash is available
eBPF map_type lru_percpu_hash is available
eBPF map_type lpm_trie is available
eBPF map_type array_of_maps is available
eBPF map_type hash_of_maps is available
eBPF map_type devmap is available
eBPF map_type sockmap is available
eBPF map_type cpumap is available
eBPF map_type xskmap is available
eBPF map_type sockhash is available
eBPF map_type cgroup_storage is available
eBPF map_type reuseport_sockarray is available
eBPF map_type percpu_cgroup_storage is available
eBPF map_type queue is available
eBPF map_type stack is available
eBPF map_type sk_storage is available
eBPF map_type devmap_hash is available
eBPF map_type struct_ops is available
eBPF map_type ringbuf is available
eBPF map_type inode_storage is available
eBPF map_type task_storage is available
eBPF map_type bloom_filter is available
eBPF map_type user_ringbuf is available
eBPF map_type cgrp_storage is available
eBPF map_type arena is available

参考内核头文件:include/uapi/linux/bpf.h

常用的映射类型及其功能和使用场景

映射类型 功能描述
BPF_MAP_TYPE_HASH 哈希表映射,用于保存 key/value 对
BPF_MAP_TYPE_LRU_HASH 类似于哈希表映射,但在表满的时候自动按 LRU 算法删除最久未被使用的元素
BPF_MAP_TYPE_ARRAY 数组映射,用于保存固定大小的数组(注意数组元素无法删除)
BPF_MAP_TYPE_PROG_ARRAY 程序数组映射,用于保存 BPF 程序的引用,特别适合于尾调用(即调用其他 eBPF 程序)
BPF_MAP_TYPE_PERF_EVENT_ARRAY 性能事件数组映射,用于保存性能事件跟踪记录
BPF_MAP_TYPE_PERCPU_HASHBPF_MAP_TYPE_PERCPU_ARRAY 每个 CPU 单独维护的哈希表和数组映射
BPF_MAP_TYPE_STACK_TRACE 调用栈跟踪映射,用于存储调用栈信息
BPF_MAP_TYPE_ARRAY_OF_MAPSBPF_MAP_TYPE_HASH_OF_MAPS 映射数组和映射哈希,用于保存其他映射的引用
BPF_MAP_TYPE_CGROUP_ARRAY CGROUP 数组映射,用于存储 cgroups 引用
BPF_MAP_TYPE_SOCKMAP 套接字映射,用于存储套接字引用,特别适用于套接字重定向

比如 BCC 的预制的映射函数

1
BPF_HASH(name, key_type=u64, leaf_type=u64, size=10240)

通过以下方式来创建

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 使用默认参数 key_type=u64, leaf_type=u64, size=10240
BPF_HASH(stats);

// 使用自定义key类型,保持默认 leaf_type=u64, size=10240
struct key_t {
  char c[80];
};
BPF_HASH(counts, struct key_t);

// 自定义所有参数
BPF_HASH(cpu_time, uint64_t, uint64_t, 4096);

BPF 映射会在用户态程序关闭文件描述符的时候自动删除(即close(fd) )
想在程序退出后还保留映射,就需要调用 BPF_OBJ_PIN 命令,将映射挂载到 /sys/fs/bpf 中

通过命令来创建管理映射

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//创建一个哈希表映射,并挂载到/sys/fs/bpf/stats_map(Key和Value的大小都是2字节)
bpftool map create \
/sys/fs/bpf/stats_map type hash key 2 value 2 entries 8 name stats_map

//查询系统中的所有映射
bpftool map
//示例输出
//1: hash  flags 0x0
//        key 9B  value 1B  max_entries 500  memlock 59424B
//206: hash  name stats_map  flags 0x0
//        key 2B  value 2B  max_entries 8  memlock 14848B

//向哈希表映射中插入数据
bpftool map update name stats_map key 0xc1 0xc2 value 0xa1 0xa2

//查询哈希表映射中的所有数据
bpftool map dump name stats_map
//示例输出
//key: c1 c2  value: a1 a2
//Found 1 element

//删除哈希表映射
rm /sys/fs/bpf/stats_map

类型格式

内核头文件 linux-headers-$(uname -r) 也是必须要安装的一个依赖项
因为 BCC 在编译 eBPF 程序时,需要从内核头文件中找到相应的内核数据结构定义

主要有这三个方面:

  • 在开发 eBPF 程序时,为了获得内核数据结构的定义,就需要引入一大堆的内核头文件
  • 内核头文件的路径和数据结构定义在不同内核版本中很可能不同。因此,你在升级内核版本时,就会遇到找不到头文件和数据结构定义错误的问题
  • 生产环境以你为安全,不允许安装内核头文件,无法得到内核数据结构的定义。在程序中重定义数据结构虽然可以暂时解决这个问题,但也很容易把使用着错误数据结构的 eBPF 程序带入新版本内核中运行。

= 5.2 版本的内核中就自动内嵌在内核二进制文件 vmlinux 中
导出命令

1
bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

在开发 eBPF 程序时只需要引入一个 vmlinux.h 即可,不用再引入一大堆的内核头文件
GRANT SELTZER 博客

一些例子

create_map 例子

 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
#include <linux/bpf.h>    // For BPF_MAP_CREATE and bpf_attr
#include <sys/syscall.h>  // For syscall()
#include <unistd.h>       // For __NR_bpf
#include <stdio.h>

int create_map() {
    union bpf_attr attr = {
        .map_type    = BPF_MAP_TYPE_HASH,
        .key_size    = sizeof(int),
        .value_size  = sizeof(int),
        .max_entries = 1024,
    };

    // Use syscall() instead of direct bpf() invocation
    int fd = syscall(__NR_bpf, BPF_MAP_CREATE, &attr, sizeof(attr));

    if (fd < 0) {
        perror("Map creation failed");
        return -1;
    }
    printf("Map created with FD: %d\n", fd);
    return fd;
}

int main() {
    return create_map();
}

编译执行

1
2
3
4
clang -O2 -I/usr/include/$(uname -m)-linux-gnu xx.c -o xx

./xx
Map created with FD: 3

perf_event_open 函数

1
strace -f -e trace=perf_event_open python3 hello.py

打印结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
perf_event_open(
  {
	type=0x8 /* PERF_TYPE_??? */, 
	size=0x88 /* PERF_ATTR_SIZE_??? */, 
	config=0,
	sample_period=1, 
	sample_type=0, 
	read_format=0, 
	precise_ip=0 /* arbitrary skid */, ...
  }, 
	-1, 0, -1, 
	PERF_FLAG_FD_CLOEXEC
) = 5

BPF 与性能事件的绑定过程分为以下几步:

  • 首先,借助 bpf 系统调用,加载 BPF 程序,并记住返回的文件描述符;
  • 然后,查询 kprobe 类型的事件编号。BCC 实际上是通过 /sys/bus/event_source/devices/kprobe/type 来查询的;
  • 接着,调用 perf_event_open 创建性能监控事件。比如,事件类型(type 是上一步查询到的 6)、事件的参数( config1 包含了内核函数 do_sys_openat2 )等;
  • 最后,再通过 ioctl 的 PERF_EVENT_IOC_SET_BPF 命令,将 BPF 程序绑定到性能监控事件。

各类事件

事件类型

6.13内核支持的事件类型

 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
enum bpf_prog_type {
	BPF_PROG_TYPE_UNSPEC,
	BPF_PROG_TYPE_SOCKET_FILTER,
	BPF_PROG_TYPE_KPROBE,
	BPF_PROG_TYPE_SCHED_CLS,
	BPF_PROG_TYPE_SCHED_ACT,
	BPF_PROG_TYPE_TRACEPOINT,
	BPF_PROG_TYPE_XDP,
	BPF_PROG_TYPE_PERF_EVENT,
	BPF_PROG_TYPE_CGROUP_SKB,
	BPF_PROG_TYPE_CGROUP_SOCK,
	BPF_PROG_TYPE_LWT_IN,
	BPF_PROG_TYPE_LWT_OUT,
	BPF_PROG_TYPE_LWT_XMIT,
	BPF_PROG_TYPE_SOCK_OPS,
	BPF_PROG_TYPE_SK_SKB,
	BPF_PROG_TYPE_CGROUP_DEVICE,
	BPF_PROG_TYPE_SK_MSG,
	BPF_PROG_TYPE_RAW_TRACEPOINT,
	BPF_PROG_TYPE_CGROUP_SOCK_ADDR,
	BPF_PROG_TYPE_LWT_SEG6LOCAL,
	BPF_PROG_TYPE_LIRC_MODE2,
	BPF_PROG_TYPE_SK_REUSEPORT,
	BPF_PROG_TYPE_FLOW_DISSECTOR,
	BPF_PROG_TYPE_CGROUP_SYSCTL,
	BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE,
	BPF_PROG_TYPE_CGROUP_SOCKOPT,
	BPF_PROG_TYPE_TRACING,
	BPF_PROG_TYPE_STRUCT_OPS,
	BPF_PROG_TYPE_EXT,
	BPF_PROG_TYPE_LSM,
	BPF_PROG_TYPE_SK_LOOKUP,
	BPF_PROG_TYPE_SYSCALL, /* a program that can execute syscalls */
	BPF_PROG_TYPE_NETFILTER,
	__MAX_BPF_PROG_TYPE
};

include/uapi/linux/bpf.h

查询当前系统支持的类型

1
bpftool feature probe | grep program_type

结果

 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
eBPF program_type socket_filter is available
eBPF program_type kprobe is available
eBPF program_type sched_cls is available
eBPF program_type sched_act is available
eBPF program_type tracepoint is available
eBPF program_type xdp is available
eBPF program_type perf_event is available
eBPF program_type cgroup_skb is available
eBPF program_type cgroup_sock is available
eBPF program_type lwt_in is available
eBPF program_type lwt_out is available
eBPF program_type lwt_xmit is available
eBPF program_type sock_ops is available
eBPF program_type sk_skb is available
eBPF program_type cgroup_device is available
eBPF program_type sk_msg is available
eBPF program_type raw_tracepoint is available
eBPF program_type cgroup_sock_addr is available
eBPF program_type lwt_seg6local is available
eBPF program_type lirc_mode2 is NOT available
eBPF program_type sk_reuseport is available
eBPF program_type flow_dissector is available
eBPF program_type cgroup_sysctl is available
eBPF program_type raw_tracepoint_writable is available
eBPF program_type cgroup_sockopt is available
eBPF program_type tracing is available
eBPF program_type struct_ops is available
eBPF program_type ext is available
eBPF program_type lsm is available
eBPF program_type sk_lookup is available
eBPF program_type syscall is available
eBPF program_type netfilter is available

这些程序类型大致可以划分为三类:

  • 跟踪,即从内核和程序的运行状态中提取跟踪信息,来了解当前系统正在发生什么
  • 网络,即对网络数据包进行过滤和处理,以便了解和控制网络数据包的收发过程
  • 除跟踪和网络之外的其他类型,包括安全控制、BPF 扩展等等

跟踪类 eBPF 程序

跟踪类 eBPF 程序主要用于从系统中提取跟踪信息,进而为监控、排错、性能优化等提供数据支撑

比如

  • BPF_PROG_TYPE_KPROBE 类型的跟踪程序,它的目的是跟踪内核函数是否被某个进程调用了
  • KPROBE、TRACEPOINT 以及 PERF_EVENT 都是最常用的 eBPF 程序类型
  • 大量应用于监控跟踪、性能优化以及调试排错等场景中
程序类型 功能描述 功能限制
BPF_PROG_TYPE_KPROBE 用于对特定函数进行动态插桩,根据函数位置的不同,又可以分为内核态 kprobe 和用户态 uprobe 内核函数和用户函数的定义属于不稳定 API,在不同内核版本中使用时,可能需要调整 eBPF 代码实现
BPF_PROG_TYPE_TRACEPOINT 用于内核静态跟踪点(可以使用 perf list 命令,查询所有的跟踪点) 虽然跟踪点可以保持稳定性,但不如 KPROBE 类型灵活,无法按需增加新的跟踪点
BPF_PROG_TYPE_PERF_EVENT 用于性能事件(perf_events)跟踪,包括内核调用、定时器、硬件等各类性能数据 需配合 BPF_MAP_TYPE_PERF_EVENT_ARRAY 或 BPF_MAP_TYPE_RINGBUF 类型的映射使用
BPF_PROG_TYPE_RAW_TRACEPOINT BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE 用于原始跟踪点 不处理参数
BPF_PROG_TYPE_TRACING 用于开启 BTF 的跟踪点 需要开启 BTF

bcc程序

网络类 eBPF 程序

根据事件触发位置的不同,网络类 eBPF 程序又可以分为

  • XDP(eXpress Data Path,高速数据路径)程序
  • TC(Traffic Control,流量控制)程序
  • 套接字程序
  • cgroup 程序

XDP

XDP 程序

  • 类型定义为 BPF_PROG_TYPE_XDP
  • 它在网络驱动程序刚刚收到数据包时触发执行
  • 由于无需通过繁杂的内核网络协议栈,XDP 程序可用来实现高性能的网络处理方案
  • 常用于 DDoS 防御、防火墙、4 层负载均衡等场

根据网卡和网卡驱动是否原生支持 XDP 程序,XDP 运行模式可以分为下面这三种:

  • 通用模式。它不需要网卡和网卡驱动的支持,XDP 程序像常规的网络协议栈一样运行在内核中,性能相对较差,一般用于测试
  • 原生模式。它需要网卡驱动程序的支持,XDP 程序在网卡驱动程序的早期路径运行
  • 卸载模式。它需要网卡固件支持 XDP 卸载,XDP 程序直接运行在网卡上,而不再需要消耗主机的 CPU 资源,具有最好的性能。
结果码 含义 使用场景
XDP_DROP 丢包 数据包尽早丢弃可减少 CPU 处理时间,常用于防火墙、DDoS 防御等丢弃非法包场景
XDP_PASS 传递到内核协议栈 内核协议栈接收网络包,按正常流程继续处理
XDP_TX XDP_REDIRECT 转发数据包到同一 / 不同网卡 XDP 程序修改数据包后转发到网卡,按内核协议栈流程处理,常用在负载均衡中
XDP_ABORTED 错误 XDP 程序运行错误时,丢弃数据包并记录错误行为以便排错

XDP 程序通过 ip link 命令加载到具体的网卡上,加载格式为

1
2
3
4
5
6
7
# eth1 为网卡名
# xdpgeneric 设置运行模式为通用模式
# xdp-example.o 为编译后的 XDP 字节码
sudo ip link set dev eth1 xdpgeneric object xdp-example.o

## 卸载
sudo ip link set veth1 xdpgeneric off

BCC 也提供了方便的库函数,可以在同一个程序中管理 XDP 程序的生命周期

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
from bcc import BPF

# 编译XDP程序
b = BPF(src_file="xdp-example.c")
fn = b.load_func("xdp-example", BPF.XDP)

# 加载XDP程序到eth0网卡
device = "eth0"
b.attach_xdp(device, fn, 0)

# 其他处理逻辑
...

# 卸载XDP程序
b.remove_xdp(device)

TC

TC 程序

  • 类型定义为 BPF_PROG_TYPE_SCHED_CLS 和 BPF_PROG_TYPE_SCHED_ACT
  • 分别作为 Linux 流量控制 的分类器和执行器
  • Linux 流量控制通过网卡队列、排队规则、分类器、过滤器以及执行器等
  • 实现了对网络流量的整形调度和带宽控制。

下图展示了  HTB(Hierarchical Token Bucket,层级令牌桶)流量控制的工作原理 Traffic Control HOWTO

TC 程序可以直接在一个程序内完成分类和执行的动作,而无需再调用其他的 TC 排队规则和分类器
具体如下图所示

同 XDP 程序相比

  • TC 程序可以直接获取内核解析后的网络报文数据结构sk_buff(XDP 则是 xdp_buff)
  • 并且可在网卡的接收和发送两个方向上执行(XDP 则只能用于接收)

TC 程序的执行位置

  • 对于接收的网络包,TC 程序在网卡接收(GRO)之后、协议栈处理(包括 IP 层处理和 iptables 等)之前执行
  • 对于发送的网络包,TC 程序在协议栈处理(包括 IP 层处理和 iptables 等)之后、数据包发送到网卡队列(GSO)之前执行

除此之外,由于 TC 运行在内核协议栈中,不需要网卡驱动程序做任何改动,因而可以挂载到任意类型的网卡设备(包括容器等使用的虚拟网卡)上
同 XDP 程序一样,TC eBPF 程序也可以通过 Linux 命令行工具来加载到网卡上 不过相应的工具要换成 tc

下面命令分别加载接收和发送方向的 eBPF 程序

1
2
3
4
5
6
7
8
# 创建 clsact 类型的排队规则
sudo tc qdisc add dev eth0 clsact

# 加载接收方向的 eBPF 程序
sudo tc filter add dev eth0 ingress bpf da obj tc-example.o sec ingress

# 加载发送方向的 eBPF 程序
sudo tc filter add dev eth0 egress bpf da obj tc-example.o sec egress

Socket

介绍

  • 套接字程序用于过滤、观测或重定向套接字网络包
  • 根据类型的不同,套接字 eBPF 程序可以挂载到套接字(socket)、控制组(cgroup )以及网络命名空间(netns)等各个位置
套接字程序类型 应用场景 挂载方法
BPF_PROG_TYPE_SOCKET_FILTER 用于套接字过滤和观测 用户态程序可通过系统调用 setsockopt(sock, SOL_SOCKET, SO_ATTACH_BPF, ...) ,绑定 BPF 程序到具体的 socket 上
BPF_PROG_TYPE_SOCK_OPS 用于套接字修改或重定向 用户态程序可通过 BPF 系统调用的 BPF_PROG_ATTACH 命令(指定挂载类型为 BPF_CGROUP_SOCK_OPS ),将其挂载到 cgroup 上
BPF_PROG_TYPE_SK_SKB 用于套接字修改或消息流动态解析 用户态程序可通过 BPF 系统调用的 BPF_PROG_ATTACH 命令(指定挂载类型为 BPF_SK_SKB_STREAM_VERDICTBPF_SK_SKB_STREAM_PARSER ),将其挂载到 BPF_MAP_TYPE_SOCKMAP 类型的 BPF 映射上
BPF_PROG_TYPE_SK_MSG 用于控制内核是否发送消息到套接字 用户态程序可通过 BPF 系统调用的 BPF_PROG_ATTACH 命令(指定挂载类型为 BPF_SK_MSG_VERDICT )将其挂载到 BPF_MAP_TYPE_SOCKMAP 类型的 BPF 映射上
BPF_PROG_TYPE_SK_REUSEPORT 用于控制端口是否重用 用户态程序可通过系统调用 setsockopt(sock, SOL_SOCKET, SO_ATTACH_REUSEPORT_EBPF, ...) ,绑定 BPF 程序到具体的 socket 上
BPF_PROG_TYPE_SK_LOOKUP 用于为新的 TCP 连接选择监听套接字,或为 UDP 数据包选择未连接的套接字,可用来绕过 bind 系统调用的限制 用户态程序可通过系统调用 bpf(BPF_LINK_CREATE, ...) ,绑定 BPF 程序到网络命名空间(netns)上

cgroup

cgroup 程序用于对 cgroup 内所有进程的网络过滤、套接字选项以及转发等进行动态控制
它最典型的应用场景是对容器中运行的多个进程进行网络控制

cgroup 程序类型 应用场景
BPF_PROG_TYPE_CGROUP_SKB 在入口和出口过滤数据包,并可以接受或拒绝数据包
BPF_PROG_TYPE_CGROUP_SOCK 在套接字创建、释放和绑定地址时,接受或拒绝操作,也可用来统计套接字信息
BPF_PROG_TYPE_CGROUP_SOCKOPT 在 setsockopt 和 getsockopt 操作中修改套接字选项
BPF_PROG_TYPE_CGROUP_SOCK_ADDR 在 connect、bind、sendto 和 recvmsg 操作中,修改 IP 地址和端口
BPF_PROG_TYPE_CGROUP_DEVICE 对设备文件的访问进行过滤
BPF_PROG_TYPE_CGROUP_SYSCTL 对 sysctl 的访问进行过滤

这些类型的 BPF 程序都可以通过 BPF 系统调用的 BPF_PROG_ATTACH 命令来进行挂载
并设置挂载类型为匹配的 BPF_CGROUP_xxx 类型
在挂载 BPF_PROG_TYPE_CGROUP_DEVICE 类型的 BPF 程序时,需要设置 bpf_attach_type 为 BPF_CGROUP_DEVICE:

1
2
3
4
5
6
7
8
9
union bpf_attr attr = {};
attr.target_fd = target_fd;            // cgroup文件描述符
attr.attach_bpf_fd = prog_fd;          // BPF程序文件描述符
attr.attach_type = BPF_CGROUP_DEVICE;  // 挂载类型为BPF_CGROUP_DEVICE

if (bpf(BPF_PROG_ATTACH, &attr, sizeof(attr)) < 0) {
  return -errno;
}
...

补充

  • 这几类网络 eBPF 程序是在不同的事件触发时执行的
  • 因此,在实际应用中可以把多个类型的 eBPF 程序结合起来实现复杂的网络控制功能 比如,最流行的 Kubernetes 网络方案 Cilium 就大量使用了 XDP、TC 和套接字 eBPF 程序

图片来自 Cilium 官方文档,图中黄色部分即为 Cilium eBPF 程序

其他类 eBPF 程序

除了上面的跟踪和网络 eBPF 程序之外,Linux 内核还支持很多其他的类型

BPF 程序类型 应用场景
BPF_PROG_TYPE_LSM 用于 Linux 安全模块(Linux Security Module, LSM)访问控制和审计策略
BPF_PROG_TYPE_LWT_IN BPF_PROG_TYPE_LWT_OUT BPF_PROG_TYPE_LWT_XMIT 用于轻量级隧道(如 vxlan、mpls 等)的封装或解封装
BPF_PROG_TYPE_LIRC_MODE2 用于红外设备的远程遥控
BPF_PROG_TYPE_STRUCT_OPS 用于修改内核结构体,目前仅支持拥塞控制算法 tcp_congestion_ops
BPF_PROG_TYPE_FLOW_DISSECTOR 用于内核流量解析器(Flow Dissector)
BPF_PROG_TYPE_EXT 用于扩展 BPF 程序

参考