首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

eBPF 简史(3)

eBPF 简史(3)

再见了汇编利用高级语言书写 BPF 逻辑并经由编译器生成出伪代码来并不是什么新鲜的尝试,比如 libpcap 就是在代码中内嵌了一个小型编译器来分析                tcpdump 传入的 filter expression 从而生成 BPF                伪码的。只不过长久以来该功能一直没有能被独立出来或者做大做强,究其原因,主要还是由于传统的 BPF                所辖领域狭窄,过滤机制也不甚复杂,就算是做的出来,估计也不堪大用。
然而到了 eBPF 的时代,情况终于发生了变化:现行的伪指令集较之过去已经复杂太多,再用纯汇编的开发方式已经不合时宜,于是,自然而然的,利用 C                一类的高级语言书写 BPF 伪代码的呼声便逐渐高涨了起来。
目前,支持生成 BPF 伪代码的编译器只有 llvm 一家,即使是通篇使用 gcc 编译的 Linux 内核,samples 目录下的 bpf                范例也要借用 llvm 来编译完成。还是以 sockex1 为例,用户态下的代码 sockex_user.c 是利用 HOSTCC                定义的编译器编译的;但 sockex_kern.c 就需要用到 clang 和 llvm 了。在中,可以看到:
清单 5                    samples/bpf/Makefile
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
# ......
# List of programs to build
hostprogs-y := test_lru_dist
hostprogs-y += sockex1
# ……
sockex1-objs := bpf_load.o $(LIBBPF) sockex1_user.o
# ……
# 注意,这里有一个小 tip,就是如果在内核的 Makefile 中,
# 有某一个目标文件你不希望使用内核的通用编译规则的话(类似于本文的 sockex1_kern.o),
# 可以像这里一样,并不把该文件加入任何 xxxprogs 或 xxx-objs,
# 而是直接放入 always,这样内核就会在本地 Makefile 中搜索编译规则了。
always := $(hostprogs-y)
<strong>always </strong><strong>+= sockex1_kern.o</strong>
# ……
LLC ?= llc
CLANG ?= clang
# ……
# sockex1_kern.o 就是使用了下述规则编译为 BPF 代码的,请注意笔者加粗的部分
$(obj)/%.o: $(src)/%.c
$(CLANG) $(NOSTDINC_FLAGS) $(LINUXINCLUDE) $(EXTRA_CFLAGS) \
-D__KERNEL__ -D__ASM_SYSREG_H -Wno-unused-value -Wno-pointer-sign \
-Wno-compare-distinct-pointer-types \
-Wno-gnu-variable-sized-type-not-at-end \
-Wno-address-of-packed-member -Wno-tautological-compare \
-Wno-unknown-warning-option \
-O2 -emit-llvm -c $< -o -|
$(LLC) -march=bpf -filetype=obj -o
$@




能用 C 书写 BPF 自然是便利了许多,但也不代表余下的开发工作就是一片坦途了:首先 llvm 的输出是 elf                文件,这也意味着想要获取能传入内核的代码,我们还需要额外做一段解析 elf 的工作,这也是为什么 Sample 下的范例几乎无一例外地都链接了                libelf 库;其次,同时也是比较重要的一点,不要忘记 BPF                的代码是跑在内核空间中的,因此书写时必得煞费苦心一番才好,以防一个不小心就做出个把内核干趴下的漏洞来:下文中提及的 verifier                就是为了这一点而生,每一个被放进内核的 BPF 代码,都须要经过它的检验才行。
BPF 程序的类别以及 Map                机制清单 3 中我们看到 sockex1_kern.o 是由 load_bpf_file()函数载入内存的,但实际上 eBPF 提供用来将 BPF                    代码载入内核的正式接口函数其实是 bpf_load_program(),该接口负责通过参数向内核提供三类信息:
  • BPF 程序的类型、
  • BPF 代码
  • 代码运行时所需要的存放 log 的缓存地址(位于用户空间);
