2023-从石器时代到成为“神”,一文讲透 eBPF 技术发展演进史

从石器时代到成为“神”,一文讲透 eBPF 技术发展演进史

1.前言

技术的发展往往是积跬步而至千里的。Linux 从 1992 年诞生,发展至今已经覆盖大小各类的信息基础设施。是什么样的力量让 Linux 能够始终保持发展活力?又该如何看待 Linux 之上出现的新的技术趋势?

本文试图通过梳理eBPF的演进过程,探索 Linux 内核的发展动力来源与发展轨迹,与大家一同畅想 eBPF 给内核技术、Linux 生态带来的全新变局。

2.eBPF 技术概览

2.1.实现原理

大家可能都知道图灵机,这是一个可计算理论模型,可以用来判断计算机的计算能力。图灵机是目前有可能实现的计算能力最强的理论模型,目前我们常用的计算机,理论上都是等价于图灵机的。

BPF 的出现是对计算能力的渴求,其原理就是通过 IR 模拟一台 RISC 指令集的计算机嵌入到内核中,将内核内部的静态编译逻辑转变为更加灵活的动态编译逻辑,使内核获得近似于图灵机的动态逻辑定制能力。而从 classic BPF 到 extended BPF 的发展,是将这一计算方式进一步夯实和通用化。

BPF 的出现乃至到 eBPF 的进一步发展,为内核带来了巨大的改变,使内核具备了更加强大、可编程的动态变化的能力。这种能力在各种需要定制化的应用场景中,将发挥巨大的价值,既可以用于扩展功能,也可以用于优化性能。

在实现上,为适应不同业务场景的需求,使 eBPF 具备等价于一台 RISC 指令集计算机的计算能力,通过输入参数、Map 数据存储、Helper 帮助函数,构成了 eBPF 程序与内核交互的运行环境。eBPF 指令集的计算和控制能力、运行环境与内核的交互能力,两者叠加构成了 eBPF 程序强大的处理能力。

在安全方面,通过 Verifier 严格检查 eBPF 程序的可完成性、数据访问的合法性等,保证了 eBPF 程序与内核交互过程中内核不被挂起、核心数据不会被破坏。

在 BPF 发展过程中,由 cBPF 发展成为 eBPF 是一次大的技术升级。eBPF 在 cBPF 的基础上重新设计了指令集、引入了 JIT、增加了辅助函数,大大扩展了复杂逻辑的设计能力。虽然 eBPF 有巨大的进步,但是基本的底层设计还是一致的,因此两者统称为 BPF。

由于 eBPF 兼容 cBPF,在未指定时,BPF 更多指 eBPF 所定义的内涵。后文用 BPF 泛指整个 BPF 相关的基础机制,eBPF 特指最新的 BPF 标准。

2.2.技术特点

BPF 还在快速发展,它的计算能力和完备性也在迅速提高,前景无限。但就具体的版本而言,却又呈现具体技术特点,主要是其支持的能力和受到的约束两个方面。下面以最新的 BPF 技术标准(v6.1)为蓝本,介绍 BPF 的主要技术特点。

RISC 指令集

BPF 的核心是一个虚拟计算机,它采用类 RISC 指令集,支持跳转、算数运算、尾调用等基本操作。在运行 BPF 程序的计算机上,BPF 指令会被内核的 JIT 编译器动态编译为物理机原生指令,实现运行效率的“零”损耗。在支持 BPF 卸载的设备上,BPF 程序也可以卸载到设备上执行。在 BPF 的指令集中还支持伪调用指令,可以调用到内核帮助函数。

同时,BPF 的指令的编码空间中还有大量的储备,未来根据需要一定还会继续增加指令,提升 BPF 实现复杂逻辑的能力。

Map

基于键值对的数据存储机制,可用于实现内核、用户态的数据存储和交换。

Helper 函数

专用于 BPF 程序调用的函数接口,用于封装内核中的功能,使 BPF 程序可以和内核互操作,同时保持 BPF 程序和内核的安全隔离。

BPF 子程序

实现了 BPF 程序之间的调用。

上下文

BPF 程序的语境和运行上下文,是一种内部透明的数据结构。只有在明确 BPF 程序的类型时,上下文的定义和内部数据结构才是确定的。不同的 BPF 程序类型,上下文也各不相同。

CO-RE

通过运行时类型支持,实现一次编译、随处运行。

支持特权和非特权级两类运行模式

分为特权级(百万 ins)和非特权级(4096ins)两类运行方式。特权级模式下 BPF 程序可以获得更宽的权限,实现更复杂的逻辑功能。

