eBPF 的发展演进 --- 从石器时代到成为神(一)
1. 前言
技术的发展往往是积跬步而至千里的。Linux 从 92 年诞生,发展至今已经覆盖大小各类的信息基础设施。是什么样的力量,让 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 在这些应用场景中,应该在哪些特性方面进行加强或改进。
评论