有意思的是,目前所有注入内核的 BPF 程序都需要附带 GPL 协议支持信息,bpf_load_program()的 license                参数就是用来载入协议字串的。
由 eBPF 伊始,BPF 程序开始有分类了,通过 bpf_load_program 的参数 bpf_prog_type,我们可以看到 eBPF 支持的程序类型。这里笔者将一些常用的类型罗列于下表之中供读者参考:
表 2 常见                bpf_prog_type 定义bpf_prog_typeBPF prog 入口参数(R1)程序类型BPF_PROG_TYPE_SOCKET_FILTERstruct __sk_buff 用于过滤进出口网络报文,功能上和 cBPF 类似。                        BPF_PROG_TYPE_KPROBEstruct pt_regs 用于 kprobe 功能的 BPF 代码。                        BPF_PROG_TYPE_TRACEPOINT 这类 BPF 的参数比较特殊,根据 tracepoint                            位置的不同而不同。  用于在各个 tracepoint 节点运行。                        BPF_PROG_TYPE_XDPstruct xdp_md 用于控制 XDP(eXtreme Data Path)的                            BPF 代码。 BPF_PROG_TYPE_PERF_EVENTstruct bpf_perf_event_data 用于定义 perf event 发生时回调的 BPF 代码。                        BPF_PROG_TYPE_CGROUP_SKBstruct __sk_buff 用于在 network cgroup 中运行的 BPF                            代码。功能上和 Socket_Filter 近似。具体用法可以参考范例 test_cgrp2_attach。                        BPF_PROG_TYPE_CGROUP_SOCKstruct bpf_sock 另一个用于在 network cgroup 中运行的 BPF                            代码,范例 test_cgrp2_sock2 中就展示了一个利用 BPF 来控制 host 和 netns 间通信的例子。                        
深入对比清单 3(eBPF)和清单 2(cBPF)的实现的差异,还会发现一个比较明显的不同之处:BPF 代码进内核之后,cBPF 和内核通讯的方式是                recv();而 eBPF 则将 socket 丢到一边,使用一种名为 map 的全新机制和内核通讯,其大致原理下图所示:
图 4 eBPF 的 map                机制从图上看,这套设计本身不复杂:位于用户空间中的应用在内核中辟出一块空间建立起一个数据库用以和 eBPF                程序交互(bpf_create_map());数据库本身以 Key-Value                的形式进行组织,无论是从用户空间还是内核空间都可以对其进行访问,两边有着相似的接口,最终在逻辑上也都殊途同归。
不难发现,map 带来的最大优势是效率:相对于 cBPF 一言不合就把一个通信报文从内核空间丢出来的豪放,map                机制下的通讯耗费就要小家碧玉的多了:还是以 sockex1 为例,一次通信从内核中仅仅复制 4                个字节,而且还是已经处理好了可以直接拿来就用的,做过内核开发的人都知道这对于性能意味着什么。
map 机制解决的另一个问题是通信数据的多样性问题。cBPF 所覆盖的功能范围很简单,无外乎是网络监控和 seccomp                两块,数据接口设计的粗放一点也就算了;而 eBPF                的利用范围则要广的多,性能调优、内核监控、流量控制什么的应有尽有,数据接口的多样性设计就显得很必要了。下表中就列出了现有 eBPF 中的 map                机制中常见的数据类型:
表 3. map                    机制下的常见数据类型CategorySourceBpf_map_type用途ArrayArraymap.cBPF_MAP_TYPE_ARRAY BPF_MAP_TYPE_CGROUP_ARRAY                            BPF_MAP_TYPE_PERF_EVENT_ARRAY BPF_MAP_TYPE_PERCPU_ARRAY                            BPF_MAP_TYPE_ARRAY_OF_MAPS实际就是数组,所以所有的 key 必须是整数。

BPF_MAP_TYPE_PROG_ARRAY该类型是一个特例,主要用于自定义函数,利用 JUMP_TAIL_CALL令跳转HashHashmap.cBPF_MAP_TYPE_HASH BPF_MAP_TYPE_PERCPU_HASH                            BPF_MAP_TYPE_LRU_HASH BPF_MAP_TYPE_LRU_PERCPU_HASH                            BPF_MAP_TYPE_HASH_OF_MAPS真正意义上的 map 数据类型,如果 key 值为整数以外的类型必须使用Stack TraceStackmap.cBPF_MAP_TYPE_STACK_TRACE真正意义上的 map 数据类型,如果 key                            值为整数以外的类型必须使用存储特定应用在某一特定时间点的栈状态(包括内核态和用户态),key 只有两个:分别为内核栈 id                            和用户栈 id,利用 bpf_get_stackid()获取;Longest Prefix Match                                TrieLpm_trie.cBPF_MAP_TYPE_LPM_TRIE基于 Longest Prefix Match 前缀树实现,适宜处理以 CIBR 为键值时的情况
返回列表