编程接口:eBPF 程序是怎么跟内核进行交互的?

news2024/12/27 12:42:10

目录

背景

BPF 系统调用

BPF 辅助函数

BPF 映射

BPF 类型格式 (BTF)

小结


背景

用高级语言开发的 eBPF 程序,需要首先编译为 BPF 字节码,然后借助 bpf 系统调用加载到内核中,最后再通过性能监控等接口与具体的内核事件进行绑定。这样,内核的性能监控模块才会在内核事件发生时,自动执行我们开发的 eBPF 程序。

那么,eBPF 程序到底是如何跟内核事件进行绑定的?又该如何跟内核中的其他模块进行交互呢?接下来,我就带你一起看看 eBPF 程序的编程接口。

BPF 系统调用

如下图(图片来自brendangregg.com)所示,一个完整的 eBPF 程序通常包含用户态和内核态两部分。其中,用户态负责 eBPF 程序的加载、事件绑定以及 eBPF 程序运行结果的汇总输出;内核态运行在 eBPF 虚拟机中,负责定制和控制系统的运行状态。

对于用户态程序来说,我想你已经了解,它们与内核进行交互时必须要通过系统调用来完成。而对应到 eBPF 程序中,我们最常用到的就是 bpf 系统调用:bpf(2) - Linux manual page

在命令行中输入 man bpf ,就可以查询到 BPF 系统调用的调用格式:

#include <linux/bpf.h>

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

BPF 系统调用接受三个参数:

第一个,cmd ,代表操作命令,比如上一讲中我们看到的 BPF_PROG_LOAD 就是加载 eBPF 程序;

第二个,attr,代表 bpf_attr 类型的 eBPF 属性指针,不同类型的操作命令需要传入不同的属性参数;

第三个,size ,代表属性的大小。

注意,不同版本的内核所支持的 BPF 命令是不同的,具体支持的命令列表可以参考内核头文件 include/uapi/linux/bpf.h 中  bpf_cmd 的定义。比如,v5.13 内核已经支持 36 个 BPF 命令:bpf.h - include/uapi/linux/bpf.h - Linux source code (v5.13) - Bootlin

enum bpf_cmd {
  BPF_MAP_CREATE,
  BPF_MAP_LOOKUP_ELEM,
  BPF_MAP_UPDATE_ELEM,
  BPF_MAP_DELETE_ELEM,
  BPF_MAP_GET_NEXT_KEY,
  BPF_PROG_LOAD,
  BPF_OBJ_PIN,
  BPF_OBJ_GET,
  BPF_PROG_ATTACH,
  BPF_PROG_DETACH,
  BPF_PROG_TEST_RUN,
  BPF_PROG_GET_NEXT_ID,
  BPF_MAP_GET_NEXT_ID,
  BPF_PROG_GET_FD_BY_ID,
  BPF_MAP_GET_FD_BY_ID,
  BPF_OBJ_GET_INFO_BY_FD,
  BPF_PROG_QUERY,
  BPF_RAW_TRACEPOINT_OPEN,
  BPF_BTF_LOAD,
  BPF_BTF_GET_FD_BY_ID,
  BPF_TASK_FD_QUERY,
  BPF_MAP_LOOKUP_AND_DELETE_ELEM,
  BPF_MAP_FREEZE,
  BPF_BTF_GET_NEXT_ID,
  BPF_MAP_LOOKUP_BATCH,
  BPF_MAP_LOOKUP_AND_DELETE_BATCH,
  BPF_MAP_UPDATE_BATCH,
  BPF_MAP_DELETE_BATCH,
  BPF_LINK_CREATE,
  BPF_LINK_UPDATE,
  BPF_LINK_GET_FD_BY_ID,
  BPF_LINK_GET_NEXT_ID,
  BPF_ENABLE_STATS,
  BPF_ITER_CREATE,
  BPF_LINK_DETACH,
  BPF_PROG_BIND_MAP,
};

