发展历程

  1. 1992 年,在 BSD 操作系统中引入了革命性的包过滤机制 BPF,性能非常好
  2. BPF 的两大设计
    • 内核态引入一个新的虚拟机,所有指令都在内核虚拟机中运行
    • 用户态使用 BPF 字节码来定义过滤表达式,然后传递给内核,由内核虚拟机解释执行
  3. BPF 使得包过滤可以直接在内核中执行,避免向用户态复制每个数据包,从而极大提升了包过滤的性能,被各大操作系统广泛接受
  4. BPF 诞生 5 年后,在 Linux 2.1.75 首次引入了 BPF 技术,在 Linux 3.0 中增加的 BPF JIT,替换掉性能更差的解释器,进一步优化了 BPF 指令运行的效率
  5. 直到此时,BPF 的应用领域还仅限于网络包过滤
  6. 2014 年,将 BPF 扩展为一个通用的虚拟机,即 eBPF
    • eBPF 不仅扩展了寄存器的数量,引入了全新的 BPF 映射存储
    • 还在 4.x 内核中将原本单一的数据包过滤事件逐步扩展到了内核态函数用户态函数跟踪点性能事件以及安全控制
  7. eBPF 的诞生了 BPF 技术的转折点,使得 BPF 不再仅限于网络栈,而是成为内核的一个顶级子系统 - 最活跃
  8. eBPF 无需修改内核源码重新编译就可以扩展内核的功能

b44562381748de369b50403219c0d1ff

工作原理

  1. eBPF 程序并不像常规的线程那样,启动后就一直运行在那里,它需要内核事件触发后才会执行
    • 事件 - 系统调用内核跟踪点内核函数用户态函数调用退出网络事件
  2. 借助于强大的内核态插桩(kprobe)和用户态插桩(uprobe),eBPF 程序几乎可以在内核和应用的任意位置进行插桩
  3. 确保安全稳定是 eBPF 的首要任务,不安全的 eBPF 程序不会提交内核虚拟机中执行

执行过程

linux_ebpf_internals

  1. 借助 LLVM 把编写的 eBPF 程序转换为 BPF 字节码,然后再通过 bpf 系统调用提交给内核执行
  2. 内核在接受 BPF 字节码之前,会首先通过验证器字节码进行校验,只有校验通过BPF 字节码才会提交JIT 执行
  3. 如果 BPF 字节码中包含了不安全的操作,验证器会直接拒绝 BPF 程序的执行,典型的验证过程
    • 只有特权进程才可以执行 bpf 系统调用
    • BPF 程序不能包含无限循环
    • BPF 程序不能导致内核崩溃
    • BPF 程序必须在有限时间内完成
  4. BPF 程序可以利用 BPF 映射进行存储,而用户程序也需要通过 BPF 映射运行在内核中的 BPF 程序进行交互
  5. eBPF 程序的运行需要经历编译加载验证内核态执行等过程,而用户态程序则需要借助 BPF 映射来获取内核态 eBPF 程序的运行状态

性能观测中,BPF 程序收集内核运行状态存储在 BPF 映射中,用户程序再从 BPF 映射中读取这些状态

ebpf-maps

局限性

  1. eBPF 程序必须被验证器校验通过才能执行,且不能包含无法到达的指令
  2. eBPF 程序不能随意调用内核函数,只能调用在 API定义辅助函数
  3. eBPF 程序空间最多只有 512 字节,要更大的存储,只能借助于 BPF 映射
  4. 在内核 5.2 之前,eBPF 字节码最多只支持 4096 条指令,在 5.2 内核版本将该限制提高到了 100 万条
  5. 内核快速发展,在不同版本内核中运行时,需要访问内核数据结构的 eBPF 程序很可能需要调整源码,并重新编译
  6. eBPF 很多新特性都是在 4.x 版本中才逐步增加的,要稳定运行 eBPF 程序,内核版本至少要 **4.9+**,推荐 5.x