保证向后兼容

这一原则对于 BPF 的推广应用非常重要,可以保证旧标准的 BPF 程序在新标准下也可以正确执行。但同时,也对未来 BPF 发展带来了约束,只有把握好 BPF 的发展方向,做好底层设计,才能两者得到兼顾。

比如,从老版本遗留下来的 cBPF 程序在 eBPF 中都会被 JIT 正确翻译和执行。

稳定的 ABI

BPF 稳定的 ABI 包括,BPF 程序类型对应的输入参数定义,可调用的内核帮助函数定义,返回值定义等。使用稳定的 ABI 的 BPF 程序,可保证与不同版本的内核都是兼容的。

另外,BPF 还在快速发展中,它的功能特性需要逐步释放,因此目前还有诸多限制,其中有些是基于安全、可靠性考虑,有些是没有超出范围的应用需求的保守设计等等。随着安全机制的完善、应用程序的扩展、生态体系的成熟,相应的限制也会逐步的改变。

目前的实现中,有如下限制:

  • 总运行时间有界:有界性这是基本原则,应该在比较长的时间内都不会改变。但是,在不改变有界性的前提下,根据具体需要适当调整更合理的上限,这是存在极大可能的。
  • 指令总数限制:非特权用户最大指令数 4096,特权用户最大指令数 1 百万。
  • 分支数限制。
  • BPF 调用嵌套层次限制。
  • Map 实例数限制。
  • 验证状态数限制。
  • 最大分支数限制。
  • 堆栈长度限制:目前支持的堆栈最大长度为 512 字节。
  • 上下文限制:每一种类型的 BPF 程序,都有其对应输入参数定义,彼此不同。也就是说,BPF 程序只能接受特定的输入并进行处理,不能访问内核的全部状态空间。
  • 辅助函数限制:每一个 BPF 程序类,都有其对应的辅助函数集合。这些辅助函数,由内核各子系统提供,是 BPF 程序类上下文的一部分。它们帮助 BPF 程序与内核各子系统交互,同时又保护内核不会被破坏。

上面赘述了很多特性,大家可能会有很多疑问,比如:为什么采用精简指令集呢?因为这是目前最主流的指令集类型,相对于复杂指令集,精简指令集更有利于实现更高密度、更高吞吐量、更高主频的处理器。因此 x86 之后出现的新型指令集系统,绝大多数都是精简指令集,包括现在的开源指令集 RISC-V。另外也有人会问,为什么不采用原生的指令集呢?为什么 5 个参数寄存器呢?本篇暂不深入讨论,后续主题涉及到的时候再详细讲解。

2.3.应用价值

BPF 的应用价值与其动态和可定制特性强相关。内核研发中一直坚守的原则是:“机制与策略分离”,即:内核负责提供机制,将策略开放给上层。在机制与策略之间需要一层界面来进行交互。系统调用是最初方案。它是单向发起的,缺少事件模型。虚拟文件系统,提供了双向的交互方式,但难以灵活定制复杂的逻辑。

由于软件功能越来越复杂,无法用简单规则来表达,软件的基础功能设施与业务逻辑,需要进行解偶。而业务逻辑部分,需要根据业务定制,因此很适合用 BPF 实现。比如:过滤器、权限检查、模糊测试等类型的功能,比较适合用 BPF 实现。另外,视具体问题,也可以应用于调度算法、用户态交互(替代系统调用,实现更加可变的服务逻辑)、加载器、模拟器、兼容层、轻量化内核、多态内核、启动方式。

每一种业务类型都有其独具特征的逻辑模型,通过更形式化地定义这些业务模型,可以更好地理解它们和 BPF 的结合性,找到更好的实现方案,充分发挥 BPF 带来的强大能力。后续篇章,我们会对典型的应用模型进行更深入的讨论,以及 BPF 在这些应用场景中,应该在哪些特性方面进行加强或改进。

3.eBPF 技术发展溯源

回顾技术的发展过程,就像观看非洲大草原日出日落一样,宏大的过程让人感动,细节部分引人深思。每天循环不辍,却又每天不同。

BPF 的应用早已超越了它最初的设计,但如果要追溯 BPF 最初的来源,则必须回归到它最初的应用领域,再进行理解分析。BPF 最初的用途在于观测,最初用于网络报文的抓取和分析。因此 BPF 的最初、最根本的来源,是作为一种观测手段出现的。而在这个领域中,技术的演进迭代,是一个很长的过程,体现了内核技术发展的艰辛、也同时充满了趣味。