为了方便你掌握,我把用户程序中常用的命令整理成了一个表格,你可以在需要时参考:

BPF 辅助函数

说完用户态程序的 bpf 系统调用格式,我们再来看看内核态的 eBPF 程序。

eBPF 程序并不能随意调用内核函数,因此,内核定义了一系列的辅助函数,用于 eBPF 程序与内核其他模块进行交互。比如,上一讲的 Hello World 示例中使用的 bpf_trace_printk() 就是最常用的一个辅助函数,用于向调试文件系统(/sys/kernel/debug/tracing/trace_pipe)写入调试信息。

这里补充一个知识点:从内核 5.13 版本开始,部分内核函数(如 tcp_slow_start()、tcp_reno_ssthresh()  等)也可以被 BPF 程序直接调用了,具体你可以查看这个链接:Calling kernel functions from BPF [LWN.net]。 不过,这些函数只能在 TCP 拥塞控制算法的 BPF 程序中调用,所以本课程不会过多展开。

需要注意的是,并不是所有的辅助函数都可以在 eBPF 程序中随意使用,不同类型的 eBPF 程序所支持的辅助函数是不同的。比如,对于 Hello World 示例这类内核探针(kprobe)类型的 eBPF 程序,你可以在命令行中执行  bpftool feature probe ,来查询当前系统支持的辅助函数列表:

$ bpftool feature probe
...
eBPF helpers supported for program type kprobe:
  - bpf_map_lookup_elem
  - bpf_map_update_elem
  - bpf_map_delete_elem
  - bpf_probe_read
  - bpf_ktime_get_ns
  - bpf_get_prandom_u32
  - bpf_get_smp_processor_id
  - bpf_tail_call
  - bpf_get_current_pid_tgid
  - bpf_get_current_uid_gid
  - bpf_get_current_comm
  - bpf_perf_event_read
  - bpf_perf_event_output
  - bpf_get_stackid
  - bpf_get_current_task
  - bpf_current_task_under_cgroup
  - bpf_get_numa_node_id
  - bpf_probe_read_str
  - bpf_perf_event_read_value
  - bpf_override_return
  - bpf_get_stack
  - bpf_get_current_cgroup_id
  - bpf_map_push_elem
  - bpf_map_pop_elem
  - bpf_map_peek_elem
  - bpf_send_signal
  - bpf_probe_read_user
  - bpf_probe_read_kernel
  - bpf_probe_read_user_str
  - bpf_probe_read_kernel_str
...

对于这些辅助函数的详细定义,你可以在命令行中执行  man bpf-helpers ,或者参考内核头文件 include/uapi/linux/bpf.h ,来查看它们的详细定义和使用说明。为了方便你掌握,我把常用的辅助函数整理成了一个表格,你可以在需要时参考:

这其中,需要你特别注意的是以bpf_probe_read  开头的一系列函数。我在上一讲中已经提到,eBPF 内部的内存空间只有寄存器和栈。所以,要访问其他的内核空间或用户空间地址,就需要借助 bpf_probe_read  这一系列的辅助函数。这些函数会进行安全性检查,并禁止缺页中断的发生。

而在 eBPF 程序需要大块存储时,就不能像常规的内核代码那样去直接分配内存了,而是必须通过 BPF 映射(BPF Map)来完成。接下来,我带你看看 BPF 映射的具体原理。

BPF 映射

BPF 映射用于提供大块的键值存储,这些存储可被用户空间程序访问,进而获取 eBPF 程序的运行状态。eBPF 程序最多可以访问 64 个不同的 BPF 映射,并且不同的 eBPF 程序也可以通过相同的 BPF 映射来共享它们的状态。下图(图片来自docs.cilium.io)展示了 BPF 映射的基本使用方法。

在前面的 BPF 系统调用和辅助函数小节中,你也看到,有很多系统调用命令和辅助函数都是用来访问 BPF 映射的。我相信细心的你已经发现了:BPF 辅助函数中并没有 BPF 映射的创建函数,BPF 映射只能通过用户态程序的系统调用来创建。比如,你可以通过下面的示例代码来创建一个 BPF 映射,并返回映射的文件描述符:

