libbpf 开发指南:概述

目录

什么是libbpf

BPF生命周期

骨架文件

BPF CO-RE(编译一次 - 到处运行)


什么是libbpf

libbpf 是一个基于 C 的库,包含一个 BPF 加载程序,该加载程序获取已编译的 BPF 目标文件并准备它们并将其加载到 Linux 内核中。libbpf 承担了加载、验证 BPF 程序并将其附加到各种内核挂钩的繁重工作,使 BPF 应用程序开发人员能够只关注 BPF 程序的正确性和性能。

以下是 libbpf 支持的高级功能:

  • 为用户空间程序与BPF程序交互提供高层和低层API。低级 API 封装了所有 bpf 系统调用功能,当用户需要对用户空间和 BPF 程序之间的交互进行更细粒度的控制时,这非常有用。
  • 为bpftool生成的BPF对象骨架提供全面支持。框架文件简化了用户空间程序访问全局变量和使用 BPF 程序的过程。
  • 提供BPF侧API,包括BPF辅助定义、BPF映射支持、跟踪辅助等,方便开发者简化BPF代码编写。
  • 支持BPF CO-RE机制,使BPF开发人员能够编写可移植的BPF程序,这些程序可以一次编译并跨不同内核版本运行。

libbpf 提供了一组 API,使得开发者能在用户空间加载、附加和管理 eBPF 程序。以下是 libbpf 的一些重要特性:

  1. 加载 eBPF 程序:libbpf 提供了用于加载 eBPF 程序的功能。开发者可以使用 libbpf 将 eBPF 程序加载到内核,并且可以查询和验证这些程序。
  2. 附加 eBPF 程序到内核钩子:libbpf 支持将 eBPF 程序附加到内核的各种钩子上,例如网络接口、跟踪点(tracepoints)和 kprobes(内核探测点)。这使得 eBPF 程序可以在特定事件发生时被触发,从而实现对内核行为的监控和控制。
  3. 操作 eBPF maps:eBPF maps 是一种用于在 eBPF 程序和用户空间应用程序之间共享数据的内核对象。libbpf 提供了一组 API,用于创建、更新和访问 eBPF maps。这使得开发者可以方便地在 eBPF 程序和用户空间应用程序之间传递数据。
  4. 兼容性和可移植性:libbpf 尽力确保与不同版本的 Linux 内核和 eBPF 子系统保持兼容。这使得使用 libbpf 编写的应用程序在各种环境中具有较好的可移植性。
  5. 与 BPF 编译器集成:libbpf 可以与 BPF 编译器(例如 clang 和 LLVM)集成,使得开发者可以更方便地编译和加载 eBPF 程序。

BPF生命周期

一个 BPF 应用程序由一个或多个 BPF 程序(协作或完全独立)、BPF 映射和全局变量组成。全局变量在所有 BPF 程序之间共享,这使得它们能够在一组公共数据上进行合作。libbpf 提供了用户空间程序可用于通过触发 BPF 应用程序生命周期的不同阶段来操作 BPF 程序的 API。

以下部分简要概述了 BPF 生命周期中的每个阶段:

  • 开放阶段:在此阶段,libbpf 解析 BPF 目标文件并发现 BPF 映射、BPF 程序和全局变量。打开 BPF 应用程序后,用户空间应用程序可以在创建和加载所有实体之前进行其他调整(如有必要,设置 BPF 程序类型;为全局变量预先设置初始值等)。
  • 加载阶段:在加载阶段,libbpf 创建 BPF 映射、解析各种重定位、验证 BPF 程序并将其加载到内核中。此时,libbpf 验证了 BPF 应用程序的所有部分,并将 BPF 程序加载到内核中,但尚未执行任何 BPF 程序。加载阶段之后,可以设置初始 BPF 映射状态,而无需与 BPF 程序代码执行竞争。
  • Attachment阶段:在这个阶段,libbpf将BPF程序附加到各种BPF钩子点(例如,tracepoints、kprobes、cgroup钩子、网络数据包处理管道等)。在此阶段,BPF 程序执行有用的工作,例如处理数据包或更新可从用户空间读取的 BPF 映射和全局变量。
  • 拆卸阶段:在拆卸阶段,libbpf 分离 BPF 程序并将其从内核中卸载。BPF 映射被销毁,并且 BPF 应用程序使用的所有资源都被释放。

骨架文件

BPF对象骨架文件是一种用于简化BPF程序加载和使用的机制。它是一个由BPF编译器(例如:clang)生成的C源代码文件,包含了BPF程序的元数据(例如:程序类型、附件点)和二进制代码。

BPF对象骨架文件的主要优势是将BPF程序和用户空间应用程序的编译过程关联起来。这样,当BPF程序发生更改时,用户空间应用程序会自动重新编译并使用新的BPF程序。这避免了在用户空间应用程序中硬编码BPF程序的二进制代码,从而简化了BPF程序的开发和维护工作。

要生成BPF对象骨架文件,你需要遵循以下步骤:

  1. 编写一个BPF程序,通常使用C语言并包含特定于BPF的内核头文件。
  2. 使用BPF编译器(例如:clang)将BPF程序编译为ELF(可执行和链接格式)文件。在这个过程中,编译器会将BPF程序的元数据和二进制代码嵌入到ELF文件中。
  3. 使用BPF编译器或相关工具(例如:bpftool)从ELF文件生成BPF对象骨架文件。这个骨架文件是一个C源代码文件,包含了BPF程序的元数据和二进制代码。
  4. 在用户空间应用程序中包含和使用这个BPF对象骨架文件。这样,用户空间应用程序就可以直接加载和使用BPF程序,而无需手动处理BPF程序的二进制代码和元数据。