如果把内核看作一个世界,在这个广袤的土地上,观测技术的发展,也同样经历了从蒙昧到现代的发展过程。每个时代都有其独具特色的观测技术,它决定了当时的开发人员需要具备什么样的功底,什么样的开发方式,这构成了一个时代特色,也谱写了时代的故事。

而每次时代的更迭,总是在某些方面颠覆了或者突破了传统的思维,从而引发了观测方式的巨大进步,促进了效率和可观测性的提升。对现有技术的深入研究与颠覆性的思想所构成的创新,是技术领域演进的基本形式。而其创新的动力又是什么呢?我们在后文逐步揭示。

3.1.石器时代

曾几何时,内核的开发还在初始阶段,由于内核的原理复杂、所处的位置特殊,开发方式和用户态有很大不同。内核开发难度远远大于用户态的应用开发,尤其调试比较困难。犹记得那时对于内核是否引入 GDB 调试机制,有过一些争论。其分歧点就在于,引入过于复杂的机制会改变内核的行为特性,影响问题的稳定性,反而不利于问题的分析定位。

那时最值得信赖的工具就是 Printk。这是一种低介入的观测工具,使用简单,几乎可以用于任何地方,帮助开发人员观测内核的运行状态。但显著的缺点是不够灵活,如果问题涉及的逻辑路径比较长、分支比较复杂的话,需要反复多次才能定位问题的根源。因此,那时候对内核开发人员的一个必不可少的要求,就是对所负责子系统的实现原理和代码逻辑的熟悉程度需要非常高,能够根据比较少的观测信息,准确定位问题的根源。

事物总是存在两面性,就像当初产生的那场争论一样,Printk 除了基本的信息输出机制外,几乎没有提供任何强有力的特性。这固然体现了当时的技术水平还在比较原始的阶段(没错,就像是石器时代),但同时也倒逼当时的内核开发人员超强的代码理解和分析能力,以便弥补简陋的工具对效率的掣肘,更快地解决程序中的 BUG。

另一方面,客观地讲,Printk 固然简单,卓尔无往不利。它可以使用在任何地方,具有完全的上下文访问能力,不受约束的表达能力。它的观测能力和程序本身完全相等,程序本身能看到什么,它就能看到什么,可以说是强大到巅峰。这种强大也是其无法被取代的根本原因,尽管内核的调测技术不断在发展,这一点始终未被超越。它可以用任何线性的文本形式,输出开发人员关注的上下文信息。在后来,这种表达能力得到了进一步发展,支持了部分正则文法。

它的缺点在于缺乏交互性,任何一点改变都需要修改程序。另一方面,不管上层流程是否被关注,它的信息都会被输出,大大影响了性能。

Printk 可以说是最强大的工具,至今我也是这样认为。但它同时也是最粗糙的工具。就像石头一样,prink 随处可见,随处可用,用了就一定有所得。简单、强大、直接。但是同样像石头一样,如果用得多了,就会成为垃圾。

Printk 相比于 BPF,拥有完全不受限制的上下文访问能力,使用的地方几乎没有限制,仅从观测的角度,强大之处有过之而无不及。但是使用方式过于原始,缺乏工业化的扩展能力,因此如果在更长的时间尺度、更广的应用领域来看的话,Printk 无法和 BPF 相提并论。

3.2.铁器时代

在石器时代,人们使用石头磨制的工具进行生产,这些工具粗糙、非标准化、材质原始容易损坏,笨重、使用寿命短。Printk 也是一样,每次执行时都会输出信息,但大多数时候是不需要的;寿命短,每次改变需要修改代码。

随着内核越来越成熟,架构设计、模块划分、内部功能等等都越来越规范合理。内核的特性,由各个子系统分别负责,内核的整体表现是各个子系统行为表现的综合。而子系统内部的关键路径,决定了子系统主要的行为表现,比如:调度系统中的 CPU 时间统计、上下文切换,迁移等等;内存管理系统中的内存分配、NUMA 平衡;虚拟内存中的页面错误、交换次数等等。

随着内核设计的规范化,其内部的关键节点和呈现在外部的语义都越来越清晰和标准化。要掌握内核的运行状态,其实并不需要随处观察,只需要掌握几个关键节点、关键信息就可以了。

以关键变量为基础,工具得以升级;以语义规范化为基础,为交互式的观测机制提供了基础。至此,观测手段不再是单纯的信息输出,它也可以反过来影响系统行为实现多维度的观测。

