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在这些应用场景中,应该在哪些特性方面进行加强或改进。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net
机房租用,北京机房租用,IDC机房托管, http://www.e1idc.net