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

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

1.前言

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

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

2.eBPF技术概览

2.1.实现原理

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

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

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

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

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

BPF发展过程中,由cBPF发展成为eBPF是一次大的技术升级。eBPFcBPF的基础上重新设计了指令集、引入了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程序类型对应的输入参数定义,可调用的内核帮助函数定义,返回值定义等。使用稳定的ABIBPF程序,可保证与不同版本的内核都是兼容的。

另外,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函数,做到了逻辑约束和上下文隔离。虚拟机、VerifierHelper函数,是BPFKprobe的根本区别。

工具 观测能力 交互性 使用范围 修改内核 可编程 类型
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则是对内核本身的扩展。两者存在根本差异,因此也存在相互结合的可能,从而形成更加强大的软件架构。

而这种架构会用于什么地方呢?我们已经做了初步尝试,FUSEBPF进行结合。可以实现用户态文件系统和内核更加高效的交互(这一话题,我们在后续的篇章中再详细讨论。推而广之,内核的网络、安全、文件系统、驱动,都可以放在用户态来实现,通过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将成为乘波之舟,它存在很多可能性,相信以此为起点,开发者们将会谱写更华丽的篇章。这是包括作者在内的众多开发者,所期待的广阔未来。

下一页