eBPF - Principle
发展历程
- 1992 年,在 BSD 操作系统中引入了革命性的包过滤机制 BPF,性能非常好
- BPF 的两大设计
- 内核态引入一个新的虚拟机,所有指令都在内核虚拟机中运行
- 用户态使用 BPF 字节码来定义过滤表达式,然后传递给内核,由内核虚拟机解释执行
- BPF 使得包过滤可以直接在内核中执行,避免向用户态复制每个数据包,从而极大提升了包过滤的性能,被各大操作系统广泛接受
- BPF 诞生 5 年后,在 Linux 2.1.75 首次引入了 BPF 技术,在 Linux 3.0 中增加的 BPF JIT,替换掉性能更差的解释器,进一步优化了 BPF 指令运行的效率
- 直到此时,BPF 的应用领域还仅限于网络包过滤
- 2014 年,将 BPF 扩展为一个通用的虚拟机,即 eBPF
- eBPF 不仅扩展了寄存器的数量,引入了全新的 BPF 映射存储
- 还在 4.x 内核中将原本单一的数据包过滤事件逐步扩展到了内核态函数、用户态函数、跟踪点、性能事件以及安全控制等
- eBPF 的诞生了 BPF 技术的转折点,使得 BPF 不再仅限于网络栈,而是成为内核的一个顶级子系统 - 最活跃
- eBPF 无需修改内核源码和重新编译就可以扩展内核的功能
工作原理
- eBPF 程序并不像常规的线程那样,启动后就一直运行在那里,它需要内核事件触发后才会执行
- 事件 - 系统调用、内核跟踪点、内核函数和用户态函数的调用退出、网络事件等
- 借助于强大的内核态插桩(kprobe)和用户态插桩(uprobe),eBPF 程序几乎可以在内核和应用的任意位置进行插桩
- 确保安全和稳定是 eBPF 的首要任务,不安全的 eBPF 程序不会提交到内核虚拟机中执行
执行过程
- 借助 LLVM 把编写的 eBPF 程序转换为 BPF 字节码,然后再通过 bpf 系统调用提交给内核执行
- 内核在接受 BPF 字节码之前,会首先通过验证器对字节码进行校验,只有校验通过的 BPF 字节码才会提交到 JIT 执行
- 如果 BPF 字节码中包含了不安全的操作,验证器会直接拒绝 BPF 程序的执行,典型的验证过程
- 只有特权进程才可以执行 bpf 系统调用
- BPF 程序不能包含无限循环
- BPF 程序不能导致内核崩溃
- BPF 程序必须在有限时间内完成
- BPF 程序可以利用 BPF 映射进行存储,而用户程序也需要通过 BPF 映射与运行在内核中的 BPF 程序进行交互
- eBPF 程序的运行需要经历编译、加载、验证和内核态执行等过程,而用户态程序则需要借助 BPF 映射来获取内核态 eBPF 程序的运行状态
在性能观测中,BPF 程序收集内核运行状态存储在 BPF 映射中,用户程序再从 BPF 映射中读取这些状态
局限性
- eBPF 程序必须被验证器校验通过才能执行,且不能包含无法到达的指令
- eBPF 程序不能随意调用内核函数,只能调用在 API 中定义的辅助函数
- eBPF 程序栈空间最多只有 512 字节,要更大的存储,只能借助于 BPF 映射
- 在内核 5.2 之前,eBPF 字节码最多只支持 4096 条指令,在 5.2 内核版本将该限制提高到了 100 万条
- 内核快速发展,在不同版本内核中运行时,需要访问内核数据结构的 eBPF 程序很可能需要调整源码,并重新编译
- eBPF 很多新特性都是在 4.x 版本中才逐步增加的,要稳定运行 eBPF 程序,内核版本至少要 **4.9+**,推荐 5.x
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.