从之前的分析已经得知,.c的eBPF程序会通过BCC等工具编译并加载到内核中,但是具体在内核中,ebpf是如何工作的呢?
eBPF程序,hello.c
int hello_world(void *ctx)
{
bpf_trace_printk("Hello, World!");
return 0;
}
用户态程序
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()
~
运行命令python3 helle.py
当上述case运行后,执行bpftool prog lis
t命令
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
通过eBPF程序编号,可以查看这个程序所有的指令,执行bpftool prog dump xlated id 580
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
由此可看,LLVM编码后的BPF指令中标记了存储模块中各个寄存器的调用,上面的程序指令含义如下:
当上面的BPF指令加载到内核后,JIT会将BPF bytecode再次转成机器指令,执行bpftool prog dump jited id 580
查看BPF程序的机器指令
我们写的BPF用户态程序hello.py使用了BCC完成eBPF内核态程序hello.c的编译与加载,跟踪BCC的系统调用过程,
可以执行strace -v -f -ebpf ./hello.py
过一会儿可以看到BCC调用bpf加载
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系统调用格式
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
对应前面的 strace 输出结果,参数意义则是
其实eBPF 程序需要事件触发后才会执行,其中我们用户态程序hello.py中,
b.attach_kprobe(event="do_sys_openat2", fn_name="hello_world")
调用了 attach_kprobe 函数,绑定了一个内核跟踪事件
执行strace -v -f ./hello.py
再看下系统调用
...
/* 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
...
eBPF程序:
C
Py
综上,梳理出eBPF在内核中实现
上篇中也介绍到了,一个完成整eBPF程序中,通常包含用户态程序与内核态程序两部分,
用户态:负责eBPF的编译、加载、事件绑定、结果输出,它与内核进行交互的时候必须通过系统调用来完成
内核态:负责定制和控制系统的运行状态
执行man bpf
查看bpf系统调用格式
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
参数:
注:不同版本的内核支持的BPF操作命令不一样,常用的BPF操作命令
eBPF程序并不能随便的调用内核函数,必须通过辅助函数才可完成eBPF程序和其他内核模块的交互,eg bpf_trace_printk()
注:不同类型的eBPF程序支持的辅助函数是不同的
执行bpftool feature probe
查看bpf系统支持的辅助函数列表
执行man bpf-helpers
查看辅助函数的详细定义
注:由于eBPF虚拟机的只有寄存器和栈,所以要访问其他内核空间或者用户控件地址,就需要借助bpf_probe_read系列辅助函数,eg
BPF映射基本使用方法
注:BPF Map 只能通过用户态程序的系统调用来创建,并不能通过辅助函数创建,而且在用户态程序关闭文件描述符的时候就会自动删除
eg:
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
可以查看系统所有可用的映射类型
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映射
//创建一个哈希表映射,并挂载到/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
我们在部署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程序就不用自己定义数据结构(防止将错误的数据结构带入内核中)和引入一堆头文件了
借助BTF、pbftool等工具,我们可以直接看到BPF映射的结构化数据,eg:
# bpftool map dump id xxxx
[
{
"key": 0,
"value": {
"eth0": {
"value": 0,
"ifindex": 0,
"mac": []
}
}
}
]
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 删除。