共计 9451 个字符,预计需要花费 24 分钟才能阅读完成。
编者按:BPF 技术还在热火朝天的倒退着,本文先通过对 BPF 常识的介绍,率领大家入门 BPF,而后介绍 coolbpf 的近程编译(原名 LCC,LibbpfCompilerCollection),意为酷玩 BPF,指标把简单的 BPF 开发和编译过程简化,本文还会以一个示例来体验 coolbpf 的享受式开发。本文整顿自龙蜥大讲堂第 20 期,精彩分享视频回放已上传至龙蜥官网(首页 - 动静 - 视频),欢送查看!
一、引言
要实现一个 BPF 二进制程序的开发,须要搭建开发编译环境,要关注指标零碎的内核版本状况,须要把握从 BPF 内核态到用户态程序的编写,以及如何加载、绑定至对应的 HOOK 点期待事件触发,最初再对输入的日志及数据进行解决。
这几个过程对于一个刚接触 BPF 的同学会感觉相当繁琐。本文先通过对 BPF 常识的介绍,率领大家入门 BPF,而后介绍 coolbpf 的近程编译(原名 LCC,LibbpfCompilerCollection,意为酷玩 BPF,指标把简单的 BPF 开发和编译过程简化),以一个示例来体验 coolbpf 的享受式开发。
二、BPF 入门
BPF 最早在 1992 年被提出,过后叫伯克利包过滤器(Berkely Packet Filter,个别称为 cBPF),号称比过后最先进的数据包过滤技术快 20 倍,次要利用场景在 tcpdump、seccomp。
2014 年,Alexei Starovoitov 对 BPF 进行彻底地革新,提出 Extended Berkeley Packet Filter (eBPF)。eBPF 指令更靠近硬件的 ISA,便于晋升性能,提供了可基于零碎或程序事件高效平安执行特定代码的通用能力,通用能力的使用者不再局限于内核开发者。其应用场景不再仅仅是网络分析,能够基于 eBPF 开发性能剖析、零碎追踪、网络优化等多种类型的工具和平台。咱们通常不加区分,把 cBPF 和 eBPF 都称之为 BPF。
2.1 BPF 知识点总结
大家都在谈 BPF,介绍 BPF 的文章也很多,这里先总结 BPF 的知识点,最初咱们从最根底的 BPF 指令架构及 map 和 prog type 去介绍如何疾速入门。
- risc 指令集:蕴含 11 个 64 位寄存器 (R0 ~ R10)。
- maps:BPF 程序之间或内核及用户态间数据交互。
- prog type:BPF 程序类型用来确定程序性能以及程序 attach 到什么地位。
- helper functions:通过辅助函数拜访内核数据,如拜访 task、pid 等。
- jit:将 BPF 程序的字节码转换成指标机的机器码。
- object pinning:提供 BPF 文件系统,缩短 map 和 prog 的生命周期。
- tail call:一个 BPF 程序能够调用另一个 BPF 程序,并且调用实现后不必返回到原来的程序。
- hardening:爱护 BPF 程序和其二进制程序不被毁坏(设置成只读)。
2.2 BPF 倒退详情
BPF 通过几十年的倒退,曾经从原来繁多的性能用处,到现在遍布各个领域的利用,包含云原生、企业服务器、平安零碎等都在使用这一技术,服务生产和生存。下图是 BPF 技术的发展史(图源:https://zhuanlan.zhihu.com/p/…)。
随着越来越多的个性被合入到 Linux 内核社区,BPF 反对的性能因为这些个性加持,曾经越来越丰盛。
2.3 BPF 和内核模块比照
BPF 的劣势是灵便、平安。在没有引入 BPF 之前,如果咱们想要实现一些跟踪诊断的性能,能够应用 tracefs/debugfs 中导出的文件系统接口,或者编写内核模块插入到内核中运行。前者的性能较为固定,无奈灵便地实现对数据的过滤;后者尽管灵便,但容易导致内核 crash,不够平安。ftrace 和 kernel module 给咱们很大的自在去抓取内核的一些信息,然而却存在不少弊病。
下图是内核模块和 BPF 的比照(图源:https://zhuanlan.zhihu.com/p/…)。
2.4 BPF 指令集
BPF(默认指 eBPF 非 cBPF) 程序指令都是 64 位,应用了 11 个 64 位寄存器,32 位称为半寄存器(subregister)和一个程序计数器(program counter),一个大小为 512 字节的 BPF 栈。所有的 BPF 指令都有着雷同的编码方式。eBPF 虚构指令系统属于 RISC,领有 11 个虚构寄存器、r0-r10,在理论运行时,虚构机会把这 11 个寄存器一 一对应于硬件 CPU 的物理寄存器。下图是新老指令的比照:
BPF 指令的外围构造体如下,每一条 eBPF 指令都以一个 bpf_insn 来示意,在 cBPF 中是其余的一个构造体 (struct sock_filter),不过最终都会转换成对立的格局,这里咱们只钻研 eBPF:
由构造体中的__u8 code 能够晓得,一条 BPF 指令是 8 个字节长。这 8 位的 code,第 0、1、2 位示意的是该操作指令的类别,共 8 种:
从最低位到最高位别离是:
- 8 位的 opcode;有 BPF_X 类型的基于寄存器的指令,也有 BPF_K 类型的基于立刻数的指令
- 4 位的指标寄存器 (dst)
- 4 位的原始寄存器 (src)
- 16 位的偏移(有符号),是绝对于栈、映射值(map values)、数据包(packet data)等的绝对偏移量
- 32 位的立刻数 (imm)(有符号)
8 bit 的 opcode 进一步拆开,下图示意的是存储和加载类指令:
下图示意的是运算和跳转指令:
总之,BPF 指令很简洁,咱们未必会在开发过程中应用它来进行代码编写(相似于纯汇编,会让人解体),理解这些是有助于咱们更粗浅的了解 BPF 的运行原理。
2.5 BPF 的 prog type 和 map
PROG TYPE
BPF 相干的程序,首先须要设置为绝对应的的程序类型,截止 Linux 内核 5.8 程序类型定义有 29 个,而且还在继续减少中,BPF 程序类型(prog_type)决定了程序能够调用的内核辅助函数的子集,也决定了程序输出上下文 — bpf_context 构造的格局。
咱们常常应用 BPF 程序类型次要波及以下两类:
- 跟踪
大部 BPF 程序都是这一类,次要通过 kprobe、tracepoint(rawtracepoint)等追踪零碎行为及获取零碎硬件信息。也能够拜访特定程序的内存区域,从运行过程中提取执行跟踪信息。
- 网络
这类 BPF 程序用于检测和控制系统的网络流量。能够对网络接口数据包进行过滤,甚至能够齐全回绝数据包。
用户态是通过零碎调用来加载 BPF 程序到内核的,在加载程序时,须要传递的参数中有一个字段叫 prog_type,这个就是 BPF 的程序类型,跟踪相干的是:BPF_PROG_TYPE_KPROBE 和 BPF_PROG_TYPE_TRACEPOINT,网络相干是:BPF_PROG_TYPE_SK_SKB、BPF_PROG_TYPE_SOCK_OPS 等。上面是形容 BPF 程序类型的枚举构造:
enum bpf_prog_type {
BPF_PROG_TYPE_UNSPEC, /* Reserve 0 as invalid program type */
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,
/* See /usr/include/linux/bpf.h for the full list. */
};
BPF MAP
BPF 的 map 可用于内核 BPF 程序和用户应用程序之间实现双向的数据交换,是重要根底数据结构,它能够通过申明 struct bpf_map_def 构造实现创立。
对于 BPF 最吸引人的一个方面,就是运行在内核上的程序能够在运行时应用消息传递互相通信,而 BPF Map 就是用户空间和内核空间之间的数据交换、信息传递的桥梁。
BPF Map 实质上是以键 / 值形式存储在内核中的数据结构。在内核空间的程序创立 BPF Map 并返回对应的文件描述符,在用户空间运行的程序就能够通过这个文件描述符来拜访并操作 BPF Map。
依据申请内存形式的不同,BPF Map 有很多种类型,罕用的类型是 BPF_MAP_TYPE_HASH 和 BPF_MAP_TYPE_ARRAY,它们背地的内存治理形式跟咱们相熟的哈希表和数组基本一致。随着多 CPU 架构的成熟倒退,BPF Map 也引入了 per-cpu 类型,如 BPF_MAP_TYPE_PERCPU_HASH、BPF_MAP_TYPE_PERCPU_ARRAY 等,每个 CPU 都会存储并看到它本人的 Map 数据,从属于不同 CPU 之间的数据是相互隔离的。
上面是形容 BPF map 的枚举构造:
enum bpf_map_type {
BPF_MAP_TYPE_UNSPEC,
BPF_MAP_TYPE_HASH,
BPF_MAP_TYPE_ARRAY,
BPF_MAP_TYPE_PROG_ARRAY,
BPF_MAP_TYPE_PERF_EVENT_ARRAY,
BPF_MAP_TYPE_PERCPU_HASH,
BPF_MAP_TYPE_PERCPU_ARRAY,
BPF_MAP_TYPE_STACK_TRACE,
BPF_MAP_TYPE_CGROUP_ARRAY,
BPF_MAP_TYPE_LRU_HASH,
BPF_MAP_TYPE_LRU_PERCPU_HASH,
BPF_MAP_TYPE_LPM_TRIE,
BPF_MAP_TYPE_ARRAY_OF_MAPS,
BPF_MAP_TYPE_HASH_OF_MAPS,
BPF_MAP_TYPE_DEVMAP,
BPF_MAP_TYPE_SOCKMAP,
BPF_MAP_TYPE_CPUMAP,
BPF_MAP_TYPE_XSKMAP,
BPF_MAP_TYPE_SOCKHASH,
BPF_MAP_TYPE_CGROUP_STORAGE,
BPF_MAP_TYPE_REUSEPORT_SOCKARRAY,
BPF_MAP_TYPE_PERCPU_CGROUP_STORAGE,
BPF_MAP_TYPE_QUEUE,
BPF_MAP_TYPE_STACK,
BPF_MAP_TYPE_SK_STORAGE,
BPF_MAP_TYPE_DEVMAP_HASH,
BPF_MAP_TYPE_STRUCT_OPS,
BPF_MAP_TYPE_RINGBUF,
BPF_MAP_TYPE_INODE_STORAGE,
BPF_MAP_TYPE_TASK_STORAGE,
BPF_MAP_TYPE_BLOOM_FILTER,
};
三、BPF 的开发姿态
3.1 BPF 开发框架
上图就是十分经典的 BPF 开发框架图了,个别开发流程都是先将用户编写的特定程序通过 LLVM 编译为 BPF 字节码,在注入到内核中时会通过 verifier 严格的查看,确保代码不会呈现死循环、宕机之类的问题,而后再通过 jit 将其翻译为 native code 进行执行,用户能够通过查看内核透出的数据,理解零碎的运行状况。
尽管 BPF 有十分多的利用,但应用的时候也有许多的限度。比方:
- 不能有不确定的循环操作;内核要确保执行流肯定能够从 BPF 程序中进去。
- 不容许睡眠;睡眠可能导致内核执行流始终出不来。
- 不容许批改内核数据结构;个别不能批改数据报文。
- 栈空间无限;以后只有 512 字节。
- 不容许被间接调用内核函数;必须通过辅助函数。
3.2 开源工具和平台
BPF 的编程分为两局部,一部分是运行在内核态,这是 BPF 编程的外围;另一部分是运行在用户态,这部分代码次要用来加载 BPF,采集并解决数据等。
libbpf
libbpf 是官网的用户态库。和编译一般的 c 程序会失去一个.o 文件一样,LLVM 编译完 BPF 程序也会失去一个的.o 文件 (个别咱们命名为 xx.bpf.o)。这个 bpf.o 文件依照 elf 的格局组织 BPF 程序的字节码、map 定义以及符号等信息,libbpf 库负责解析这些信息,创立 map,注入 BPF 程序到内核工作。因而,对于用户来说,须要做很多繁琐的工作:
- 用 C 语言来编写 BPF 程序;
- 编写 makefile,调用 LLVM 编译生成 .o 文件;
- 调用 libbpf 的接口加载 .o 文件;
- 应用 libbpf 的接口获取 map 中的数据。
BCC
BCC 是现在最热门也是对老手最敌对的开源平台。它用 python 封装了编译、加载和读取数据的过程,提供了很多十分好用的 API。
和 libbpf 须要提前把 bpf 程序编译成 bpf.o 不同,BCC 是在运行时才调用 LLVM 进行编译的,所以要求用户环境上有 llvm 和 kernel-devel。这个就会像前面咱们提到的,它会呈现运行时偏移导致的刹时资源冲高问题。
bpftrace
bpftrace 是单命令工具,让用户像应用脚本一样应用 BPF。它自定义了本人的 DSL 作为前端,底层也是调用 LLVM 的。事实上,它依赖于 BCC 提供的 libbcc.so 文件。
3.3 libbpf CORE
后面提到 libbpf 开发,成为以后最支流的开发方式,然而须要为每个内核版本都开发特定的二进制程序,不能达到同一个 BPF 程序在不同内核版本上平安运行。因为不同内核版本数据的内存布局不同,就须要反对 CORE(CompileOnce、Runeverywhere),提到 CORE 咱们要先来理解一下 BTF。
BTF(BPF Type Format)是一种相似于 DWARF 的格局,专用于形容程序中数据类型。其次要存在于两个中央:
一是 vmlinux 镜像内。
二是用户编写的 BPF 程序内。
二者存在着肯定的差别。第一个差别是:BPF 程序内除了存在 .btf 段外,还存在.btf.ext 段,专用于记录 BPF 程序内应用的数据类型的状况。第二个差别是:vmlinux 镜像内的 BTF 程序由原存在于该镜像内的 dwarf 信息简化而来,而 BPF 程序内的 BTF 段由 CLANG 编译器生成,须要在编译时指定 –target bpf。利用 BPF 程序里的 BTF 段和寄存在每个零碎上的 BTF 文件,咱们将这些信息进行重定位,就能确定每个数据结构的偏移,达到 CORE 的目标。
要害组件:
- BTF: 形容内核镜像,获取内核及 BPF 程序类型和代码的要害信息 (http://pylcc.openanolis.cn/)
- Clang 开释 bpf 程序重定位信息到 .btf 段
- Libbpf CO-RE 依据 .btf 段重定位 bpf 程序
目前在 libbpf CO-RE 中须要进行重定位的信息次要有三类:
1)构造体相干重定位,这部分和 BTF 非亲非故;
Clang 通过 __builtin_preserve_access_index() 记录成员偏移量
u64 inode = task->mm->exe_file->f_inode->i_ino;
u64 inode = BPF_CORE_READ(task, mm, exe_file, f_inode, i_ino);
2)map fd、全局变量、extern 等重定位,这部分次要依赖于 ELF 重定位机制。通过查找 ELF 重定位段收集重定位信息,更新相应指令的 imm 字段。
skel->rodata->my_cfg.feature_enabled = true;
skel->rodata->my_cfg.pid_to_filter = 123;
extern u32 LINUX_KERNEL_VERSION __kconfig;
extern u32 CONFIG_HZ __kconfig;
3)子函数重定位,也是依赖于 ELF 重定位机制。然而目标不一样,子函数重定位是为了将 eBPF 程序调用的子函数同主函数放在同一块间断的内存中,便于一起加载到内核。例如将所有子程序拷贝到主程序所在区域,always_inline 函数。
libbpf CORE 开发步骤:
1)生成带所有内核类型的头文件 vmlinux.h
bpftoolbtf dump file vmlinux format c > vmlinux.h
2)应用 Clang (版本 10 或更新版本) 将 BPF 程序的源代码编译为 .o 对象文件;
3)从编译好的 BPF 对象文件中生成 BPF skeleton 头文件 bpftool gen 命令生成;
4)在用户空间代码中蕴含生成的 BPF skeleton 头文件;
5)编译用户空间代码,这样会嵌入 BPF 对象代码,后续就不必公布独自的文件。
6)生成的 BPF skeleton 应用如下步骤加载、绑定、销毁:
<name>__open() – 创立并关上 BPF 利用,之后能够设置 skel->rodata 变量。
<name>__load() – 初始化,加载和校验 BPF 利用局部。
<name>__attach() – 附加所有能够主动附加的 BPF 程序 ( 可选,能够间接应用 libbpf API 作更多管制)。
<name>__destroy() – 拆散所有的 BPF 程序并应用其应用的所有资源。
四、coolbpf 享受式开发
后面咱们曾经对 BPF 有了一个意识,到此曾经入门了,同时也学习了 BPF 的高阶常识:libbpf 的 CORE,它也是将来的一个方向,然而咱们也看到这外面,还须要写一堆代码:open、load、attach 等等。咱们是否把这所有进行简化呢?能不能享受式的进行开发。别急,先来看看罕用的开发方式:
4.1 BPF 开发罕用计划比照
1)原生 libbpf,无 CO-RE (内核 samples/bpf 示例)
劣势:资源占用量低
毛病:
- 须要搭建代码工程、开发效率低;
- 不同内核版本兼容性差;
2)BCC(BPF Compile Collection、python 代码)
劣势:开发效率高,可移植性好,反对动静批改内核局部代码
毛病:
- 部署依赖的 Clang/LLVM;
- 每次运行都要执行 Clang/LLVM 编译,争抢内存 CPU 内存等资源;
- 依赖指标环境头文件;
3)BPF CO-RE(libbpf-tools 上面的代码)
劣势:不依赖在环境中部署 Clang/LLVM,资源占用少
毛病:
- 仍须要搭建编译编译工程;
- 局部代码绝对固定,无奈动静配置;
- 用户态开发反对信息较少,不足高级语言对接;
综上所述,上述计划不能很好适配生产环境中,多内核并存、疾速批量部署等需要
4.2 Coolbpf(能够酷玩的 BPF)解决的问题
通过将 BPF 的三种开发方式比照,咱们发现都不能完满的在生产环境中解决如下几个问题:
- 装置依赖库和内核头文件;
- CPU 和内存等资源刹时冲高;
- BTF 须要按版本随 BPF 二进制程序公布。
为解决这几个问题,咱们提出一个 coolbpf 的开发编译平台,目前蕴含 pylcc、rlcc、golcc 等目录,别离是高级语言 python、rust 和 go 语言反对近程和本地编译的能力。
代码链接地址:
https://gitee.com/anolis/coolbpf
https://github.com/aliyun/coo…
这里不对 coolbpf 过多介绍,具体内容请参考《龙蜥社区开源 coolbpf,BPF 程序开发效率晋升百倍》。
4.3 pyLCC 的享受式
为了介绍什么叫享受式开发,在这里咱们拿 coolbpf 的 pyLCC 进行演示:
import sys
from pylcc.lbcBase import ClbcBase, CexecCmd //import pylcc base 库
bpfProg = r"""
struct data_t {
int cpu;
int type; // 0: irq, 1:sirq
u32 stack_id;
u64 delayed;
};
LBC_PERF_OUTPUT(e_out, struct data_t, 128); // 定义 perf event output array map
LBC_STACK(call_stack, 256); // 定义 stack 的 map
SEC("kprobe/check_timer_delay")
int j_check_timer_delay(struct pt_regs *ctx)
{struct data_t data = {};
data.cpu = PT_REGS_PARM2(ctx);
data.type = PT_REGS_PARM1(ctx);
data.delayed = PT_REGS_PARM3(ctx);
data.stack_id = bpf_get_stackid(ctx, &call_stack, KERN_STACKID_FLAGS);
bpf_perf_event_output(ctx, &e_out, BPF_F_CURRENT_CPU, &data, sizeof(data));
return 0;
}
"""
class Crunlatency(ClbcBase):
def __init__(self, lat=10):
self._exec = CexecCmd
self.setupKo(lat >> 1)
// 只须要简略的 init,就能够把 open load attach 等动作做好,而后专一于数据处理
super(Crunlatency, self).__init__("runlatency", bpf_str=bpfProg)
def _cb(self, cpu, data, size):
stacks = self.maps['call_stack'].getStacks(e.stack_id)
print("call trace:") //call back 函数里分心解决数据
for s in stacks:
print(s)
大家看到下面的示例,只须要以下三步,就能够实现一个程序的开发:
1)pip install coolbpf。
2)编写 bpf.c 代码。
3)编写 python,通过 init() 加载之后,就专一性能开发。
你能够不必关怀 BPF 的汇编、字节码,也不必装置 Clang,不必装置 kernel-dev 头文件,不必本人生成 BTF 文件(它会主动到近程服务器下载),只需专一你的性能开发,比方剖析网络流量、监控文件关上和敞开、跟踪零碎调用。
总结来看,BPF 技术还在热火朝天的倒退着,但只有把握了这些根底的知识点,就可能举一反三,利用好已有的工具或平台,更加能锦上添花。通过后面的介绍,咱们不仅对 BPF 有了比拟粗浅的了解,还能借助 coolbpf,十分酷的玩了一把享受式的开发,简洁如此,谁能不爱呢?
对于回放和课件获取
【视频回放】:视频回访已上传至龙蜥官网:https://openanolis.cn/video 查看。
【PPT 课件获取】:关注微信公众号(OpenAnolis),回复“龙蜥课件”即可获取。有任何疑问请随时征询龙蜥助手—小龙(微信:openanolis_assis)。
—— 完 ——