前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【eBPF笔记中篇】运行原理、交互、event触发 解析(未完)

【eBPF笔记中篇】运行原理、交互、event触发 解析(未完)

原创
作者头像
历久尝新
修改2023-05-24 17:42:42
2.6K0
修改2023-05-24 17:42:42
举报
文章被收录于专栏:学而时习之学而时习之

一、eBPF的虚拟机在内核是如何工作的?

从之前的分析已经得知,.c的eBPF程序会通过BCC等工具编译并加载到内核中,但是具体在内核中,ebpf是如何工作的呢?

eBPF运行描述
eBPF运行描述

eBPF在内核中运行主要是由五个模块组成

  • BPF Verifier:它提供了一系列用于 eBPF 程序与内核其他模块进行交互的函数;
  • BPF JIT:将由LLVM从内核态程序(例如上篇case中的hello.c)转成BPF bytecode 再次译成本地机器指令,以便更高效地在内核中执行;
  • BPF Helpers:提供了用于 eBPF 程序与内核其他模块进行交互的函数,hello.c 使用的bpf_get_current_pid_tgid、bpf_ktime_get_ns等函数;
  • BPF 存储模块: 11 个 64 位寄存器、一个程序计数器和一个 512 字节的栈组成的存储模块;
  • BPF Map:Large custom data storage,用来与用户态程序进行交互(例如上篇case中的hello.py)。

二、再通过一个case详细解析下在内核中的指令执行!

我是case

eBPF程序,hello.c

代码语言:javascript
复制
int hello_world(void *ctx)
{
  bpf_trace_printk("Hello, World!");
  return 0;
}
  • bpf_trace_printk()  输出一段字符串,因为eBPF在内核中运行,所以不能stdout,而是在 /sys/kernel/debug/tracing/trace_pipe,需要用户态程序调用trace_print()输出,或者可以cat

用户态程序

代码语言:javascript
复制
1 #!/usr/bin/env python3
2 from bcc import BPF
3
4 b = BPF(src_file="hello.c")
5 b.attach_kprobe(event="do_sys_openat2", fn_nam    e="hello_world")
6 b.trace_print()
~
  • 第二行:导入bcc模块
  • 第四行:编译并加载eBPF程序
  • 第五行:将 eBPF程序挂载到kprobe,并绑定事件openat()
  • 第六行:读取内核调试文件 /sys/kernel/debug/tracing/trace_pipe 的内容,并打印到标准输出中。

运行命令python3 helle.py

bpftool:查看eBPF的运行状态

当上述case运行后,执行bpftool prog list命令

代码语言:javascript
复制
root@ubuntu-impish:/home/ebpf-test/case1# sudo bpftool prog list
...
580: kprobe  name hello_world  tag 38dd440716c4900f  gpl
	loaded_at 2022-02-16T14:30:56+0000  uid 0
	xlated 104B  jited 70B  memlock 4096B
	btf_id 66
  • 580:eBPF程序的编号(实际上在虚拟机上会跑出来很多cgroup的eBPF程序哦= =!)
  • kprob:程序类型,内核态插桩
  • name hello_word:程序名

通过eBPF程序编号,可以查看这个程序所有的指令,执行bpftool prog dump xlated id 580

代码语言:javascript
复制
root@ubuntu-impish:/home/ebp   f-test/case1# bpftool prog dump xlated id 580
int hello_world(void * ctx):
; int hello_world(void *ctx)
   0: (b7) r1 = 33
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
   1: (6b) *(u16 *)(r10 -4) = r1
   2: (b7) r1 = 1684828783
   3: (63) *(u32 *)(r10 -8) = r1
   4: (18) r1 = 0x57202c6f6c6c6548
   6: (7b) *(u64 *)(r10 -16) = r1
   7: (bf) r1 = r10
;
   8: (07) r1 += -16
; ({ char _fmt[] = "Hello, World!"; bpf_trace_printk_(_fmt, sizeof(_fmt)); });
   9: (b7) r2 = 14
  10: (85) call bpf_trace_printk#-61856
; return 0;
  11: (b7) r0 = 0
  12: (95) exit
  • 分号后:c的代码;
  • 冒号前面的数字0-12:代表 BPF 指令行数;
  • 括号中的 16 进制数值:表示 BPF 指令码,为64位寄存器赋值,指令码含义可以参考IOVisor BPF
  • 括号后面:BPF 指令的伪代码;

