概述

eBPF (Extended Berkeley Packet Filter) 自从在 2014 年下半年被引入 Linux 内核后,迅速被许多子系统使用起来。但是,仅仅是通过 eBPF 的使用案例而不是基于它的设计来理解 eBPF 是一件很困难的事情。而且,eBPF 这个名称在字面上表达了使用包过滤技术,实际上它已经跟网络没有太大关系。这篇文章将通过讲述 eBPF 的过去,现状和将来,向你介绍 eBPF 的起源和随着它的持续开发有怎样的发展前景。 [编辑] 介绍

BPF 是“扩展的伯克利包过滤”的简写。很难用一个整体的维度去解释 eBPF。因为它用跟以前完全不同的方式去解决同样的问题。而且它的名字具有争议。当有人第一次得知 eBPF 时,他们很可能不知道 BPF 是什么而扩展 (extended) 又代表什么。Berkely 这个词除了代表一个地名没有其他作用。在它的名字中能让人想到有意义的词就只有“包过滤”。但这并没有展现 eBPF 除了网络过滤以外可以做的功能。Linux 社区在他们没有意识到 eBPF 会广泛传播前就取了这个名字。

在内核中,eBPF 的核心是一个高效虚拟机。为了能高效地过滤网络帧而发明了这个虚拟机。因此,eBPF 可以作为一个理想的引擎,处理通用的事件。由于这个原因,目前有12个不同类型的 eBPF 程序 (在写这篇文章是,这些程序中大多数跟网络无关) [编辑] 术语

BPF: 伯克利包过滤,诞生于 FreeBSD eBPF: 扩展的伯克利包过滤, 诞生于 Linux eBPF-map: 通用术语,用于总体描述多个 eBPF 中用于存储数据的类型。关于 eBPF 的文档中,会将所有的 eBPF 存储类型都称之为 map,即使他们中有一些数据类型并不是 map。因此本篇文章中也将使用 eBPF-map 代表 eBPF 的存储类型 Network Frame:数据链路层的单元数据 Network Packet:网络层的单元数据 Network Segment:传输层的单元数据 [编辑] BPF 和 FreeBSD 的过去

理解 Linux 中的 eBPF 需要从另外一个截然不同的操作系统讲起:FreeBSD。 在 1993 年,一篇名叫 “BSD 包过滤 - 一个全新用户层包捕捉架构 McCanne & Jacobson” 的论文在 1993 年的冬季 USENIX 大会中发表。论文的作者分别是 Steven McCanne 和 Van Jacobson, 他们当时在劳伦斯伯克利国家实验室工作。论文中,他们描述了 BSD 包过滤技术,内核中如何实现以及虚拟机实现。

BPF 与它的前辈们(比如 CMU/Stanford Packet Filter (CSPF) )截然不同的地方在于,它使用了一个构想的内存模型,然后将其导入到一个存在于内核的虚拟机中。这样一来,BPF 过滤器可以以一种高效的方式运行,与此同时与内核自身的代码保持边界。

McCanne 与 Jacobson 在论文中通过下面的图片来展示了他们的实现。BPF 在过滤器和用户空间中使用了一个缓冲,来避免针对每个包过滤而产生的用户空间与内核空间切换

Image:Bpf layout diagram.png

McCanne 与 Jacobson 意义深远地通过 5 条声明定义了 BPF 虚拟机实现

