《架构师》2023年6月
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

特别专题|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的根本区别。

4.内在驱动

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

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

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

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

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

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

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

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

4.1.代码规模问题

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

由统计数据可以看出,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将成为乘波之舟,它存在很多可能性,相信以此为起点,开发者们将会谱写更华丽的篇章。这是包括作者在内的众多开发者,所期待的广阔未来。

作者介绍

钟俊,统信软件研发技术专家,专注于内核与编译器技术,长期在通信、云计算、安全、信创等多个行业从事底层软件技术研发及相关工作。