由此可看,LLVM编码后的BPF指令中标记了存储模块中各个寄存器的调用,上面的程序指令含义如下:

  • 第 0-8 行,借助 R10 寄存器从栈中把字符串 “Hello, World!” 读出来,并放入 R1 寄存器中;
  • 第 9 行,向 R2 寄存器写入字符串的长度 14(即代码注释里面的 sizeof(_fmt) );
  • 第 10 行,调用 BPF 辅助函数 bpf_trace_printk 输出字符串;
  • 第 11 行,向 R0 寄存器写入 0,表示程序的返回值是 0;
  • 最后一行,程序执行成功退出。

当上面的BPF指令加载到内核后,JIT会将BPF bytecode再次转成机器指令,执行bpftool prog dump jited id 580

查看BPF程序的机器指令

strace:用进程照妖镜,分析下BPF程序都做了啥

我们写的BPF用户态程序hello.py使用了BCC完成eBPF内核态程序hello.c的编译与加载,跟踪BCC的系统调用过程,

可以执行strace -v -f -ebpf ./hello.py过一会儿可以看到BCC调用bpf加载

代码语言:javascript
复制
bpf(BPF_PROG_LOAD, 
    {prog_type=BPF_PROG_TYPE_KPROBE,
     insn_cnt=13, 
     insns=[
     {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x21}, 
     {code=BPF_STX|BPF_H|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-4, imm=0}, 
     {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x646c726f}, 
     {code=BPF_STX|BPF_W|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-8, imm=0}, 
     {code=BPF_LD|BPF_DW|BPF_IMM, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0x6c6c6548}, 
     {code=BPF_LD|BPF_W|BPF_IMM, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x57202c6f}, 
     {code=BPF_STX|BPF_DW|BPF_MEM, dst_reg=BPF_REG_10, src_reg=BPF_REG_1, off=-16, imm=0}, 
     {code=BPF_ALU64|BPF_X|BPF_MOV, dst_reg=BPF_REG_1, src_reg=BPF_REG_10, off=0, imm=0}, 
     {code=BPF_ALU64|BPF_K  |BPF_ADD, dst_reg=BPF_REG_1, src_reg=BPF_REG_0, off=0, imm=0xfffffff0}, 
     {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_2, src_reg=BPF_REG_0, off=0, imm=0xe}, 
     {code=BPF_JMP|BPF_K|BPF_CALL, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0x6}, 
     {code=BPF_ALU64|BPF_K|BPF_MOV, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}, 
     {code=BPF_JMP|BPF_K|BPF_EXIT, dst_reg=BPF_REG_0, src_reg=BPF_REG_0, off=0, imm=0}], 
     license="GPL", log_level=0, log_size=0, log_buf=NULL, 
     kern_version=KERNEL_VERSION(5, 13, 19), prog_flags=0, 
     prog_name="hello_world", prog_ifindex=0, 
     expected_attach_type=BPF_CGROUP_INET_INGRESS, prog_btf_fd=3, func_info_rec_size=8, 
     func_info=0x1f5b990, func_info_cnt=1, line_info_rec_size=16, line_info=0xfc42b0, 
     line_info_cnt=5, attach_btf_id=0, attach_prog_fd=0},
    128) = 4

执行man bpf查看bpf系统调用格式

代码语言:javascript
复制
int bpf(int cmd, union bpf_attr *attr, unsigned int size);

对应前面的 strace 输出结果,参数意义则是

  • 第一个参数是 BPF_PROG_LOAD , 表示加载 BPF 程序;
  • 第二个参数是 bpf_attr 类型的结构体,表示 BPF 程序的属性。其中
    • prog_type 表示 BPF 程序的类型,这里 BPF_PROG_TYPE_KPROBE 的跟用户态代码中的 attach_kprobe 一致;
    • insn_cnt (instructions count) 表示指令条数,与前面 bpftool prog dump 的结果是一致;
    • insns (instructions) 包含了具体的每一条指令;
    • prog_name 则表示 BPF 程序的名字,即 hello_world 。
  • 第三个参数 120 表示属性的大小。

其实eBPF 程序需要事件触发后才会执行,其中我们用户态程序hello.py中,

代码语言:javascript
复制
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")

调用了 attach_kprobe 函数,绑定了一个内核跟踪事件

执行strace -v -f ./hello.py再看下系统调用

代码语言:javascript
复制
...
/* 1) 加载BPF程序 */
bpf(BPF_PROG_LOAD,...) = 4
...

/* 2)查询事件类型 */
openat(AT_FDCWD, "/sys/bus/event_source/devices/kprobe/type", O_RDONLY) = 5
read(5, "6\n", 4096)                    = 2
close(5)                                = 0
...