虚拟文件系统 Proc 首先打通了用户态和内核态的交互通道,从原来只能控制日志级别,到可以控制数据本身,可以控制的范围更广、更深了;从文本交互,转换为二进制交互,内核性能受到的影响进一步降低。提供了标准化的 API、类型的支持,降低了开发难度,便于推广使用。提炼出关键参数,通过虚拟文件系统进行交互式的系统观测,反过来有利于内核的规范化。

3.3.蒸汽时代

Proc 的定义很大一部分还是与具体的上下文相关,并不适合大批量的使用。而 Trace 定义了协议规范,抽象层次更高,可以批量使用。

Trace 是一个更加纯粹的观测机制,给用户提供了通用简单的接口,底层实现了很丰富的机制。可以支持大量使用,对于可观测性的提升起到了根本性的推动。可以批量重复使用,这是它和其他观测方式的区别。

如果说 Proc 采用了代码数据化的思想,那么 Trace 采用很多元编程的思想,极大简化了外部接口,减少了重复代码。

3.4.电气时代

Trace 机制固然好用,只要预先铺设了基础设施,运行时就可以随时开启观测。但缺点是,对于没有铺设铁轨的地方,火车的承载能力再强也是无法到达的。

Trace 的机制很通用,但另一方面,它无法深入业务层面进行更进一步的调测。要实现这一点,需要完整的上下文能力和可编程能力,因此 kprobe 出现了。只要由函数的地方,就像通了电一样,随时可以点亮,这是 Kprobe 强于 Trace 的覆盖能力。能够完整访问函数上下文,这是 Kprobe 强于 Trace 的业务理解能力。

3.5.智能时代

Kprobe 是动态性的萌芽,但是存在很多不足。它在内核态运行需要对内核编程有一定了解,编程门槛较高。此外,它还存在安全性问题、可扩展性问题,等等。

从计算能力来说,所有图灵机的计算能力是相等的,要解决能力问题,最终是要实现一个虚拟机的。而在内核态实现一个虚拟机,所涉及到的安全问题是必须考虑的,通过 Verifier 和运行时 Helper 函数,做到了逻辑约束和上下文隔离。虚拟机、Verifier 和 Helper 函数,是 BPF 和 Kprobe 的根本区别。

工具 观测能力 交互性 使用范围 修改内核 可编程 类型
Printk 可见上下文 单向 任意 需要 可以 日志
proc 预定义 双向 关键参数 不需要 不可以 固定观察窗
trace 预定义 单向 关键流程 不需要 不可以 动态观察窗
kprobe 函数边界 双向 任意函数 不需要 可以 动态逻辑
BPF trace+kprobe+预定义 双向 任意函数+预定义 不需要 可以 虚拟机

4.内在驱动

由以上简要的回顾和梳理可见,内核开发者们所不断寻找的是一种充分表达能力的动态机制,进而打破内核和用户态的壁垒(至少在逻辑层面),从而实现一种自由、直接的需求实现。技术成为内核开发者们锋利的工具,不断突破限制,揭示事物的本质。

BPF 技术的出现和发展,从时间尺度来说并不长,但是从其内在的驱动来说,有着复杂的动因,是很多因素就和在一起的必然结果。由于其复杂性,从任何一个孤立的角度进行分析都是不充分,只有从各个不同的角度分析,才可以体会出不同的趣味。

通过探寻其深层次的原因,可以梳理出更加清晰的发展脉络,从而可以更好地展望 BPF 及其相关技术领域的未来发展,为我们学习、研究和加入 BPF 的发展打下基础。

下面,我们试着从复杂性、微内核化两个方面,分析 BPF 发展的内在动力。

内核的发展历史就是一个复杂性不断递增的历史,内核的发展也是不断控制复杂性、维持内核代码的可理解性的过程。因此,内核的开发始终坚持一个原则,就是机制与策略的分离。

在不同时期,如何进行机制与策略的分离,有着不同的答案。随着技术和应用的不断发展,维持这一原则的的难度是不断增加的,需要更新的思想、更先进的技术才能支撑。或者也可以说,正因为内核的发展过程中,始终坚持了这个原则,所以才不断有影响深远的基础技术的出现。

我相信,要实现定制与动态,有很多不同的方案。但我认为 BPF 的出现是最佳的选择,使内核的发展有了应对未来变局的基础。

这是 BPF 出现的契机,也是其未来快速发展的动力。

4.1.代码规模问题

Linux 项目发展至今,其代码总量早已超过千万,是一个非常庞大的项目。

img

注:来源https://www.phoronix.com/misc/linux-eoy2019/lines.html

由统计数据可以看出,Linux 项目的复杂度(从代码量角度)一直在不断增长。

4.2.软件结构的复杂