int bpf_create_map(enum bpf_map_type map_type,
       unsigned int key_size,
       unsigned int value_size, unsigned int max_entries)
{
  union bpf_attr attr = {
    .map_type = map_type,
    .key_size = key_size,
    .value_size = value_size,
    .max_entries = max_entries
  };
  return bpf(BPF_MAP_CREATE, &attr, sizeof(attr));
}

这其中,最关键的是设置映射的类型。内核头文件 include/uapi/linux/bpf.h 中的  bpf_map_type 定义了所有支持的映射类型,你可以使用如下的 bpftool 命令,来查询当前系统支持哪些映射类型:

$ bpftool feature probe | grep map_type
eBPF map_type hash is available
eBPF map_type array is available
eBPF map_type prog_array is available
eBPF map_type perf_event_array is available
eBPF map_type percpu_hash is available
eBPF map_type percpu_array is available
eBPF map_type stack_trace is available
...

在下面的表格中,我给你整理了几种最常用的映射类型及其功能和使用场景:

如果你的 eBPF 程序使用了 BCC 库,你还可以使用预定义的宏来简化 BPF 映射的创建过程。比如,对哈希表映射来说,BCC 定义了  BPF_HASH(name, key_type=u64, leaf_type=u64, size=10240),因此,你就可以通过下面的几种方法来创建一个哈希表映射:

// 使用默认参数 key_type=u64, leaf_type=u64, size=10240
BPF_HASH(stats);

// 使用自定义key类型,保持默认 leaf_type=u64, size=10240
struct key_t {
  char c[80];
};
BPF_HASH(counts, struct key_t);

// 自定义所有参数
BPF_HASH(cpu_time, uint64_t, uint64_t, 4096);

除了创建之外,映射的删除也需要你特别注意。BPF 系统调用中并没有删除映射的命令,这是因为 BPF 映射会在用户态程序关闭文件描述符的时候自动删除(即close(fd) )。 如果你想在程序退出后还保留映射,就需要调用  BPF_OBJ_PIN 命令,将映射挂载到 /sys/fs/bpf 中。

在调试 BPF 映射相关的问题时,你还可以通过 bpftool 来查看或操作映射的具体内容。比如,你可以通过下面这些命令创建、更新、输出以及删除映射:

//创建一个哈希表映射,并挂载到/sys/fs/bpf/stats_map(Key和Value的大小都是2字节)
bpftool map create /sys/fs/bpf/stats_map type hash key 2 value 2 entries 8 name stats_map

//查询系统中的所有映射
bpftool map
//示例输出
//340: hash  name stats_map  flags 0x0
//        key 2B  value 2B  max_entries 8  memlock 4096B

//向哈希表映射中插入数据
bpftool map update name stats_map key 0xc1 0xc2 value 0xa1 0xa2

//查询哈希表映射中的所有数据
 
bpftool map dump name stats_map
//示例输出
//key: c1 c2  value: a1 a2
//Found 1 element

//删除哈希表映射
rm /sys/fs/bpf/stats_map

BPF 类型格式 (BTF)

了解过 BPF 辅助函数和映射之后,我们再来看一个开发 eBPF 程序时最常碰到的问题:内核数据结构的定义。

在安装 BCC 工具的时候,你可能就注意到了,内核头文件 linux-headers-$(uname -r) 也是必须要安装的一个依赖项。这是因为 BCC 在编译 eBPF 程序时,需要从内核头文件中找到相应的内核数据结构定义。这样,你在调用 bpf_probe_read 时,才能从内存地址中提取到正确的数据类型。

但是,编译时依赖内核头文件也会带来很多问题。主要有这三个方面:

首先,在开发 eBPF 程序时,为了获得内核数据结构的定义,就需要引入一大堆的内核头文件;