/* 3)创建性能监控事件 */
perf_event_open(
    {
        type=0x6 /* PERF_TYPE_??? */,
        size=PERF_ATTR_SIZE_VER7,
        ...
        wakeup_events=1,
        config1=0x7f275d195c50,
        ...
    },
    -1,
    0,
    -1,
    PERF_FLAG_FD_CLOEXEC) = 5

/* 4)绑定BPF到kprobe事件 */
ioctl(5, PERF_EVENT_IOC_SET_BPF, 4)     = 0
...
  1. 借助 bpf 系统调用,加载 BPF 程序,并记住返回的文件描述符;
  2. 查询 kprobe 类型的事件编号。BCC 实际上是通过 /sys/bus/event_source/devices/kprobe/type 来查询的;
  3. 调用 perf_event_open 创建性能监控事件。比如,事件类型(type 是上一步查询到的 6)、事件的参数( config1 包含了内核函数 do_sys_openat2 )等;
  4. 再通过 ioctl 的 PERF_EVENT_IOC_SET_BPF 命令,将 BPF 程序绑定到性能监控事件。

eBPF程序:

C

  • struct data_t:定义想在用户态中获取的数据,没有固定结构;
  • BPF_PERF_OUTPUT:定义test_events,其中用户态.py程序中,也会get相同变量名的event;
  • hello_world:定义kprobe探针的处理函数,除了ctx是固定参数以外,dfd, filename和open_how是openat2的参数,如果入参就必须写全,会自动进行绑定;
  • perf_submit:提交性能事件,参数应该都是固定参数.

Py

  • print_event:定义处理c中struct data_t的处理函数.
  • open_perf_buffer:定义了名为 “test_evens” 的 Perf 事件映射.
  • perf_buff_poll:读取映射的内容,并执行回调函数输出进程信息.

交互

综上,梳理出eBPF在内核中实现

  1. 高级语言开发的eBPF程序,用户态、内核态两部分
  2. 编译成BPF字节码
  3. 借助linux bpf系统调用加载到内核
  4. 通过性能监控等接口与具体的内核事件进行绑定

上篇中也介绍到了,一个完成整eBPF程序中,通常包含用户态程序与内核态程序两部分,

用户态:负责eBPF的编译、加载、事件绑定、结果输出,它与内核进行交互的时候必须通过系统调用来完成

内核态:负责定制和控制系统的运行状态

BPF系统调用

执行man bpf查看bpf系统调用格式

代码语言:javascript
复制
int bpf(int cmd, union bpf_attr *attr, unsigned int size);

参数:

  1. cmd:操作命令,eg BPF_PROG_LOAD 就是加载eBPF程序
  2. attr:bpf_attr类型的eBPF属性指针,不通类型的操作命令需要传入不同的属性参数
  3. size:属性大小

注:不同版本的内核支持的BPF操作命令不一样,常用的BPF操作命令

  • BPF_MAP_CREATE: 创建一个BPF映射
  • BPF_MAP_LOOKUP_ELEM:查找BPF映射
  • BPF_MAP_UPDATE_ELEM:更新BPF映射
  • BPF_MAP_DELETE_ELEM:删除BPF映射
  • BPF_MAP_LOOKUP_AND_DELETE_ELEM:查找并删除BPF映射
  • BPF_MAP_GET_NEXT_ELEM:遍历BPF映射
  • BPF_PROG_LOAD:验证并加载BPF程序
  • BPF_PROG_ATTACH:把BPF程序挂载到内核事件上
  • BPF_PROG_DETACH:把BPF程序从内核事件上卸载
  • BPF_OBJ_PIN:把BPF程序或映射挂载到sysfs中的/sys/fs/bpf目录
  • BPF_OBJ_GET:从/sys/fs/bpf目录中查找BPF程序
  • BPF_BTF_LOAD:验证并加载BTF信息

BPF辅助函数

eBPF程序并不能随便的调用内核函数,必须通过辅助函数才可完成eBPF程序和其他内核模块的交互,eg bpf_trace_printk()

注:不同类型的eBPF程序支持的辅助函数是不同的

执行bpftool feature probe查看bpf系统支持的辅助函数列表

执行man bpf-helpers查看辅助函数的详细定义

注:由于eBPF虚拟机的只有寄存器和栈,所以要访问其他内核空间或者用户控件地址,就需要借助bpf_probe_read系列辅助函数,eg

  • bpf_probe_read:从内存指针中读取数据
  • bpf_probe_read_user:从用户空间内存指针中读取数据
  • bpf_probe_read_kernel:从内核空间内存指针中读取数据