整个软件系统,从应用程序到内核是一个繁杂的层次结构,又由于模块之间的交叉,实际的运行流程是一个复杂的有向图结构。

以完成一次简单的文件操作为例,首先应用程序需要 open 一个文件,这首先会运行到某种运行时库,完成资源分配、接口转换等等处理。然后,流程才会到系统调用这一层。系统调用中,由 VFS 解析文件路径信息,找到对应的文件系统信息。再由具体的文件系统完成文件打开的操作。

这其中至少涉及到了应用程序、运行时库、系统调用、VFS、文件系统等多个层次。如果再细分的话,还涉及到用户态内存管理,内核态内存管理、权限管理、命名空间管理、句柄管理、缓存管理、锁、钩子等次级模块。

目前 Linux 支持的文件系统至少已经达到七十多种,有基于本地存储设备的、基于网络的、分布式的、基于内存的、虚拟的等等。有的文件系统在内核态实现,有的在用户态实现。

另外,C 语言的条件编译,可以针对使用场景选择适合的代码编译。每一个条件编译选项就是对现实条件的一个考量。从 Linux 整个源码树中使用的条件编译选项的数量,也可以反映出 Linux 整个源码的复杂度。对 Linux 5.10 的源代码粗略统计,条件编译选项已经多达 18000 多个;而在 6.1 版本中,已经达到了 19000 多个。

4.3.业务系统的复杂

在虚拟化技术以前,不同应用场景的业务系统的结构差异,主要表现为平面性的拓扑结构的差异,比如:对等式的、分布式的、客户服务器模式的等等,由于网络拓扑结构的不同、节点承担的业务角色的不同,形成了各种各样的业务系统。

在虚拟化技术出现后,云计算迅猛发展,云成为了信息系统的基础设施。业务系统的差异不仅仅体现在横向拓扑结构上,其自身逻辑的深层组成也是非常复杂。它可以运行在真实计算机上也可能在虚拟机上,可能在一个独立的命名空间,也可能和别的业务共享。不同业务模块之间的联系有可能是直接的,也可能在无法感知的情况下被层层嵌套。

**业务系统的复杂性,体现在业务的复杂性与业务系统的复杂性两个方面。**业务的复杂,导致我们需要对业务系统进行分层设计,需要有定制化的能力,需要有运营与持续开发并行的能力。上线前的产品级的开发很重要,但是上线后的业务级的持续定制和开发同样重要。复杂的业务必然导致复杂业务系统的产生,如何以一个统一的、足够强大的方式来解决复杂性问题,使复杂业务系统的复杂性是可以拆解的、可管理的,就非常重要。

4.4.维护限制的要求

Linux 系统已经规模化运行在各种类型的设备上,每一个商业系统,在其运行期间都是需要进行维护的。

于大型的服务器系统,承载在成千上万的在线业务,是不能中断服务的,需要在线的定制能力。对于个人终端,每个人的使用习惯不同,如何使每个用户都能获得最佳的使用体验,需要数据分析和个性化的定制能力。对于散布在各处角落的边缘节点乃至物联网设备,需要内核提供更智能的介入方法,使维护人员能够远程完成对大量设备的维护工作。

5.eBPF 技术的意义

BPF 最初来源于解决网络报文过滤的问题,实现灵活的过滤规则。网络报文的过滤规则,最初只需要正则语言就能表达,但后来就不够了。而 BPF 提供了更强大的表达能力,BPF 具有近似图灵完备性,必将成为问题分解、解决复杂问题的神级工具。

5.1.图灵完备性

讨论 BPF 的计算能力,涉及到图灵完备。BPF 目前的基本设计中,有限性是基本设计原则,这是保证内核不被扩展逻辑挂死的基本要求。而有限性,是 BPF 和图灵机的根本差异,因此它不是图灵完备的。这个结论固然没错,但如果讨论仅止于此的话,那么这一论断过于粗糙,换个有趣一点的说法,这样的讨论不是图灵完备的,因此还需要具体分析。

完备性,不是评价工具优劣的完全准则。一般认为,C 语言是图灵完备的。但 C 语言的所有数据类型都是有界的,其实是弱于图灵机的。但不妨碍人们认为 C 是图灵完备的,因为它的能力边界距离实际应用的需求很远,我们感受不到。虽然 C 语言图灵不完备,但是不妨碍它的发展潜力,在它的成长过程中,也在不断的改版、丰富。这是因为它的完备性不足吗?显然不是。一种工具,在工程实践中,完备性是次要的,因为他被选择,就说明它是够用的。其他方面才是当下更应该关注的问题。