其次,内核头文件的路径和数据结构定义在不同内核版本中很可能不同。因此,你在升级内核版本时,就会遇到找不到头文件和数据结构定义错误的问题;

最后,在很多生产环境的机器中,出于安全考虑,并不允许安装内核头文件,这时就无法得到内核数据结构的定义。在程序中重定义数据结构虽然可以暂时解决这个问题,但也很容易把使用着错误数据结构的 eBPF 程序带入新版本内核中运行。

那么,这么多的问题该怎么解决呢?不用担心,BPF 类型格式(BPF Type Format, BTF)的诞生正是为了解决这些问题。从内核 5.2 开始,只要开启了 CONFIG_DEBUG_INFO_BTF,在编译内核时,内核数据结构的定义就会自动内嵌在内核二进制文件 vmlinux 中。并且,你还可以借助下面的命令,把这些数据结构的定义导出到一个头文件中(通常命名为 vmlinux.h):

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

如下图(图片来自GRANT SELTZER 博客:https://www.grant.pizza/blog/vmlinux-header/)所示,有了内核数据结构的定义,你在开发 eBPF 程序时只需要引入一个 vmlinux.h 即可,不用再引入一大堆的内核头文件了。

同时,借助 BTF、bpftool 等工具,我们也可以更好地了解 BPF 程序的内部信息,这也会让调试变得更加方便。比如,在查看 BPF 映射的内容时,你可以直接看到结构化的数据,而不只是十六进制数值:

# bpftool map dump id 386
[
  {
      "key": 0,
      "value": {
          "eth0": {
              "value": 0,
              "ifindex": 0,
              "mac": []
          }
      }
  }
]

解决了内核数据结构的定义问题,接下来的问题就是,如何让 eBPF 程序在内核升级之后,不需要重新编译就可以直接运行。eBPF 的一次编译到处执行(Compile Once Run Everywhere,简称 CO-RE)项目借助了 BTF 提供的调试信息,再通过下面的两个步骤,使得 eBPF 程序可以适配不同版本的内核:

第一,通过对 BPF 代码中的访问偏移量进行重写,解决了不同内核版本中数据结构偏移量不同的问题;

第二,在 libbpf 中预定义不同内核版本中的数据结构的修改,解决了不同内核中数据结构不兼容的问题。

BTF 和一次编译到处执行带来了很多的好处,但你也需要注意这一点:它们都要求比较新的内核版本(>=5.2),并且需要非常新的发行版(如 Ubuntu 20.10+、RHEL 8.2+ 等)才会默认打开内核配置 CONFIG_DEBUG_INFO_BTF。对于旧版本的内核,虽然它们不会再去内置 BTF 的支持,但开源社区正在尝试通过 BTFHub:GitHub - aquasecurity/btfhub: BTFhub, in collaboration with the BTFhub Archive repository, supplies BTF files for all published kernels that lack native support for embedded BTF. This joint effort ensures that even kernels without built-in BTF support can effectively leverage the benefits of eBPF programs, promoting compatibility across various kernel versions. 等方法,为它们提供 BTF 调试信息。

小结

我带你一起梳理了 eBPF 程序跟内核交互的基本方法。

一个完整的 eBPF 程序,通常包含用户态和内核态两部分:用户态程序需要通过 BPF 系统调用跟内核进行交互,进而完成 eBPF 程序加载、事件挂载以及映射创建和更新等任务;而在内核态中,eBPF 程序也不能任意调用内核函数,而是需要通过 BPF 辅助函数完成所需的任务。尤其是在访问内存地址的时候,必须要借助  bpf_probe_read 系列函数读取内存数据,以确保内存的安全和高效访问。

在 eBPF 程序需要大块存储时,我们还需要根据应用场景,引入特定类型的 BPF 映射,并借助它向用户空间的程序提供运行状态的数据。

这一讲的最后,我还带你一起了解了 BTF 和 CO-RE 项目,它们在提供轻量级调试信息的同时,还解决了跨内核版本的兼容性问题。很多开源社区的 eBPF 项目(如 BCC 等)也都在向 BTF 进行迁移。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1118833.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VNC Viewer安装教程(保姆级安装)

一、 VNC Viewer简介 VNC是一款开源的远程控制软件&#xff0c;功能强大且高效实用&#xff0c;其性能不逊色同类软件&#xff0c;它的工作原理和WIN远程控制软件类似&#xff0c;但是更为重要的是&#xff0c;VNC-Viewer完全免费开源&#xff0c;更新速度也比较快&#xff01;…

解释CSS伪类和伪元素的区别,并举例说明。

CSS伪类和伪元素是用于选择HTML文档中特定元素或元素的部分内容的CSS选择器。它们的区别在于它们所选择的目标不同。 CSS伪类&#xff08;Pseudo-classes&#xff09;是用于选择符合特定状态或特定条件的元素。伪类以冒号&#xff08;:&#xff09;开头&#xff0c;用于选择元…

【VSCode】解决Open in browser无效

问题描述&#xff1a; 在VSCode中无论是点击右键&#xff0c;选择在默认浏览器中打开&#xff0c;还是按快捷键alt b都没有反应。 解决办法&#xff1a; 右击文件 --> 在文件资源管理器中显示 右击文件&#xff0c;选择属性 点击更改 选择用默认浏览器打开 最后 此时…

Redis 命令—— 超详细操作演示!!!

内存数据库 Redis7 三、Redis 命令3.1 Redis 基本命令3.2 Key 操作命令3.3 String 型 Value 操作命令3.4 Hash 型 Value 操作命令3.5 List 型 Value 操作命令3.6 Set 型 Value 操作命令3.7 有序Set 型 Value 操作命令3.8 benchmark 测试工具3.9 简单动态字符串SDS3.10 集合的底…

Whisper 整体架构图

Attention 注意力机制模块&#xff0c;兼容自注意力和交叉注意力。 AttentionBlock Transformer 模块&#xff0c;包含一个自注意力&#xff0c;一个交叉注意力&#xff08;可选&#xff09;和一个 MLP 模块。 AudioEncoderTextDecoder 音频编码器和文本解码器。编码器的 Tr…

【Unity地编】地形系统搭建入门详解

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;UI_…

【Bug】8086汇编学习

文章目录 随笔Bug1、masm编译报错&#xff1a;Illegal use of register2、debug中使用段前缀3、[idata]在编译器中的处理4、push立即数报错5、报错&#xff1a;improper operand type6、程序莫名跳转到未知位置 (doing)7、DOSBox失去响应8、程序运行显示乱码9、程序运行导致DOS…

Vue中的mixins是什么?

在Vue中,Mixins(混入)是一种可重用的代码块,用于在多个组件之间共享逻辑和功能。通过使用Mixins,可以将一组属性、方法和生命周期钩子等混合到多个组件中,从而实现代码的复用和组件之间的共享功能。 使用Mixins有以下几个步骤: 1:创建Mixins: 创建一个包含共享代码的…

c#中使用Task.WhenAll

&#x1f680;简介 Task.WhenAll用于等待所有提供的Task对象完成执行。这个方法返回一个新的Task&#xff0c;这个Task将在所有提供的Task完成后完成。如果任何一个Task失败&#xff0c;Task.WhenAll返回的Task也将以异常状态完成。这个方法非常适合在你需要并行执行多个操作&…

(二)docker:建立oracle数据库mount startup

这章其实我想试一下startup部分做mount&#xff0c;因为前一章在建完数据库容器后&#xff0c;需要手动创建用户&#xff0c;授权&#xff0c;建表等&#xff0c;好像正好这部分可以放到startup里&#xff0c;在创建容器时直接做好&#xff1b;因为setup部分我实在没想出来能做…

ES6 Promise 详解

目录 一、Promise基本介绍 二、Promise实现多次请求 1.传统Ajax方式实现多次请求 : 1.1 json数据准备 1.2 JQuery操作Ajax 2.使用ES6新特性Promise方式 : 三、Promise代码重排优化 1.问题分析 : 2.代码优化 : 2.1 数据准备 2.2 代码重排 一、Promise基本介绍 (1) Ajax方…

四川农业大学就业指南←缺失的就业指导课

推荐 125页就业指南&#xff0c; 包含就业去向、就业政策介绍&#xff0c; 也有毕业生常见问题&#xff0c; 就业协议与劳动合同的阐释&#xff0c; 帮助毕业生系统梳理庞杂的各类就业信息&#xff0c; 人文温度冲淡就业惨淡的灰冷阴霾。 有这样一群人&#xff0c;在2023&…

联邦学习的梯度重构

梯度泄露的攻击方法&#xff1a;深度泄露梯度&#xff08;DLG&#xff09;——>在高度压缩的场景下是失效的 原因&#xff1a;梯度压缩&#xff08;可减小通信开销&#xff09;——>存在信息损失<——从而DLG方法效果有限 但是这本身存在的信息损失怎么解决呢&#x…

前端刷题 | 网站

W3Cschoolhttps://www.w3cschool.cn/exam 计算机方面的知识涵盖较全 牛客网 应届生招聘题库&#xff0c;校招实习笔试面试真题 力扣 前端方面的题目较为基础&#xff0c;基本不考复杂算法题 稀土掘金https://juejin.cn/search?query%E5%89%8D%E7%AB%AF%E9%9D%A2%E8%AF%95&a…

基因表达分析聚类分析

基因表达分析聚类&分析 1. Introduction to gene expression analysis Technology: microarrays vs. RNAseq. Resulting data matricesSupervised (Clustering) vs. unsupervised (classification) learning 微阵列技术&#xff1a; 制备DNA探针阵列并进行互补性杂交。 …

(2)Nmap

笔记目录 渗透测试工具(1)wireshark 渗透测试工具(2)Nmap渗透测试工具(3)Burpsuite 1.工具简介 (1)定义 ①功能 网络扫描和嗅探工具包&#xff0c;三个主要基本功能&#xff1a; 探测一组主机是否在线 扫描主机端口、嗅探所提供的网络服务 推断出主机所用的操作系统 ②namp …

配置Linux

首先安装VMware&#xff1a; 安装说明&#xff1a;&#xff08;含许可证的key&#xff09; https://mp.weixin.qq.com/s/XE-BmeKHlhfiRA1bkNHTtg 给大家提供了VMware Workstation Pro16&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1q8VE3TkPzDnM3u9bkTdA_g 提取码&…

【【萌新的FPGA学习之快速回顾 水 水 】】

萌新的FPGA学习之快速回顾 水 水 上一条FPGA的更新在9 25 并且2个礼拜没写 verilog 了 正好 刷新一下记忆 FPGA CPU DSP 的对比 在数字电路发展多年以来&#xff0c;出现了 CPU、DSP 和 FPGA 三种经典器件&#xff0c;每个都是具有划时代意义的器件。CPU、DSP 和 FPGA 都有各…

[SQL开发笔记]在windows系统安装Postgres

一、软件简介 PostgreSQL是一种自由软件的对象-关系型数据库管理系统&#xff08;ORDBMS&#xff09;&#xff0c;它以加州大学计算机系开发的POSTGRES&#xff0c;4.2版本为基础。PostgreSQL支持大部分的SQL标准并且提供了很多其他现代特性&#xff0c;如复杂查询、外键、触发…

5G来临,迎客莱带你探索运营商大数据的应用

随着5G时代的来临&#xff0c;不仅在算力的基础上得到了加强和保障&#xff0c;同时也丰富了计算的方式和模式&#xff0c;如边缘计算、霾计算等。计算方式和模式的改变&#xff0c;对于运营商来说&#xff0c;意味着更丰富的数据维度&#xff0c;更鲜活的数据和更强大的数据处…