【摘要】eBPF带来的最大的好处即是可以对内核进行编程性处理,实现对内核中不灵活的部分,实现自定义的处理。这种灵活性使得对于不可见的kernel具有了可观测性的基础,在进行内核监控、网络故障分析、文件系统分析等方面具有广泛的使用前景。本文将带你了解eBPF发展历程、原理、用途及特点。

【作者】陈成,中国联通软件研究院容器云研发工程师,公共平台与架构研发事业部云计算研发组长,长期从事大规模基础平台建设相关工作,先后从事Mesos、KVM、K8S等研究,专注于容器云计算框架、集群调度、虚拟化等。

 

 

eBPF对应用开发者而言,可能些许陌生,但是对于从事云、网络、操作系统等相关的开发者而言,近年来或多或少听说过或使用过。那么eBPF到底是什么?它有何作用?如何使用?今天这篇文章就来一一解答。

 

1、eBPF发展历程及原理

eBPF(extended Berkeley Packet Filter),意为“可扩展数据包过滤器”,是一种网络数据包的过滤技术。在eBPF出现前,BPF (也称为cBPF , classic BPF ) 得到了广泛的应用,BPF 最初的名字 BSD Packet Filter ,源于BSD操作系统上的一种包过滤机制,后经发展,被作者的工作单位名所替代,变成了 Berkeley Packet Filter。BPF支持数据包筛选,允许用户态程序提供要接收的数据包条件。最常用的tcpdump就是基于BPF 实现 的过滤出tcp包或udp包等功能。由于BPF实现了在内核直接过滤处理的能力,因此避免了不符合条件的数据包被“多余地”复制到用户态,从而大大提高了性能。

eBPF,作为扩展版本的BPF,支持JIT机制,动态地将用户态自定义指令解释为机器代码,并在内核中执行,从而大大提高了灵活性。在eBPF之前,内核模块是注入内核的最主要机制。由于缺乏对内核模块的安全控制,内核的基本功能很容易被一个有缺陷的内核模块破坏。而eBPF则借助即时编译器(JIT),并在kernel预先控制好开放的植入范围及校验程序,保证只有被验证安全的eBPF指令才会被内核执行。

eBPF伴随着kernel的发展,逐步完善和成熟,其发展历程大致如下:

  • 1992年,BPF诞生,初衷是提供一种内核中自定义报文过滤的手段
  • 2011年,kernel 3.2版本对BPF重大改版,引入BPF JIT,性能得到提高
  • 2014年,kernel 3.15版本,eBPF出现,引入了verify、高级编程语言编程支持等
  • 2016年,kernel 4.8版本,eBPF支持XDP
  • 2018年,kernel 4.18版本,引入BTF
  • 2020年,kernel 4.20版本,引入CO-RE
  • 2020年,kernel 5.2版本,引入debug_btf

2、eBPF用途及特点

为什么使用eBPF?
  • 内核可编程性
    eBPF带来的最大的好处即是可以对内核进行编程性处理,实现对内核中不灵活的部分,实现自定义的处理,这大大提高了灵活性。
  • 广泛应用场景
    正是由于这种灵活性,使得对于不可见的kernel具有了可观测性的基础,在进行内核监控、网络故障分析、文件系统分析等方面具有广泛的使用前景。

正是由于这些突出的特性,eBPF程序可以附加到各种内核子系统,包括网络,跟踪和Linux安全模块(LSM)。比如,Facebook开源的高性能网络负载均衡器Katran、Isovalent开源的容器网络方案 Cilium ,以及著名的内核跟踪排错工具 BCC 和 bpftrace 等,都是基于eBPF技术实现的。更有意思的是,盘古实验室发现了NSA也使用BPF技术在Linux布置后门。

下图为内核跟踪排错工具BCC所支持的追踪点示意图,可见当前内核中除了驱动部分外的几乎所有子系统、高级语言、应用均已支持细粒度的跟踪分析:

 

[转帖]一文入门前景广阔的 eBPF_Go

 

▲BCC工具集 ( 来源:BPF之巅:洞悉Linux系统和应用性能)

那么,eBPF程序又是如何工作的呢?

 

3、eBPF工作流程

在了解eBPF之前,需要先了解钩子(hook)机制,钩子机制顾名思义即可以挂载一种未确定的逻辑处理过程以实现对特定位置的处理,这种未确定性即灵活性,可以实现定制化或配置化管理,实现不同的需求。在linux内核中有一套hook函数机制(khook),可以在不同的hook点监控或处理如:文件打开、关闭等操作;网络包过滤、转换。最常见的netfilter就是借助于一整套的hook函数机制,实现了数据包在三层以上的过滤、NAT转换、基于协议的跟踪等等,具体参考:linux netfilter hook

eBPF程序是事件驱动的,当内核或应用程序通过某个钩子点(hook point)时触发执行。预定义的钩子包括:系统调用、函数进入/退出、内核跟踪点、网络事件等。

 

[转帖]一文入门前景广阔的 eBPF_加载_02

 

▲hook architecture

如果不存在用于特定需求的预定义钩子,则可以创建内核探测器(kprobe)或用户探测器(uprobe)以将eBPF程序附加到内核或用户应用程序中的几乎任何位置。

 

[转帖]一文入门前景广阔的 eBPF_Go_03

 

▲eBPF hook overview

虽然现在我们依然不清楚如何写一个钩子处理函数,即eBPF程序的具体逻辑,不用担心,我们假设已经有了这样一个程序,那它会如何被处理,并执行起来呢?

 

[转帖]一文入门前景广阔的 eBPF_Go_04

 

▲eBPF全流程调用 ( 以Go语言为例)

上图展示了一个程序是如何被加载、验证并执行的,具体来看,经历了如下步骤:

  • 第一步:编写的c eBPF程序,经过编译器,编译为eBPF伪代码,即eBPF字节码(具体会在第二讲中进行介绍)
  • 第二步:编译好的代码,会被eBPF所对应的高级语言库程序加载,并由高级语言进行系统调用处理(目前eBPF支持golang、python、c/c++、rust等)
  • 第三步:通过系统调用陷入内核后,首先由内核eBPF程序进行验证(verify),这一步确保(1)程序本身无误(不会崩溃、不会出现死循环);(2)没有权限异常;然后进行将编译为eBPF伪代码的程序再转换为具体的机器指令集,以优化程序执行,并最终挂载到对应的hook点或追踪点。
  • 第四步:内核在处理某个追踪点时,刚好有eBPF,就会触发事件,并由加载的eBPF程序处理

 

4、eBPF的不足

但是我们要知道的是eBPF不是万能的,受限于本身内核版本支持能力、eBPF指令的种类、eBPF虚拟机结构等,eBPF程序并不能发挥其无限的能力,仅仅是“山顶跳舞”。

不过,基于这广泛的应用,无论是linux kernel社区,还是工具社区,都在试图突破这些限制,例如将高版本kernel才具备的能力以低成本的方式移植到低版本上,或者是实现一次编译,到处运行(CO-RE)等。

eBPF开发工具

  • BCC
  • bpftrace
  • eBPF Go Library
  • libbpf C/C++ Library

参考文献