作为libbpf API 的替代接口,用于处理 BPF 对象。骨架代码抽象出通用的 libbpf API,以显着简化从用户空间操作 BPF 程序的代码。骨架代码包含 BPF 目标文件的字节码表示形式,简化了分发 BPF 代码的过程。嵌入 BPF 字节码后,无需与应用程序二进制文件一起部署额外的文件。

(.skel.h)您可以通过将 BPF 对象传递给 bpftool 来生成特定对象文件的骨架头文件。生成的 BPF 骨架提供了以下与 BPF 生命周期相对应的自定义函数,每个函数都以特定的对象名称为前缀:

  • <name>__open()– 创建并打开BPF应用程序(<name>代表特定的bpf对象名称)
  • <name>__load()– 实例化、加载和验证 BPF 应用程序部分
  • <name>__attach()– 附加所有可自动附加的 BPF 程序(这是可选的,您可以直接使用 libbpf API 进行更多控制)
  • <name>__destroy()– 分离所有BPF程序并释放所有使用的资源

使用骨架代码是使用 bpf 程序的推荐方法。请记住,BPF 框架提供了对底层 BPF 对象的访问,因此即使使用 BPF 框架,使用通用 libbpf API 可以做的任何事情仍然是可能的。这是一个附加的便利功能,没有系统调用,也没有繁琐的代码。

BPF CO-RE(编译一次 - 到处运行)

BPF CO-RE(BPF Compile-Once, Run-Everywhere)是一种使eBPF(扩展Berkeley Packet Filter)程序在不同内核版本和配置下更具可移植性的技术。BPF CO-RE的目标是允许在一台机器上编译BPF程序,然后在其他具有不同内核版本或配置的机器上运行,而无需重新编译。

传统上,eBPF程序通常需要访问内核数据结构,如网络套接字、任务结构等。这些内核数据结构会随着内核版本的变化而发生变化,导致eBPF程序需要针对每个内核版本单独编译。这种情况下,如果内核数据结构发生变化,你需要重新编译和部署eBPF程序,以使其与新内核兼容。这不仅增加了维护成本,还限制了eBPF程序在不同内核版本之间的可移植性。

BPF CO-RE通过以下两个主要特性来解决这个问题:

  1. 基于偏移量的重定位:BPF CO-RE使用基于偏移量的重定位技术,而不是在编译时直接引用内核数据结构。这意味着,eBPF程序不再依赖于特定内核版本的数据结构,而是使用相对偏移量来访问内核数据结构中的成员。这样可以在不重新编译eBPF程序的情况下,适应不同内核版本中的数据结构变化。
  2. 类型检查和自动降级:BPF CO-RE还在加载eBPF程序时执行类型检查。这可以确保eBPF程序仅在与目标内核数据结构兼容时才能运行。如果类型检查失败,eBPF程序可以选择自动降级,即使用较低级别的功能或完全禁用某些功能,以适应目标内核的数据结构。

BPF CO-RE技术简化了eBPF程序在不同内核版本和配置下的部署和维护工作,提高了eBPF程序的可移植性。要使用BPF CO-RE,你需要使用支持CO-RE的编译器(如LLVM/Clang)和BPF加载器(如libbpf)。

BCC是BPF可移植性的解决方案之一。然而,由于将编译器嵌入到应用程序中,它会带来运行时开销和较大的二进制大小。

libbpf 通过支持 BPF CO-RE 概念来增强 BPF 程序的可移植性。BPF CO-RE 将 BTF 类型信息、libbpf 和编译器结合在一起,生成可以在多个内核版本和配置上运行的单个可执行二进制文件。

为了使 BPF 程序可移植,libbpf 依赖于正在运行的内核的 BTF 类型信息。sysfs内核还通过at公开了这个自描述的权威 BTF 信息/sys/kernel/btf/vmlinux。

可以使用以下命令生成正在运行的内核的 BTF 信息:

bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h

该命令生成一个vmlinux.h头文件,其中包含正在运行的内核使用的所有内核类型( BTF 类型)。包含 vmlinux.h在 BPF 程序中可以消除对系统范围内核标头的依赖。

libbpf 通过查看 BPF 程序记录的 BTF 类型和重定位信息并将它们与运行内核提供的 BTF 信息 (vmlinux) 进行匹配来实现 BPF 程序的可移植性。然后 libbpf 解析并匹配所有类型和字段,并更新必要的偏移量和其他可重定位数据,以确保 BPF 程序的逻辑对于主机上的特定内核正确运行。因此,BPF CO-RE 概念消除了与 BPF 开发相关的开销,并允许开发人员编写可移植的 BPF 应用程序,而无需在目标机器上进行修改和运行时源代码编译。


以下代码片段展示了如何使用 BPF CO-RE 和 libbf 读取内核的父字段 task_struct。以 CO-RE 可重定位方式读取字段的基本帮助程序是,它将 从 引用的字段中读取字节到 指向的内存中 。

bpf_core_read(dst, sz, src)

 //...
 struct task_struct *task = (void *)bpf_get_current_task();
 struct task_struct *parent_task;
 int err;

 err = bpf_core_read(&parent_task, sizeof(void *), &task->parent);
 if (err) {
   /* handle error */
 }

 /* parent_task contains the value of task->parent pointer */

在代码片段中,我们首先获得一个指向当前task_structusing 的指针bpf_get_current_task()。然后我们使用bpf_core_read()将任务结构的父字段读入parent_task变量中。bpf_core_read()就像bpf_probe_read_kernel()BPF helper 一样,只不过它记录了应该在目标内核上重定位的字段的信息。即,如果 由于在其前面添加了一些新字段而导致该parent字段移动到不同的偏移量 ,libbpf 将自动将实际偏移量调整为正确的值。struct task_struct

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ym影子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值