图灵机是一种无限的自动机,人们穷尽办法也只能逼近,即使全世界所有计算机加在一起的总和,也弱于图灵机。所以图灵完备现实中根本不存在,讨论逼近图灵机的能力可能更现实。在实际的语境中,人们实际上把无限接近图灵机的逼近能力,等同于图灵完备性。一个很好的例子就是 C 语言,它显然不是图灵完备的,但人们一般认为它是图灵完备。从这点说,BPF 语言同样是图灵完备的。

排除语言的问题,那么 BPF 是图灵完备的吗?仍然不是,**BPF 的图灵不完备,并不主要来源于 BPF 语言本身,而是来源于运行环境。从这点说,BPF 语言是图灵完备的,BPF 虚拟机不是。**从这点也可以说,只要有需要,通过改造运行环境,BPF 可以无限逼近图灵机的计算能力。

因此,从图灵完备这一点,我们既不能过度的否定 BPF,认为它的能力有限。但同时,也不能认为它的能力可以无限扩张,因为需要满足特定的条件。总之,BPF 还在快速发展过程中,一切可能性皆在其中,任何定论皆言之过早。

从另一个角度来看,就 BPF 目前的应用领域而言,输入和状态空间是有限的,因此在有限的输入下,图灵完备并不是必须。这是从现实的需求来说,BPF 足以完成指定语境下的任何计算。

但显然 BPF 的计算能力还有很大的提升空间。

语言方面,BPF 的指令集的提出,在计算能力上,它就是超配的。现在的问题是,如何安全地释放他的能力。运行时系统和工具链的设计,是目前的焦点问题。已经呈现出思想分歧,基于运行时环境的思路和直接开放的思路同时存在。未来这两个思路应该都会有一定程度的发展,形成面向不同领域的高低搭配的解决方案。

因此,我认为运行时的改进可能更加迫切。这需要我们及早确定问题边界,提供面向问题的运行环境,才能更有效的提出平衡安全和可计算性的问题的方案,即:运行环境+必要的计算能力,构成完备的面向问题域的解决方案。定义一个安全的虚拟机,保证操作不逃逸,一个安全的运行时库,导出或者链接内核对象(Helper),在这个集合上,定义安全的操作,这样语言本身就可以不再受具体逻辑和访问对象的限制,做到语言本身的图灵完备。

5.2.编程模型的发展

在 BPF 之前,Linux 开发的编程模型,可以分为内核编程和用户态编程两种。分别使用不同的编程接口和编程规范,是两者最大的区别。BPF 出现之后,出现了新的编程模型,既不能称之为内核编程,也不能称之为用户态编程。

这是一种全新的编程模型。它运行于内核态,但是不使用任何传统的内核接口(5.13 可以调用经过筛选和处理的内核函数。至今,它仍然受限于特定函数和指定的上下文,还不是一种通用的机制。且这种机制进一步通用化之前,它的安全性仍然值得先进一步的讨论),不通过符号与内核进行链接。它使用应用编程逻辑和范式,但是不使用应用编程传统的接口,而是使用 BPF 提供的帮助函数。它所能访问的数据对象还在不断发展过程中,远未定型。

因此,笔者称这种编程模型为:临界编程。也许它未来会有更好的名字,但这个名字一方面,表明它的跨界特性,一方面表面它日新月异的发展。也表明对它未来的期待。

5.3.用户态比重的加大

由于虚拟化和软件工程的原因,网络报文处理和文件系统,呈现出往用户态迁移的趋势。BPF 和用户态化的共通点和差异点在于,都将更多的内核扩展性放在了用户态,但 BPF 的逻辑仍然从属于内核。

他们都和传统内核通过一层良好定义的接口进行了隔离。用户态驱动和文件系统,使内核的功能更容易扩展。而 BPF 则是对内核本身的扩展。两者存在根本差异,因此也存在相互结合的可能,从而形成更加强大的软件架构。

而这种架构会用于什么地方呢?我们已经做了初步尝试,FUSE 和 BPF 进行结合。可以实现用户态文件系统和内核更加高效的交互(这一话题,我们在后续的篇章中再详细讨论)。推而广之,内核的网络、安全、文件系统、驱动,都可以放在用户态来实现,通过 BPF 来优化交互。

5.4.微内核

BPF 的运行基础是运行时环境,随着 BPF 应用的增加,一定会促使内核子系统的更进一步的抽象和解偶,这在逻辑上为微内核化准备了条件。

BPF 真正避免了纯粹用户态编程的性能问题,为应用开发人员开发特色功能提供了一种临界编程工具。这或许是微内核的另一种实现路径。

