BPF和eBPF简介

哈Ha!我们通知您,我们正在准备发行《带BPF的Linux Observability一书


随着BPF虚拟机的不断发展和在实践中的积极应用,我们为您翻译了一篇文章,介绍了其主要功能和当前状态。

近年来,编程工具和技术已开始流行,其设计是为了补偿在需要高性能程序包处理的情况下Linux内核的局限性。这种最流行的方法之一称为绕过内核(内核绕过),并允许传递的网络层核心执行用户空间中的所有数据包处理。绕过内核还涉及从用户空间管理网卡。换句话说,使用网卡时,我们依赖于用户空间驱动程序

通过从用户空间将对网卡的完全控制权转移到程序,我们减少了与内核相关的成本(切换上下文,处理网络级别,中断等),这在以10Gb / s或更高的速度工作时非常重要。内核旁路以及其他功能(批处理)和精确的性能调整(NUMA记帐CPU隔离等)的组合,对应于用户空间中高性能网络处理的基础。英特尔的DPDK数据平面开发套件)可能是这种新的封装处理方法的模型示例。),尽管还有其他众所周知的工具和技术,包括Cisco的VPP(矢量数据包处理),Netmap,当然还有Snabb

用户空间中网络交互的组织具有许多缺点:

  • 操作系统的内核是硬件资源的抽象级别。因为用户空间程序必须直接管理其资源,所以它们还必须管理自己的硬件。这通常意味着您需要编写自己的驱动程序。
  • , , . , , , .
  • , .

本质上,在组织用户空间中的网络交互时,通过将数据包处理从内核转移到用户空间来提高生产率。 XDP恰好相反:将网络程序从用户空间(过滤器,转换器,路由等)移至内核区域。 XDP允许我们在数据包到达网络接口之后立即开始执行网络功能,然后再开始向上移动到内核的网络子系统。结果,分组处理速度显着提高。但是,内核如何允许用户在内核空间中执行其程序?在回答这个问题之前,让我们看一下BPF是什么。

BPF和eBPF

尽管名称不清楚,但实际上BPF(Packet Filtering,Berkeley)却是虚拟机模型。该虚拟机最初旨在处理数据包筛选,因此得名。

使用BPF的最著名的工具之一是tcpdump使用捕获数据包时,tcpdump用户可以指定一个表达式以过滤数据包。仅匹配此表达式的数据包将被捕获。例如,表达式“ tcp dst port 80”适用于到达端口80的所有TCP数据包。编译器可以通过将其转换为BPF字节码来缩短该表达式。 上面的程序基本上是这样做的:

$ sudo tcpdump -d "tcp dst port 80"
(000) ldh [12]
(001) jeq #0x86dd jt 2 jf 6
(002) ldb [20]
(003) jeq #0x6 jt 4 jf 15
(004) ldh [56]
(005) jeq #0x50 jt 14 jf 15
(006) jeq #0x800 jt 7 jf 15
(007) ldb [23]
(008) jeq #0x6 jt 9 jf 15
(009) ldh [20]
(010) jset #0x1fff jt 15 jf 11
(011) ldxb 4*([14]&0xf)
(012) ldh [x + 16]
(013) jeq #0x50 jt 14 jf 15
(014) ret #262144
(015) ret #0




  • 指令(000):将偏移量为12的数据包以16位字的形式下载到电池中。偏移量12对应于以太类型数据包。
  • (001): 0x86dd, , ethertype- IPv6. true, (002), – (006).
  • (006): 0x800 (ethertype- IPv4). true, (007), – (015).

依此类推,直到包过滤程序返回结果为止。通常这是一个bulean。返回非零值(指令(014))表示该数据包已到达,返回零(指令(015))表示该数据包尚未到达。

BPF虚拟机及其字节码由Steve McCann和Van Jacobson在1992年底提出,当时他们的文章BSD分组过滤器:用户级别的分组捕获的新体系结构是在1993年冬天的Usenix会议上首次提出的。

由于BPF是虚拟机,因此它定义了程序运行的环境。除字节码外,它还定义了批处理内存模型(将启动指令隐式应用于程序包),寄存器(A和X;电池和索引寄存器),暂存存储器和隐式程序计数器。有趣的是,BPF字节码是根据Motorola 6502 ISA建模的。正如史蒂夫·麦肯(Steve McCann)在Sharkfest '11的全体会议上回顾的那样,他从高中就开始对Apple II进行编程时就熟悉6502的构造,这一知识影响了他设计BPF字节码的工作。

对BPF的支持在v2.5及更高版本的Linux内核中实现,主要是由Jay Schullist的努力添加的。 BPF代码一直保持不变,直到2011年Eric Dumazett重新定义了BPF解释器以在JIT模式下工作(数据包过滤器为JIT)。之后,内核无需解释BPF字节码,而可以直接将BPF程序转换为目标体系结构:x86,ARM,MIPS等。

后来,在2014年,Alexey Starovoitov为BPF提出了一种新的JIT机制。实际上,此新的JIT已成为基于BPF的新体系结构,称为eBPF。我认为一段时间以来,两个虚拟机共存,但是当前基于eBPF实施数据包筛选。实际上,在现代文档的许多示例中,BPF被理解为表示eBPF,而经典BPF现在被称为cBPF。

