首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | 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 了。在samples/bpf/Makefile中,可以看到:
清单 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_TYPE_SOCKET_FILTERBPF_PROG_TYPE_KPROBEBPF_PROG_TYPE_TRACEPOINTBPF_PROG_TYPE_XDPBPF_PROG_TYPE_PERF_EVENTBPF_PROG_TYPE_CGROUP_SKBBPF_PROG_TYPE_CGROUP_SOCK
深入对比清单 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                    机制下的常见数据类型CategorySourceArrayArraymap.c

HashHashmap.cStack TraceStackmap.cLongest Prefix Match                                TrieLpm_trie.c
返回列表