我是 LEE,老李,一个在 IT 行业摸爬滚打 16 年的技术老兵。
事件背景
最近一直想写一个关于 ebpf 的文章,但是不知道从哪里开始写起。思考良久之后,决定站在一个研发的角度,求真务实的从入门的角度来介绍下 ebpf。 实际我真正的目的是能够用好的 cilium 的整个系统,但是站在 cilium 角度上看 ebpf,它确实一个黑盒子。emmm.... 这个不符合我的人设,决定今天整理一个文章往里尝试看看究竟。
废话不多说,我们从一个非常简单的 helloworld 的编写出发。
前置知识
这个作为 epbf 学习的第一章知识,我相信很多小伙伴跟我一样都是“白手起家,一穷二白”,害怕 ebpf 整个环境的复杂性。既然是前置知识,那一定是最简单的,能够让我们一下就懂的。
ebpf 是一个转发层的驱动模型,既然是模型,就一定有定义和抽象等概念,我们通过下图就可以有了第一印象。
eBPF 分为用户空间程序和内核程序两部分:
- 用户空间程序负责加载 BPF 字节码至内核,如需要也会负责读取内核回传的统计信息或者事件详情
- 内核中的 BPF 字节码负责在内核中执行特定事件,如需要也会将执行的结果通过 maps 或者 perf-event 事件发送至用户空间
- 其中用户空间程序与内核 BPF 字节码程序可以使用 map 结构实现双向通信,这为内核中运行的 BPF 字节码程序提供了更加灵活的控制
上面的内容用大白话说:ebpf 包含用户层和内核层两层组成。用户层主要是负责业务逻辑处理和响应,同时也兼顾着内核中的 epbf 的逻辑 bytescode 生成(当然这里可以使用第三方生成),并将 bytescode 注入到内核中。内核层主要是接受 bytescode,然后在内核层内完成对 bytescode 执行。果真是妥妥的控制与转发分离的模型,要不然为什么 ebpf 这么高效呢?
上手开发
环境准备
我们使用 Ubuntu 20.04 这个系统作为整体开发环境,内核使用的是 5.4.0。
检查内核
root@ubuntu:/tmp# uname -a
Linux ubuntu 5.4.0-126-generic #142-Ubuntu SMP Fri Aug 26 12:12:57 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux
安装编译工具和包
# apt install -y bison build-essential cmake flex git libedit-dev pkg-config libmnl-dev \
python zlib1g-dev libssl-dev libelf-dev libcap-dev libfl-dev llvm clang pkg-config \
gcc-multilib luajit libluajit-5.1-dev libncurses5-dev libclang-dev clang-tools
安装内核源码
# apt install linux-source-5.4.0
源码安装至 /usr/src/ 目录下, 使用下面的命里初始化编译环境。
# cd /usr/src/linux-source-5.4.0/
#
# tar xvf linux-source-5.4.0.tar.bz2
# cd linux-source-5.4.0
#
# cp -v /boot/config-$(uname -r) .config
# make headers_install && make modules_prepare
开发代码
ebpf 是使用内核层和用户两层,那么我们写代码也要实现两层代码。
内核层代码:hello_kern.c
#include <linux/bpf.h>
#include "bpf/bpf_helpers.h"
#define SEC(NAME) __attribute__((section(NAME), used))
SEC("tracepoint/syscalls/sys_enter_execve")
int bpf_prog(void *ctx)
{
char msg[] = "Hello Shengyan.li, I'm in eBPF World\n";
bpf_trace_printk(msg, sizeof(msg)); /* 把信息打印到 trace 队列中 */
return 0;
}
char _license[] SEC("license") = "GPL"; /* 这里很重要,告诉 ebpf 当前的模块协议是 GPL,如果不是GPL,libpbf 报错 err = 22, 退出。*/
用户层代码:hello_user.c
#include <stdio.h>
#include "bpf_load.h"
int main(int argc, char **argv)
{
if(load_bpf_file("hello_kern.o") != 0) /* 加载生成的 bytescode 到内核*/
{
printf("The kernel didn't load BPF program\n");
return -1;
}
read_trace_pipe(); /* 从 trace 队列中读取信息,输出到 stdout */
return 0;
}
编译测试
修改 Makefile
在 /usr/src/linux-source-5.4.0/linux-source-5.4.0/samples/bpf 目录下,修改 Makefile,然后在对应的位置添加对应内容。
hostprogs-y += helloworld
hello-objs := bpf_load.o hello_user.o
always += hello_kern.o
编译
# cd /usr/src/linux-source-5.4.0/linux-source-5.4.0
# make M=samples/bpf
测试
# cd /usr/src/linux-source-5.4.0/linux-source-5.4.0/samples/bpf
# ./helloworld
当你执行上面的 helloworld 命令后,发现没有反应,不要慌,这个是正常的。这个时候你只需要在打开一个新会话连接到编译环境的机器上,然后在这个新窗口执行如下命令:
# watch "ls -l"
之前执行 helloworld 的命令的窗口会有如下的输出:
总结
我们通过上面的操作和使用,基本确认了 epbf 整体开发流程。 通过简单的开发,我们基本可以得出下面的几个结论:
- ebpf 开发不需要重新编译内核和重启服务器,能够非常快速的迭代开发。
- 和 DPDK 的逻辑很像,之前很多积累的知识这里可以无脑复用。
- 有很多开发框架和库都在使用 ebpf,代码风险可控。
- ebpf 开发模型没有那么复杂,还是很简单,且很有意思的。