你好,哈布尔! 我们想通知您,我们正在准备出版一本书。”".

由于 BPF 虚拟机不断发展并在实践中得到积极使用,我们为您翻译了一篇文章,描述其主要功能和当前状态。
近年来,旨在弥补内核局限性的编程工具和技术变得越来越流行。 Linux 在需要高性能数据包处理的情况下,最常用的技术之一是…… 内核绕过 (内核旁路)并允许绕过内核网络层,从用户空间执行所有数据包处理。 绕过内核还涉及从以下位置控制网卡 用户空间。 换句话说,当使用网卡时,我们依赖于驱动程序 用户空间.
通过将对网卡的完全控制权转移给用户空间程序,我们减少了内核开销(上下文切换、网络层处理、中断等),这在以 10Gb/s 或更高速度运行时非常重要。 内核旁路加上其他功能的组合(批量处理)和仔细的性能调整(NUMA 会计, CPU隔离等)对应于用户空间中高性能网络处理的基础。 也许这种新的数据包处理方法的一个示例是 来自英特尔(数据平面开发套件),尽管还有其他众所周知的工具和技术,包括 Cisco 的 VPP(矢量包处理)、Netmap,当然还有 .
在用户空间组织网络交互有很多缺点:
- 操作系统内核是硬件资源的抽象层。 由于用户空间程序必须直接管理其资源,因此它们还必须管理自己的硬件。 这通常意味着必须编写自己的驱动程序。
- 因为我们完全放弃了内核空间,所以我们也放弃了内核提供的所有网络功能。 用户空间程序必须重新实现内核或操作系统可能已经提供的功能。
- 程序在沙箱模式下运行,这严重限制了它们的交互并阻止它们与操作系统的其他部分集成。
本质上,当在用户空间中联网时,性能增益是通过将数据包处理从内核转移到用户空间来实现的。 XDP 的作用恰恰相反:它将网络程序从用户空间(过滤器、解析器、路由等)移至内核空间。 XDP 允许我们在数据包到达网络接口后、开始向上移动到内核网络子系统之前执行网络功能。 结果,数据包处理速度显着提高。 然而,内核如何允许用户在内核空间中执行他们的程序呢? 在回答这个问题之前,我们先来看看什么是BPF。
BPF 和 eBPF
尽管名称令人困惑,但 BPF(伯克利数据包过滤)实际上是一个虚拟机模型。 该虚拟机最初设计用于处理数据包过滤,因此得名。
使用 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(即IPv6的ethertype值)进行比较。 如果结果为真,则程序计数器转到指令(002),如果结果不为真,则程序计数器转到指令(006)。
- 指令(006):将该值与 0x800(IPv4 的以太网类型值)进行比较。 如果答案为真,则程序转到(007),否则,则转到(015)。
依此类推,直到包过滤程序返回结果。 这通常是一个布尔值。 返回非零值(指令(014))意味着数据包被接受,返回零值(指令(015))意味着数据包未被接受。
BPF 虚拟机及其字节码是由 Steve McCann 和 Van Jacobson 于 1992 年底发表论文时提出的 ,这项技术首次在1993年冬天的Usenix会议上提出。
因为BPF是一个虚拟机,所以它定义了程序运行的环境。 除了字节码之外,它还定义了批处理内存模型(加载指令隐式应用于批处理)、寄存器(A 和 X;累加器和索引寄存器)、临时内存存储和隐式程序计数器。 有趣的是,BPF 字节码是按照 Motorola 6502 ISA 建模的。 正如史蒂夫·麦肯在他的书中回忆的那样 在 Sharkfest '11 上,他通过高中时代在 Apple II 上编程就熟悉了 build 6502,这些知识影响了他设计 BPF 字节码的工作。
内核中已实现 BPF 支持。 Linux 在 v2.5 及更高版本中,该功能主要由 Jay Schullist 添加。BPF 代码基本保持不变,直到 2011 年 Eric Dumazet 将 BPF 解释器重构为以 JIT 模式运行(来源: )。 此后,内核可以直接将BPF程序转换为目标架构:x86、ARM、MIPS等,而不是解释BPF字节码。
后来,在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 编写新示例,但会使用样本/bpf/ 中提供的示例之一。 我将查看代码的某些部分并解释其工作原理。 作为示例,我选择了该程序 tracex4.
一般来说,samples/bpf/ 中的每个示例都包含两个文件。 在这种情况下:
tracex4_kern.c,包含要在内核中作为 eBPF 字节码执行的源代码。tracex4_user.c,包含来自用户空间的程序。
在这种情况下,我们需要编译 tracex4_kern.c 到 eBPF 字节码。 目前在 gcc eBPF 没有后端。 幸运的是, clang 可以输出eBPF字节码。 使用 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)。 所有以大写字母书写的函数名称都对应于中定义的宏 .
如果我转储目标文件的部分,我应该看到这些新部分已经定义:
$ objdump -h tracex4_kern.o
tracex4_kern.o: 文件格式 elf64-little
部分:
Idx 名称 大小 VMA LMA 文件关闭算法
0 .文本 00000000 0000000000000000 0000000000000000 00000040 2**2
内容、分配、加载、只读、代码
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
内容、分配、加载、重新定位、只读、代码
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
内容、分配、加载、重新定位、只读、代码
3 张地图 0000001c 0000000000000000 0000000000000000 00000148 2**2
内容、分配、负载、数据
4 许可证 00000004 0000000000000000 0000000000000000 00000164 2**0
内容、分配、负载、数据
5 版本 00000004 0000000000000000 0000000000000000 00000168 2**2
内容、分配、负载、数据
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
内容、分配、加载、重新分配、只读、数据
还有 ,主程序。 基本上,这个程序监听事件 kmem_cache_alloc_node。 当此类事件发生时,就会执行相应的 eBPF 代码。 该代码将对象的 IP 属性保存到映射中,然后在主程序中循环该对象。 例子:
$ 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
用户空间程序和 eBPF 程序如何相关? 初始化时 tracex4_user.c 加载目标文件 tracex4_kern.o 使用功能 load_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;
} 表演时 eBPF 文件中定义的探针被添加到 /sys/kernel/debug/tracing/kprobe_events。 现在我们监听这些事件,当它们发生时我们的程序可以做一些事情。
$ 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
Sample/bpf/ 中的所有其他程序的结构都类似。 它们始终包含两个文件:
XXX_kern.c:eBPF 程序。XXX_user.c: 主程序。
eBPF 程序识别与节相关的映射和函数。 当内核发出某种类型的事件时(例如, tracepoint),执行绑定的函数。 这些卡提供内核程序和用户空间程序之间的通信。
结论
本文一般性地讨论了 BPF 和 eBPF。 我知道今天有很多关于 eBPF 的信息和资源,所以我会推荐一些更多的资源以供进一步学习
我建议阅读:
- 乔纳森·科贝特。 介绍 BPF 以及它如何演变为 eBPF。
- 布伦丹·格雷格. 文章来自 LWN.net。 Brendan 经常发布有关 eBPF 的推文,并在他的网站上维护有关该主题的资源列表 .
- 朱莉娅·埃文斯。 对 Suchakra Sharma 的演讲“BSD 数据包过滤器:用户级数据包捕获的新架构”的评论。 这些评论很好,确实可以帮助您理解幻灯片。
- 费里斯·埃利斯. 长读与 ,但值得一读。 我在 eBPF 上遇到的最好的文章之一。
来源: habr.com