BPF映射

  • BPF映射给eBPF虚拟机提供了大空间的kv存储,可呗用户空间访问,从而获取eBPF程序的运行状态
  • eBPF 程序最多可以访问 64 个不同的 BPF 映射,并且不同的 eBPF 程序也可以通过相同的 BPF 映射来共享它们的状态

BPF映射基本使用方法

bpf map
bpf map

注:BPF Map 只能通过用户态程序的系统调用来创建,并不能通过辅助函数创建,而且在用户态程序关闭文件描述符的时候就会自动删除

eg:

代码语言:javascript
复制
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));
}

执行命令bpftool feature probe | grep map_type 可以查看系统所有可用的映射类型

代码语言:javascript
复制
root@ubuntu-impish:/home/ebpf/hello_case# 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
.....

调试BPF映射

代码语言:javascript
复制
//创建一个哈希表映射,并挂载到/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

BTF

我们在部署eBPF环境的时候,安装了很多头文件,eg linux-headers-$(uname -r) ,这些头文件的作用就是BCC在编译eBPF程序的时候,需要在内核头文件中找到对应的数据结构定义,但是在生产机器中,很多都是不允许安装内核头文件,这个问题要怎么解决呢?

当kernel版本>5.2,只要开启了CONFIG_DEBUG_INFO_BTF,在编译内核时,内核数据结构的定义就会自动内嵌在内核二进制文件 vmlinux 中,so,你可以执行命令bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h将这些数据的定义导出到头文件中vmlinux.h

所以,当有了vmlinux.h后,我们在开发eBPF程序就不用自己定义数据结构(防止将错误的数据结构带入内核中)和引入一堆头文件了

vmlinux.h的使用示意图
vmlinux.h的使用示意图

借助BTF、pbftool等工具,我们可以直接看到BPF映射的结构化数据,eg:

代码语言:javascript
复制
# bpftool map dump id xxxx
[
  {
      "key": 0,
      "value": {
          "eth0": {
              "value": 0,
              "ifindex": 0,
              "mac": []
          }
      }
  }
]

eBPF程序分类

eBPF 程序类型决定了一个 eBPF 程序可以挂载的事件类型和事件参数,内核中不同事件会触发不同类型的 eBPF 程序

一般内核的版本或者编译配置不同,所支持的程序类型也不同,执行bpftool feature probe | grep program_type可以查看当前kernel支持的bpf程序类型

跟踪类eBPF程序

主要用于从系统中提取跟踪信息,进而为监控、排错、性能优化等提供数据支撑,最常用的:

perf_event:用于性能事件跟踪,eg 内核调用,定时器,硬件等

kprobe:用于对特定函数进行动态插桩

tracingpoint:用于内核静态跟踪点

网络类eBPF程序

网络类 eBPF 程序主要用于对网络数据包进行过滤和处理,进而实现网络的观测、过滤、流量控制以及性能优化等各种丰富的功XDP

XDP 程序的类型定义为 BPF_PROG_TYPE_XDP,它在网络驱动程序刚刚收到数据包时触发执行。由于无需通过繁杂的内核网络协议栈,XDP 程序可用来实现高性能的网络处理方案,常用于 DDoS 防御、防火墙、4 层负载均衡等场景

,XDP 程序并不是绕过了内核协议栈,它只是在内核协议栈之前处理数据包,而处理过的数据包还可以正常通过内核协议栈继续处理。你可以通过下面的图片加深对  XDP 相对内核协议栈位置的理解

根据网卡和网卡驱动是否原生支持 XDP 程序,XDP 运行模式可以分为下面这三种:

通用模式。它不需要网卡和网卡驱动的支持,XDP 程序像常规的网络协议栈一样运行在内核中,性能相对较差,一般用于测试

原生模式。它需要网卡驱动程序的支持,XDP 程序在网卡驱动程序的早期路径运行;

卸载模式。它需要网卡固件支持 XDP 卸载,XDP 程序直接运行在网卡上,而不再需要消耗主机的 CPU 资源,具有最好的性能。

XDP 程序在处理过网络包之后,都需要根据 eBPF 程序执行结果,决定数据包的去处。这些执行结果对应以下 5 种 XDP 程序结果码:

TC

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、eBPF的虚拟机在内核是如何工作的?
    • eBPF在内核中运行主要是由五个模块组成
    • 二、再通过一个case详细解析下在内核中的指令执行!
      • 我是case
        • bpftool:查看eBPF的运行状态
          • strace:用进程照妖镜,分析下BPF程序都做了啥
          • 交互
            • BPF系统调用
              • BPF辅助函数
                • BPF映射
                  • BTF
                  • eBPF程序分类
                  相关产品与服务
                  应用性能监控
                  应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档