5.5.观测代码与业务代码合一

BPF 出现的时候,最初是观测工具,但后来它也能用于实现更复杂的功能,影响网络子系统的报文转发逻辑。BPF 计算能力的强大、性能的优势,使它不仅能用于观测还可以做更多复杂的事情。

通过高度抽象化的设计,我们可以设计出复杂、通用的业务系统,但是我们设计不出“最佳”的业务系统。最佳的业务系统一定是在真实的应用场景中,通过不断的观测、分析、优化,才能达成的。

将一个复杂系统优化到“最佳”同样是一个复杂问题,多目标的一致性、动态系统的不稳定性、巨大的状态空间等等,都可能导致这个问题没有最终答案,只有采用动态反馈机制。因此,将观测代码和优化代码(业务代码的策略优化部分)合一,是使这一优化模式能够更加准确、高效、稳定的必然选择。

5.6.编译器和内核合一

从本质上讲,计算问题、语言问题其实是一个问题。最初我们解决计算问题,是在纸带上打孔,后来有了编译器。解决计算问题的效率大大提升,但是解决计算问题的能力其实没有变化。

后来有了操作系统,软件的分层模型逐渐成型,开发应用程序的效率大大提升,但其实通过编程解决计算问题的能力并没有提升,反而是在下降。因为软件的每一个分层,在带来工程化效率的同时,也导致了能力的损耗。API 的设计是一个大命题,但是没有完美的 API 设计。

开发效率的提升,带来了应用的高度发展,现在计算能力的问题出现了。回归本原,将编译器和内核合一,构建更加强大的计算能力,是未来发展的基础。

6.走向未来

未来 BPF 将如何发展呢?

它已经具备图灵机的雏形,拥有巨大的计算能力潜能。它目前的计算能力仍然受到约束,但是已经足够改变现有应用开发的基础,必将引发应用的蓬勃发展,会衍生出开发工具、测试方法等等的发展,使业务逻辑的开发与 BPF 的开发统一在一个开发模型当中,甚至引发新的开发语言出现。当在应用领域中生根后,就会继续发芽壮大,需要吸收计算能力作为养料才能抽枝散叶。BPF 应用与 BPF 技术内涵的发展就像两面相对的镜子,相互映照,形成斑斓的德罗斯特效应图景。

随着近几年云计算、人工智能、智能设备的蓬勃发展,信息系统基础设施结构、设备类型、业务复杂度都迎来再一次的变革。

Linux 系统作为现今最为广泛使用的操作系统,其自身也在发展变化。初期,沿着原有的技术路线,通过量的积累,足以应对时代的演进,这一点从代码增长就可以看出来,其背后是 Linux 支持的设备、驱动、特性、机制也来越多。产品构型也越来也复杂,Web 服务器、并行计算异构计算、桌面、智能终端、嵌入式系统。Linux 的技术设施,需要面对不同的应用场景和问题。量的积累,可以解决一段时间的问题。但是,当这种变化积累到一定程度时,需要新的手段,才能支持上层结构的灵活度。

需要指出的是,现有的文档中,大多将 BPF 定位为网络和安全工具的利器。但是 BPF 作为一种通用的动态逻辑机制,绝不仅仅可以应用这两个地方。

6.1.通用性

BPF 已经从最初网络报文分析技术,扩展到了很多应用领域,以后必然成为一种通用的内核开发技术,在定制化和功能扩展两方面推动内核发展。目前 BPF 的核心组件基本轮廓已经确定,由运行上下文、帮助函数、Map、指令集、Verifier、JIT、系统调用等关键模块构成 BPF 的核心运行机制。

运行上下文是 BPF 程序运行的语境,目前除了网络语境发展比较快速之外,其他程序类型的运行上下文发展相对落后,文件系统目前甚至还没有。对于运行上下文应该设计成什么样子,达到什么要求,有怎样的约束,还没有统一的范式,主要由各程序类型根据实际应用需要进行定义。彼此间缺乏共通性,发展比较随意,还处于比较原始的阶段。

帮助函数还不完备,各个程序类型存在差异。程序类型的定义,缺乏逻辑基础,其设计元语还需澄清。语境相关部分和通用部分划分不清楚,影响到安全机制也无法针对性设计,安全性无法验证。

Map 担负的角色过于宽泛,既是通讯机制,也是存储机制,既是 Local 的也是 Global 的。是对 BPF 核心机制补全的过渡手段。随着,远程调用、间接调用、跳转表、全局变量等的实现,Map 的作用和使用方式也将改变。

6.2.表达能力