eBPF通过以下几种方式扩展了传统的BPF虚拟机:

  • 基于现代的64位体系结构。eBPF使用64位寄存器,并将可用寄存器的数量从2个(电池和X)增加到10。eBPF还提供其他操作代码(BPF_MOV,BPF_JNE,BPF_CALL ...)。
  • . BPF . , , . , eBPF . , eBPF tracepoint kprobe. eBPF, . eBPF : kernel/bpf.
  • . – «-», . eBPF .
  • . , , . . , eBPF .
  • . eBPF 4096 . eBPF eBPF- ( 32 ).

eBPF:示例

Linux内核源代码中有几个eBPF的示例。它们可在samples / bpf /下获得。要编译这些示例,只需输入:

$ sudo make samples/bpf/

我自己不会为eBPF编写新示例,而是使用samples / bpf /中可用的示例之一。我将看一下代码的某些部分,并解释其工作原理。作为示例,我选择了一个程序tracex4

通常,samples / bpf /中的每个示例都包含两个文件。在这种情况下:

  • tracex4_kern.c,包含必须在内核中作为eBPF字节码执行的源代码。
  • tracex4_user.c,包含来自用户空间的程序。

在这种情况下,我们需要将 tracex4_kern.ceBPF 编译为字节码。当前gcc没有eBPF的服务器部分。幸运的是,它clang可以发出eBPF字节码。Makefile用于clang编译tracex4_kern.c目标文件。

上面我提到过,eBPF最有趣的功能之一就是卡片。tracex4_kern定义了一张地图:

struct pair {
    u64 val;
    u64 ip;
};  

struct bpf_map_def SEC("maps") my_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(long),
    .value_size = sizeof(struct pair),
    .max_entries = 1000000,
};

BPF_MAP_TYPE_HASH-eBPF提供的多种卡之一。在这种情况下,它只是一个哈希。您可能还注意到了一个广告SEC("maps")SEC是用于创建二进制文件的新部分的宏。实际上,该示例tracex4_kern定义了另外两个部分:

SEC("kprobe/kmem_cache_free")
int bpf_prog1(struct pt_regs *ctx)
{   
    long ptr = PT_REGS_PARM2(ctx);

    bpf_map_delete_elem(&my_map, &ptr); 
    return 0;
}
    
SEC("kretprobe/kmem_cache_alloc_node") 
int bpf_prog2(struct pt_regs *ctx)
{
    long ptr = PT_REGS_RC(ctx);
    long ip = 0;

    //  ip-   kmem_cache_alloc_node() 
    BPF_KRETPROBE_READ_RET_IP(ip, ctx);

    struct pair v = {
        .val = bpf_ktime_get_ns(),
        .ip = ip,
    };
    
    bpf_map_update_elem(&my_map, &ptr, &v, BPF_ANY);
    return 0;
}   

这两个功能允许您从卡中删除条目(kprobe/kmem_cache_free)并向卡中添加新记录(kretprobe/kmem_cache_alloc_node)。所有以大写字母表示的函数名称均与中定义的宏相对应bpf_helpers.h

如果输出目标文件各节的转储,则应该看到已经定义了这些新节:主程序 仍在那。基本上,该程序监听事件。发生此类事件时,将执行相应的eBPF代码。该代码将对象的IP属性存储在地图中,然后在主程序中循环显示该对象。示例: 用户空间程序和eBPF程序有什么关系?在初始化时,它使用function 加载目标文件

$ objdump -h tracex4_kern.o

tracex4_kern.o: file format elf64-little

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2
CONTENTS, ALLOC, LOAD, DATA
4 license 00000004 0000000000000000 0000000000000000 00000164 2**0
CONTENTS, ALLOC, LOAD, DATA
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
CONTENTS, ALLOC, LOAD, DATA
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA


tracex4_user.ckmem_cache_alloc_node

$ sudo ./tracex4
obj 0xffff8d6430f60a00 is 2sec old was allocated at ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 is 23sec old was allocated at ip ffffffff98090e8f
obj 0xffff8d5f80161780 is 6sec old was allocated at ip ffffffff98090e8f


tracex4_user.ctracex4_kern.oload_bpf_file

int main(int ac, char **argv)
{
    struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
    char filename[256];
    int i;

    snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);

    if (setrlimit(RLIMIT_MEMLOCK, &r)) {
        perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
        return 1;
    }

    if (load_bpf_file(filename)) {
        printf("%s", bpf_log_buf);
        return 1;
    }

    for (i = 0; ; i++) {
        print_old_objects(map_fd[1]);
        sleep(1);
    }

    return 0;
}

执行后,load_bpf_file会将eBPF文件中定义探针添加到中/sys/kernel/debug/tracing/kprobe_events现在我们正在侦听这些事件,并且当它们发生时,我们的程序可以执行某些操作。 sample / bpf /中的所有其他程序的结构均类似。他们总是有两个文件:

$ sudo cat /sys/kernel/debug/tracing/kprobe_events
p:kprobes/kmem_cache_free kmem_cache_free
r:kprobes/kmem_cache_alloc_node kmem_cache_alloc_node




  • XXX_kern.c:eBPF程序。
  • XXX_user.c:主程序。

eBPF程序定义与该部分关联的卡和功能。当内核引发某种类型的事件(例如tracepoint)时,绑定的函数将被执行。映射提供了内核程序和用户空间程序之间的数据交换。

结论

本文概述了BPF和eBPF。我知道今天有很多有关eBPF的信息和资源,因此我建议您进一步学习一些材料,

建议阅读:


All Articles