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 在这些应用场景中,应该在哪些特性方面进行加强或改进。

 

 

内容来源:deepin社区

转载请注明出处

发表评论