内核已经在扩展性方面在不断改进,但是这些始终还是不能根本解决问题,引入更多编译器技术特别是动态编译技术、可信编译技术才是解决问题的根本。

目前 BPF 的程序的表达能力相当于弱化的 C 语言,这显然是不够的。实现一种和传统应用开发相同的开发体验,让程序员专注于理解业务逻辑,自由地表达,需要编译器填补通用语言与 BPF 自身限制之间的沟壑,需要语言层面的扩展,也需要运行时和工具链的支持。

6.3.开发工具

目前还没有在前端支持 BPF 的开发工具,只是实现了后端的支持,这显然还远远不够。这种情况,正说明了 BPF 的发展急需编译器的支持,在前端支持 BPF,通过语言特性的扩展和新的开发支持库,实现 BPF 与通用编程语言的融合,将大大缩减包含 BPF 特性的应用程序的开发、测试和维护难度。对于 BPF 作为一项应用开发技术大力推广至关重要。

6.4.开发流程

目前,在设计阶段,需要将 BPF 的逻辑部分和一般编程逻辑部分分离出来,这增加了设计的开销,同时对于设计人员的要求加大。原本的应用设计人员,只了解业务逻辑,这显然不够,还需要了解内核的基本原理,才能够做好逻辑划分工作。既了解内核又懂应用开发和业务逻辑的人员,是交叉性人才,这样的人员往往少且难以培养。如果让原本的应用开发人员,学习掌握内核相关的知识,以便可以满足 BPF 应用开发的需要,显然费时费力不说,费效比更是难以达到商业决策的最低门槛。

而在开发阶段,BPF 和应用需要分开编码,这无疑增加了联调联试的开销。特别是,出现问题的时候需要频繁的跨组跨部门沟通,效率实在太低。如果能把 BPF 的开发完全应用化,让一个程序员承担所有工作,成本、效率都可以得到优化。在测试阶段,还缺少专用的高效率的工具。

因此,以开发工具的进步为基础,目前采用的开发流程也一定会同步地被改进。可以预想,未来的开发流程一定是融合和简化的。

7.结束语

Linux 内核的发展,将技术发展与创新演绎得淋漓尽致。源自于用户和开发者的需求,始终是推动技术不断进步的根本动力。在需求的推动下,Linux 内核始终在快速的发展,保持着强劲的动力。同时,热爱与坚持,还有最重要的开发原则的坚守,是 Linux 能够将源源不断的需求转化为创新动力的基础,而不至于被爆炸的需求摧毁。基本原则体系的维护,使 Linux 内核始终保持如一的设计框架。

在 Linux 的发展过程中,一些很小的需求,最终也可以发展成为复杂的架构。坚持与打破壁垒,是创新的范式。

在不断寻求问题的最终答案的过程中,有很多优秀的思想启发我们的认知,但限于技术发展阶段、条件是否成熟,这些优秀的思想有的潜入水底,有的浮现水面独领一段风骚。历史会有所偏好,作出它的选择,但不可否认的是这些优秀的思想,都一直在发挥着它们的作用。当历史的拐点到来的时候,它们又会重新融合,以一种全新的方式继续推动技术的进步。

BPF 是内核交互问题不断挖掘、迭代后的最新答案。内核的交互问题,本质上是内核结构问题。BPF 的强大计算能力,将推动更好地实现内核与用户态的动态交互,使内核能够更加灵活满足各种应用场景的需要,使整个系统的性能不因为这种能力而遭受损失。保持软件良好分层的基础上,减小分层对信息交互、资源共享的阻碍。而围绕 BPF 的基础设施的发展,也必定会为内核结构带来巨大改变,将安全性、规范性更加深入地融入到内核的细微层次。

安全可靠是 BPF 持续发展的原则的,在 BPF 的功能性不断扩展、计算能力不断释放的过程中,安全检查、可信编译的加持是可持续性的基础。而作为一种全新的编程方式,BPF 的开发和传统编程范式具有同样的地位和发展前景。从语言的支持到代码的生成乃至 JIT 的优化等等,是必不可少的一环。

BPF 来源于 Linux 内核发展过程中,众多优秀的开发者在效率、能力方面的不断改进,以及对技术本源的孜孜以求。它是内核发展中,众多优秀思想的集大成者,但同时,它也仅仅是新时代的开始。新的方法、新的语言、新的架构都在不断出现,催生着巨大的变革,如汹涌的波涛。而 BPF 将成为乘波之舟,它存在很多可能性,相信以此为起点,开发者们将会谱写更华丽的篇章。这是包括作者在内的众多开发者,所期待的广阔未来。

下一页