前言:当前eBPF技术在日常问题诊断中用的越来越多,也越来越成熟。eBPF可以定位内核问题,也可以进行网络相关的性能优化。尤其在不允许服务器重启的场景,eBPF作用更加突出。即写即用,工具文件写好可直接在环境执行,效率也非常高。
1.如何编写eBPF
1.1BCC 工具安装
在系统中安装 BCC 工具是比较简单的,可以使用以下命令:
sudo yum install bcc kernel-herders-$(uname -r);
上面如果按照不了,则安装:sudo yum install bcc-tools;
BCC 工具可以让你使用 Python 和 C 语言组合来编写 eBPF 程序。
安装完成后,可以使用命令 bcc -v来测试是否安装成功。如果安装失败,可以参考官网安装文档,如下:https://github.com/iovisor/bcc/blob/master/INSTALL.md
1.2 Hello world程序
eBPF包含两部分:
(1)内核态执行的c文件
(2)python文件,负责加载c文件到内核态,挂载c文件的接口到kprobe接口等。
例子创建两个文件:hello.c、hello.py。hello.c 打印了文件fd、文件名称,hello.py 把hello.c hello_world接口通过attach_kprobe attach到do_sys_open接口。
执行:python3 hello.py,即可运行本例eBPF程序。
hello.c:
int hello_world(struct pt_regs *regs, int dfd, const char __user *filename,
int flags, umode_t mode)
{
bpf_trace_printk("Hello, World!:%d\n", dfd);
bpf_trace_printk("Hello, World!:%s\n", filename);
return 0;
}
hello.py:
from bcc import BPF
b = BPF(src_file="hello.c")
b.attach_kprobe(event="do_sys_open", fn_name="hello_world")
b.trace_print()
1.3例子编写
需要先安装bcc工具,安装后,/usr/share/bcc/tools目录,有eBPF的可用工具。tools目录的文件,可以直接vim打开,查看内部实现,可修改内部实现。
例子1:
/usr/share/bcc/tools/syncsnoop文件;第一个框是c代码,会通过bpf系统调用load到内核bpf虚拟机,BPF(text””c代码在这里 ””),第二个框是python主体部分;通过kprobe跟踪内核函数,Kprobe使用方式,通过attach_kprobe跟踪sync系统调用。
/usr/share/bcc/tools/memleak文件c部分,采用的trace point方式跟踪接口,C代码直接在TRACEPOINT_PROBE()函数体实现,trace point对应内核中的实现,mm/ slub.c,trace_kmalloc(_RET_IP_, ret, size, s->size, flags)。
初期编写,可以参考/usr/share/bcc/tools目录下的文件编写,复制后一个文件,按照需要进行c部分内核功能需求编写;python部分包括:信息输出、挂载到kprobe、trace_point、过滤器等修改,例如,要跟踪哪一个接口?有多种跟踪方式,有kprobe、trace point等,选择一种即可;可以修改b.attach_kprobe部分,这个例子跟踪内核schedule接口,offcpu是自己实现的函数,b.attach_kprobe(event="schedule", fn_name="offcpu"),如果用的trace_point,可以修改TRACEPOINT_PROBE部分,其中sys_enter是内核中添加的trace point:TRACEPOINT_PROBE(raw_syscalls, sys_enter) 。查看kprobe支持哪些函数:bpftrace –l | grep 函数名,查找的到,则说明可能支持。
例子2:
/usr/share/bcc/tools/vfscount为例
第1部分,指明python
第2部分,导入需要的库
第3部分,需要下载到内核的代码,通过c语言实现。
本例中,实现了do_count函数,定义了counts的一个hash表,在do_count函数中,记录调用vfs_*函数的次数,记录在counts,key.ip = PT_REGS_IP(ctx);记录了函数ip,可以在通过b.ksym(k.ip),找到对应函数名,Counts 记录了本ip对应的vfs_*函数的调用次数;
第4部分,通过kprobe,把do_count函数attach到vfs_*接口
attach了vfs_开头的接口
第5部分,输出do_count统计的数据
通过do_count统计的counts表来输出通过此方式,counts = b.get_table(“counts”),拿到内核部分counts表,通过sorted,取得counts记录的第一个参数ip,第二个参数调用次数,通过print输出相关信息。
例子3:
/usr/share/bcc/tools/hardirqs为例
第1部分,指明python
第2部分,导入需要的库
第3部分,需要下载到内核的代码,通过c语言实现。本例中,通过trace point来跟踪,因此没有用kprobe,定义了hash表dist/irqnames,在trace函数中,通过上述hash表来记录中断信息
第4部分,输出统计的数据
2.eBPF 限制
3.eBPF架构
eBPF本身分为用户态和内核态两部分:
用户态部分负责加载 BPF 字节码至内核,负责读取内核回传的统计信息或者事件详情。
内核态的 BPF 字节码负责在内核中执行特定事件,如需要也会将执行的结果通过 maps 或者 perf-event 事件发送至用户空间。
其中用户态程序与内核态 BPF 字节码程序可以使用 map 结构实现双向通信,这为内核中运行的 BPF 字节码程序提供了更加灵活的控制
用户态与内核态 BPF 字节码交互的流程:
(1)使用 LLVM 或者 GCC 工具将编写的 BPF 代码程序编译成 BPF 字节码
(2)使用加载程序 Loader 将字节码加载至内核
(3)内核使用验证器(Verfier) 组件保证执行字节码的安全性,以避免对内核造成灾难,在确认字节码安全后将其加载对应的内核模块执行
(4)内核中运行的 BPF 字节码程序可以使用两种方式将数据回传至用户空间。maps 方式可用于将内核中实现的统计摘要信息(比如测量延迟、堆栈信息)等回传至用户空间;perf-event 用于将内核采集的事件实时发送至用户空间,用户空间程序实时读取分析。
(上图源于网络)
4.eBPF源码
4.1目录组织
用户态load:
(1) elf格式解析源码目录:tkernel4/samples/bpf/。
(2) Libbpf:提供进一步的代码下载bpf代码,目录:tkernel4/tools/lib/bpf/,例如:bpf_load_program,最终调用__NR_bpf系统调用完成相关命令。
内核态,目录tkernel4/kernel/bpf/,
(1) 通过syscall.c: bpf()系统调用完成加载解析等;
(2) core.c:bpf_prog_select_runtime(),选择运行时等。虚拟机模式:interpreters[(round_up(stack_depth, 32) / 32) - 1];Jit模式:
(3) verifier.c: bpf_check() verify入口等其他文件。
bpf()系统调用命令cmd
4.2源码分析
用户态包括eBPF工具函数、用户态lib库。内核态包括了eBPF runtime、kprobe机制、ftrace、trace pointe等。
eBPF py文件由用户态库libbpf,通过bpf系统调用加载到内核态,在eBPF内核态做校验,并且选择runtime:jit or 解释器。如果用户态选择的kprobe,则会在内核把eBPF的函数钩子注册到ftrace链表。在被hook的函数被调用时,就会执行钩子函数。
1.func_id与用户态程序调用函数之间的关系
func_id,头文件定义了BPF_FUNC开头的宏,include/uapi/linux/bpf.h
enum bpf_func_id {
__BPF_FUNC_MAPPER(__BPF_ENUM_FN)
__BPF_FUNC_MAX_ID,
};
__BPF_FUNC_MAPPE宏定义了网络相关的函数,展开一个:此宏即为func_id
BPF_FUNC_msg_redirect_hash。
用户态程序,tools/testing/selftests/bpf/bpf_helpers.h:把用户态eBPF函数定义为宏,ebpf用户态程序使用的函数,
bpf_msg_redirect_hash(msg, &sock_map_redir, &key, flags);
tools/testing/selftests/bpf/ test_sockmap_kern.h 把eBPF函数和宏建立联系:
static int (*bpf_msg_redirect_hash)(void *ctx, void *map, void *key, int flags) = (void *) BPF_FUNC_msg_redirect_hash
2. func_id与内核态网络功能函数的关系
net/core/filter.c 定义了sk_msg_verifier_ops,此ops提供了get_func_proto函数: sk_msg_func_proto,这个函数,根据func_id,找到struct bpf_func_proto结构,例如:
bpf_msg_redirect_hash_proto:
const struct bpf_verifier_ops sk_msg_verifier_ops = {
.get_func_proto = sk_msg_func_proto,
.is_valid_access = sk_msg_is_valid_access,
.convert_ctx_access = sk_msg_convert_ctx_access,
.gen_prologue = bpf_noop_prologue,
};
sk_msg_func_proto函数会取得此结构:
case BPF_FUNC_msg_redirect_hash:
return &bpf_msg_redirect_hash_proto;
net/core/sock_map.c: 定义结构:提供func_id对应的内核态函数,const struct bpf_func_proto bpf_msg_redirect_hash_prot,提供了bpf_msg_redirect_hash()函数,对应了用户态的调用函数。
3.eBPF代码load时,验证与执行
kernel/bpf/verifier.c,在eBPF load 代码做verifier时(bpf_check),调用fixup_bpf_calls函数,把insn->imm:func_id对应的struct bpf_func_proto找到,然后计算偏移,保存到insn->imm:
fn = env->ops->get_func_proto(insn->imm, env->prog);
insn->imm = fn->func - __bpf_call_base;
内核态执行eBPF内核态代码时,调用___bpf_prog_run,会找到对应的func去执行:
BPF_R0 = (__bpf_call_base + insn->imm)(BPF_R1, BPF_R2, BPF_R3, BPF_R4, BPF_R5);
本文分享了eBPF工具安装方式,并且通过不同的例子,分析了eBPF用户态工具核心文件的构成,以及内核对于eBPF用户态工具支持的原理。更多linux内核应用,欢迎留言与我们交流。
扫码添加 “鹅厂架构师小客服” ,加入【鹅厂架构师圈】,与技术爱好者、技术关注者分享交流,共同进步成长,欢迎大家!↓↓↓
关于我们
技术分享:关注微信公众号 【鹅厂架构师】