RISC-V eBPF 系列 2:bpf SYSCALL
前言
上一篇文章中介绍了 eBPF 技术的背景,本文主要围绕 bpf SYSCALL 进行介绍,包括它的使用以及具体的源码实现。
bpf SYSCALL
bpf SYSCALL 是操作系统内核给用户程序提供的用于 eBPF 的编程接口。用户空间所有的 eBPF 相关函数,归根结底都是对于 bpf SYSCALL 的包装。所以直接学习它有助于我们更好理解其使用和原理。
总体来说,bpf SYSCALL 的功能主要包含两部分:
- 对 eBPF maps 数据的增删改查
- 对 eBPF 程序进行验证和加载
bpf 函数原型
#include <linux/bpf.h>
int bpf(int cmd, union bpf_attr *attr, unsigned int size);
bpf 函数包含 3 个参数:
- cmd:本次系统调用要执行的动作,包括对 map 的增删改查以及程序的加载
- attr:一个大的联合结构体,包含了执行各种操作所需要的数据
- size:attr 结构的大小
eBPF maps
map 是一种可以用来存储不同类型数据的通用数据结构,可以使用它在不同 eBPF 程序之间以及和用户程序之间共享数据。
types
Hash-table map
- 查询性能好
- 更新是原子操作
Array map
- 最快的查询性能
- 内存是预分配的
- key 必须是 4 字节
- 无法删除元素
- 更新操作不是原子操作
Program array map
- value 是指向另一个 eBPF program 的文件描述符
- key 和 value 都是 4 字节
map create
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));
}
创建 map 时,cmd 参数设置为 BPF_MAP_CREATE。然后需要指定 map 类型(map_type),键的大小(key_size),值的大小(value_size)以及元素容量(max_entries),这些统一放入 attr 参数里即可。最后返回一个文件描述符。
map lookup
int
bpf_lookup_elem(int fd, const void *key, void *value)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
};
return bpf(BPF_MAP_LOOKUP_ELEM, &attr, sizeof(attr));
}
需要在 map 中查找元素时,cmd 参数设置为 BPF_MAP_LOOKUP_ELEM。然后指定 map 对应的文件描述符,键的地址,查询值保存的地址,一起放入 attr 参数里即可。
map update
int
bpf_update_elem(int fd, const void *key, const void *value,
uint64_t flags)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
.value = ptr_to_u64(value),
.flags = flags,
};
return bpf(BPF_MAP_UPDATE_ELEM, &attr, sizeof(attr));
}
需要更新 map 中元素时,cmd 参数设置为 BPF_MAP_UPDATE_ELEM。然后指定 map 对应的文件描述符,键的地址,值的地址,以及更新标记一起放入 attr 参数里即可。
更新标记(flags)有以下三种选项:
- BPF_ANY:创建新元素或者更新已存在的
- BPF_NOEXIST:只在不存在时创建新元素
- BPF_EXIST:只在存在时更新元素
map delete
int
bpf_delete_elem(int fd, const void *key)
{
union bpf_attr attr = {
.map_fd = fd,
.key = ptr_to_u64(key),
};
return bpf(BPF_MAP_DELETE_ELEM, &attr, sizeof(attr));
}
需要删除 map 中元素时,cmd 参数设置为 BPF_MAP_DELETE_ELEM。然后指定 map 对应的文件描述符和键的地址,一起放入 attr 参数里即可。
eBPF programs
eBPF programs 的相关操作包括:加载(load),附加(attach),链接(link)以及固定(pin)。
load
通过加载(load)过程,将指令注入内核。程序通过验证器会进行许多检查并可能重写一些指令(特别是对于 map 的访问)。如果启用了 JIT 编译,则程序可能是 JIT 进行重新编译的。
char bpf_log_buf[LOG_BUF_SIZE];
int
bpf_prog_load(enum bpf_prog_type type,
const struct bpf_insn *insns, int insn_cnt,
const char *license)
{
union bpf_attr attr = {
.prog_type = type,
.insns = ptr_to_u64(insns),
.insn_cnt = insn_cnt,
.license = ptr_to_u64(license),
.log_buf = ptr_to_u64(bpf_log_buf),
.log_size = LOG_BUF_SIZE,
.log_level = 1,
};
return bpf(BPF_PROG_LOAD, &attr, sizeof(attr));
}
需要载入一段 eBPF 程序到内核时,cmd 参数设置为 BPF_PROG_LOAD。attr 中需要指定的参数包括:
- eBPF 程序类型(prog_type)
- 程序指令数组(insns)
- 指令数量(insn_cnt)
- 授权许可证(license)
- 日志缓冲区(log_buf)
- 日志缓冲区大小(log_size)
- 日志级别(log_level)
program types
目前支持的 program types 包括:
- BPF_PROG_TYPE_SOCKET_FILTER
- BPF_PROG_TYPE_KPROBE
- BPF_PROG_TYPE_SCHED_CLS
- BPF_PROG_TYPE_SCHED_ACT
- BPF_PROG_TYPE_TRACEPOINT
- BPF_PROG_TYPE_XDP
- BPF_PROG_TYPE_PERF_EVENT
- BPF_PROG_TYPE_CGROUP_SKB
- BPF_PROG_TYPE_CGROUP_SOCK
- BPF_PROG_TYPE_LWT_IN
- BPF_PROG_TYPE_LWT_OUT
- BPF_PROG_TYPE_LWT_XMIT
- BPF_PROG_TYPE_SOCK_OPS
- BPF_PROG_TYPE_SK_SKB
- BPF_PROG_TYPE_CGROUP_DEVICE
- BPF_PROG_TYPE_SK_MSG
- BPF_PROG_TYPE_RAW_TRACEPOINT
- BPF_PROG_TYPE_CGROUP_SOCK_ADDR
- BPF_PROG_TYPE_LWT_SEG6LOCAL
- BPF_PROG_TYPE_LIRC_MODE2
- BPF_PROG_TYPE_SK_REUSEPORT
- BPF_PROG_TYPE_FLOW_DISSECTOR
- BPF_PROG_TYPE_CGROUP_SYSCTL
- BPF_PROG_TYPE_RAW_TRACEPOINT_WRITABLE
- BPF_PROG_TYPE_CGROUP_SOCKOPT
不同的 eBPF program type 主要区别在于:
- 能使用的内核帮助函数(kernel helper functions)集合
- 程序传入的上下文(context)格式
attach
eBPF programs 载入内核后可以附加(attach)到不同的事件(event)进行触发,包括网络包(network packets),追踪事件(tracing events)等。
attach 具体的方法取决于 program 或者 event 的类型。例如可以使用 setsockopt 将 program 附加到网络包事件上。
setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_BPF, &prog_fd, sizeof(prog_fd));
而对于 cgroup 相关的类型可以使用 bpf(BPF_PROG_ATTACH,...)
系统调用进行。
如果所有类型都由 bpf 系统调用统一进行 attach 会比较一致。但 BPF_PROG_ATTACH
是内核新增的特性,因为历史遗留原因,导致了不同类型的 attach 方法不一致,希望在后续内核版本中可以进行统一。
link
当加载 BPF 的程序关闭时,由于 BPF 程序引用归零,就会被内核卸载。那么如何在程序关闭时保持 BPF 程序的运行呢?此时可以就使用链接(link)。
BPF 程序可以 attach 到一个链接,而不是传统的钩子。链接本身 attach 到内核的钩子。这样有两个好处:
- 可以固定这种链接,在加载程序退出时保持 BPF 继续运行。
- 更容易跟踪程序中持有的引用,以确保加载程序意外退出时没有 BPF 程序被加载。
可以使用 bpf(BPF_LINK_CREATE,...)
系统调用来创建链接。
pin
pin 是一种保持 BPF 对象 (程序,映射,链接) 引用的方法。通过 bpf(BPF_OBJ_PIN, ...)
这个系统调用完成,这会在 eBPF 的虚拟文件系统中创建一个路径,并且之后可以用 open()
该路径来获取该 BPF 对象的文件描述符。只要一个对象被固定住,他就会一直保留在内核中,不需要 pin 或者 map 就可运行它。只要存在其他引用 (文件描述符。附加到一些钩子,或者被其他程序引用) 程序就会一直加载在内核中,attach 之后可以直接运行。
bpf 源码分析
bpf SYSCALL 在内核中对应的函数为 __sys_bpf
:
// kernel/bpf/syscall.c:4595
static int __sys_bpf(int cmd, bpfptr_t uattr, unsigned int size)
{
...
err = bpf_check_uarg_tail_zero(uattr, sizeof(attr), size); // 检查 uattr 参数是否过大等
...
if (copy_from_bpfptr(&attr, uattr, size) != 0) // attr 从用户态拷贝到内核态
...
switch (cmd) { // 根据 cmd 类型调用对于的处理函数
...
case BPF_PROG_LOAD:
err = bpf_prog_load(&attr, uattr); // 加载 bpf 程序
break;
...
}
}
__sys_bpf
函数内主要包含两部分逻辑:
- 权限和参数校验:包括 bpf 功能是否打开,attr 参数是否过大等
- 针对不同的 cmd 调用对应的处理函数:目前 cmd 支持的类型比较多,后面主要介绍加载 eBPF 程序
bpf_prog_load
当执行加载 eBPF 程序功能时会调用 bpf_prog_load
方法。下面介绍了该方法的主要内容。
// kernel/bpf/syscall.c:2207
static int bpf_prog_load(union bpf_attr *attr, bpfptr_t uattr)
{
...
// 开源许可证判断
is_gpl = license_is_gpl_compatible(license);
// 限制 eBPF 程序指令数量
if (attr->insn_cnt == 0 ||
attr->insn_cnt > (bpf_capable() ? BPF_COMPLEXITY_LIMIT_INSNS : BPF_MAXINSNS))
return -E2BIG;
// 如果是 net 相关类型,判断所需权限是否满足
if (is_net_admin_prog_type(type) && !capable(CAP_NET_ADMIN) && !capable(CAP_SYS_ADMIN))
return -EPERM;
// 如果是追踪相关类型,判断权限是否满足
if (is_perfmon_prog_type(type) && !perfmon_capable())
return -EPERM;
...
// 给 struct bpf_prog 申请内存,该结构是 bpf 在内核中的实例
prog = bpf_prog_alloc(bpf_prog_size(attr->insn_cnt), GFP_USER);
...
// 拷贝 bpf 字节码到内核
if (copy_from_bpfptr(prog->insns,
make_bpfptr(attr->insns, uattr.is_kernel),
bpf_prog_insn_size(prog)) != 0)
...
// bpf verify 机制核心
err = bpf_check(&prog, attr, uattr);
// bpf jit 机制核心,将 bpf 字节码编译为目标平台汇编代码
prog = bpf_prog_select_runtime(prog, &err);
...
// 打印一条 prog load 的 audit 信息
bpf_audit_prog(prog, BPF_AUDIT_LOAD);
//返回给应用层 bpf prog 的 fd 信息,后续应用层用该 fd 进行操作
err = bpf_prog_new_fd(prog);
}
函数里的 bpf_check
和 bpf_prog_select_runtime
分别代表了 bpf 的 verify 和 JIT 两个核心机制,准备在后面的文章继续进行分析。
总结
本文主要介绍了 bpf 技术中涉及的 map 和 program 两个对象,并示例如何用 bpf SYSCALL 进行操作。之后还分析了加载 program 部分的源码,其中更细节的 verify 和 JIT 部分会在后续的文章中进行分析。