协议独立。内核不需要经过修改来支持新的协议
    通用。指令集必须足够去处理未预见的使用场景
    包引用必é¡»最小化
        解码指令必须包含一个单独的 C 语言 switch 语句
	    抽象的机器寄存器应该存在于物理寄存器上 


	    以上对扩展性、通用性和性能的强调很有可能就是为什么 eBPF 能比 BPF 涵盖很多使用场景的原因。即使如此,eBPF 虚拟机依然非常紧凑,它包含一个加法运算,一个寻址寄存器,一个暂存存储器和一个隐含的程序计数器。可以通过下面的实例程序来看,该实例程序匹配所有的网络层数据包,指令右侧包含它解释

	    ```

	       ldh [12]                    // 读取网络类型到寄存器
	       jeq #ETHERTYPE_IP, L1, L2   // 对比网络类型是否是IP
	       L1: ret #TRUE // 如果对比成功 返回 true 
	       L2: ret #0 // 如果对比失败 返回 0 

	       ```

	       仅需要 4 条虚拟机指令, BPF 就可以提供一个极有用的 IP 包过滤器。这个设计极其高效的,这个原始的 BPF 实现能只用 184 个 CPU 指令来实现丢弃一个包的功能。这个指令计数是包括 bpf_tap 调用的开始和结束,也就意味着包含丢弃包的行为。通过这个,BPF 能让一个处理器拥有媲美新型图形处理器才有的能力——高达 2.6 Gbps 网络流量处理。这个性能可以与上下文切产生的性能损耗来对比一下,在一个 SPARCstation 2 的机器上,根据进程强占的数量以及上下文切换所需的空间大小,一次上下文切换可能需要使用 2,840 到 16,000+ 个指令周期(相当于 71 微秒到 400+ 微秒)。

	       最后,他们论文中的两件事情值得一提。首先,BPF 在他们的论文发布时已经使用了大约两年。这说明了 BPF 的开发是一个渐进的过程,它不断地再使用新科技充实它自己。其次,在写论文的时候,tcpdump 使用了 BPF 技术。也就是说,目前 tcpdump 作为被广泛用于网络问题排查的工具,已经使用 BPF 技术 24 年之久了。我提及这个的原因是因为目前没有文章描述 BPF 这个技术家族已经被使用了多久. 而当 McCanne 与 Jacobson 的论文被公布时, BPF 并不是一个俏皮的实验产品。它已经被验证并使用了 2 年,已经被许多工具所使用。

	       那些对 McCanne 与 Jacobson 的论文感兴趣的人,可以去看下,论文只有11页。
	       [编辑] 现状

	       当 Linux 内核 3.18 在 2014 年 12 月发行出来时,它包含了 eBPF 的第一个实现版本。简而言之,eBPF 是一个存在于内核的虚拟机,虽然类似 BPF,但还有其他特性。一, 由于运行时编译(JIT)的原因,eBPF 比 BPF 更加高效。二,它被设计成可以处理通用的事件。这样一来,内核开发者可以将 eBPF 整合到任何内核部件中。三,它包含了一个高效的全局数据储存类型 —— maps。这个可以将状态保持在事件之间,从而可以在某些使用场景被聚合或者感知上下文。值得一提的是,自从 eBPF 诞生后,有许多存储类型被创建,虽然并不全是 maps 类型。然后,map 这个术语被保留下来,作为一个通用的名称来指代这些数据类型。

	       内核开发者有了这些能力后,他们迅速各个内核部件中使用 eBPF。在不到两年半的时间里,它已经被包含在网络监控,网络流量计算和系统监控。因此,eBPF 程序可以用来开关网络传输,测量磁盘读取或者记录 CPU 缓存未命中。

	       理解 eBPF 当前实现的最快途径就是理解 eBPF 运行所需要的东西。eBPF 程序运行过程可以分解成三个部分

	       1. 以字节码的形式创建 eBPF 程序。当前,创建eBPF程序的标准方式是将它们以C语言的形式编写下来,然后使用 LLVM 编译器将它们编译成 eBPF 的字节码,并存放在 ELF 格式的文件中。因为 eBPF 虚拟机的设计有良好的文档和代码,并且有大量基于它们的开源工具。所以,也完全可以手动编排 eBPF 寄存器来实现,或者自定义一个 eBPF 的编译器。因为 eBPF 将内建的方法函数设计成极其简单的形式,而不是设计成一个入口以提供跳转,所以 LLVM 只需要将方法内嵌编译进去。

	       2. 加载程序到内核中并为之创建必要的 eBPF-map。通过 Linux 的系统调用 bpf 来实现。这个系统调用允许将字节码和 eBPF 程序的类型参数一起被加载进来。在写这篇文章时,eBPF 支持以套接字过滤器,kprobe 句柄,traffic control scheduler,traffic control action,tracepoint 句柄,eXpress Data Path(XDP),性能监控,cgroup restriction 和 轻量权重 tunnel 等类型加载。这个系统调用同样可以用来初始化 eBPF-maps。

	       3. 将被加载的程序联接到系统。因为不同的 eBPF 程序使用在不同的 Linux 内核子系统,不同的 eBPF 程序类型有不同的方式来联接到对应的系统上。当程序被联接上后,它会被激活,开始过滤,根据程序定义的逻辑开始分析或者捕捉信息。从这儿开始,用户层的程序就可以通过从 eBPF-maps 数据中读取状态,并根据这些状态开管理如何运行 eBPF 程序,或者通过操作 eBPF-maps 来影响 eBPF 的行为。

	       这个三个步骤在概念上非常简单,但与实际使用上还有点细微区别。后续的文章会描述更加细节的部分。当下,需要了解到有工具已经可以提供便利的方式来使用 eBPF,这些工具可能会让你以与 eBPF 原生方式不同的方式来使用 eBPF。就跟很多 Linux 的东西一样,或许在内核内部不依赖任何工具的情况下使用 eBPF 会比在外部更好。要记住,eBPF 的通用性也是导致它极其灵活和使用上复杂的原因。

	       来快速总结一下,eBPF 在 2014 才会引入 Linux 内核,却已经在许多使用场景中展现它高效的事件处理能力。通过 eBPF-map,eBPF 程序可以维护状态,然后根据事件来聚合信息,同时能拥有动态的逻辑。eBPF 的使用将因为它最小化的实现和轻量的运行性能,变得更加广泛。现在,让我们继续展望 eBPF 的未来
	       [编辑] 智能网卡和内核态程序:未来

	       在短短的几年时间,eBPF 就被整合到Linux内核的各个部件中。这让 eBPF 的前景变得有趣又模糊。因为存在两个关于将来的问题: 未来如何去使用 eBPF 和发展 eBPF 技术

	       如何使用 eBPF 在当下看来已经非常确定了。在 Linux 内核中,将继续坚持以安全,高效,事件处理这些功能点来维护 eBPF。因为 eBPF 拥有简洁的虚拟机实现,它也很容易在其他场景被使用。截止目前,最有趣的场景之一是在智能网卡中。

	       智能网卡可以在不同的层次处理网络流量,最终将网络包卸载后发送给网卡自身。在2000 年早期,这个想法就被提及,当时,有些网卡开始支持做网络包校验,包分解合并等功能,但直到最近才在所有/部分的数据平面上实现卸载功能。这类新品种的智能网卡有许多别名,如智能服务适配器,但基本都包含一些通用可编程特性和大量的内存空间。其中一个产品例子就是 Netronome’s Agilio CX 产品线的智能网卡,它目前支持 10Gbps 到 40Gbps 的端口,配置一个 ARM11 处理器,2GB 的 DDR3 内存和超过 100 个定制化处理核心。

	       因为有强大的处理能力,近期智能网卡成为 eBPF 的一个使用场景。在这个场景下,可以通过提供一些基本的方法,用来解决不同的需求,比如缓解 DoS 攻击,提供动态网络路由,网络交换,负载均衡等等。在 2016 年 10 月的 NetDev 1.2 大会上,Netronome 公司的 Jakub Kicinski 和 Nic Viljoen 做了一个名叫“智能网卡的 eBPF/XDP 硬件卸载”演讲。其中,Nic Viljoen 陈述了一些早期和粗糙的性能测试,实现了 3 百万个数据包每秒每 FPC 的处理能力。Nic Viljoen 继续指出,每个智能网卡中拥有 72 到 120 个这样的 FPC,给出了这样一个非现实的假设,eBPF 最大吞吐达到 4.3 Tbps!

	       最后来讲讲 eBPF 技术的将来。eBPF 的限制和简单实现提供了一种可移植行和高效处理事件的能力。不仅如此,eBPF 创造一种解决问题的途径。它不需要对象和有状态的代码,只需要方法和高效的存储类型。这极大缩小了一个程序的功能,但这么做却带来极大的兼容性。因此,eBPF 可以以各种方式运行,如同步,异步,并发,分布式和其他程序运行方式。因为这些原因,我认为,“方法虚拟机” FVM 这个名称更加适合 eBPF。

	       eBPF 引入的另一个最大的技术改变就是运行时编译(JIT)。这让 JIT 能将字节码编译到内核空间进行运行。因为硬件隔离的原因,内核限制了用户程序,这导致用户程序性能下降了大约 25% 到 33%,而通过 eBPF 这个问题将被解决。这就意味着,用户程序将通过运行在内核空间的虚拟机而提高 50% 的运行速度。实际上,这个点子也不是新点子。在 2014 年的 PyCon ,Gary Bernhardt 提出了一个有趣的演讲——“Javascript的诞生和消亡”。在他的演讲中,Gary Bernhardt 提到上述相同的硬件隔离性能损耗,他说在一个科幻的未来,主要的软件将运行在一个存在与内核空间的 javascript 虚拟机中。在这样的一个未来,软件可移植性将不在是一个问题,因为软件都不再编译成对应的硬件架构,而只是 javascript。他甚至继续说到,一个DOM(在浏览器展现图形化页面所需的数据对象)的实现已经被移到了内核中。这个在概念上与 eBPF-maps 非常相似。不可否认 Bernhardt 的演讲观点是靠谱的,将来计算程序将通过内核虚拟机来实现隔离,而不用依赖硬件。我们拭目以待,eBPF 是否能成为这个实现的佼佼者。