适用于生产环境下的BPF应用的通用基础模板
bootstrap 是一个简单但适用于生产环境下的 BPF 应用。它依赖 BPF CO-RE,同时要求内核编译时配置 CONFIG_DEBUG_INFO_BTF=y。
它会跟踪进程的启动和退出事件,并输出相关数据,包括文件名、进程 ID(PID)、父进程 ID(PPID),以及进程的退出状态和生命周期。
它展示了几个典型的 BPF 功能的使用方式:
- 协调工作的 BPF 程序
- 用于维护状态的 BPF map
- 用于向用户空间发送数据的 BPF ring buffer
- 用于实现应用程序行为参数化的全局变量
- 利用 BPF CO-RE 和 vmlinux.h 头文件,从内核的
task_struct结构体中读取额外的进程信息
运行
- 显示存活时间至少 50ms 的进程
1 | $ ./bootstrap -d 50 |
- 开启 libbpf 的 debug 日志
1 | $ ./bootstrap -v |
The BPF side
头文件
1 |
vmlinux.h 头文件中包含了 BPF 程序可能需要的、与某个内核相关的所有数据结构信息。它是在 libbpf-bootstrap 项目里预先生成的,开发者也可以自动使用 bpftool 生成,具体可参考 gen_vmlinux_h.sh。
只使用 libbpf 提供的 vmlinux.h 头文件就可以,如果同时包含其它内核头文件,会出现重复定义的问题。
Read-only global variable
1 | const volatile unsigned long long min_duration_ns = 0; |
const volatile 表示该变量不论是在 BPF side,还是在 user-space side,都是只读的。如果不使用 volatile,Clang 就可能假设该变量的值为 0,并将其彻底移除。
BPF maps
BPF_MAP_TYPE_HASH
1 | struct { |
定义一个名为 exec_start,类型为 BPF_MAP_TYPE_HASH 的哈希表。最大容纳 8192 个元素,key 是 pid_t 类型,value 是 u64 类型,用于存储进程运行事件的纳秒粒度的时间戳。
SEC(“tp/sched/sched_process_exec”)
1 | SEC("tp/sched/sched_process_exec") |
bpf_map_update_elem
1 | pid_t pid; |
在哈希表中添加,更新元素。
- BPF_ANY: 添加一个新的键(key),或者更新已有的键值对
- BPF_NOEXIST:仅当键(key)不存在时才会执行更新操作,以防止覆盖已有的数据。
- BPF_EXIST:仅当键(key)已存在时才会执行更新操作,以确保仅对已有键进行更新,而不创建新的键
SEC(“tp/sched/sched_process_exit”)
1 | SEC("tp/sched/sched_process_exit") |
bpf_map_lookup_elem
在另一个 BPF 程序 (handle_exit) 中,从同一个 BPF map 中查询元素,然后删除它。
1 | start_ts = bpf_map_lookup_elem(&exec_start, &pid); |
BPF ring buffer
BPF_MAP_TYPE_RINGBUF
1 | struct { |
定义一个名为 rb,类型为 BPF_MAP_TYPE_RINGBUF 的环形缓冲区。
bpf_ringbuf_reserve
1 | /* reserve sample from BPF ringbuf */ |
bpf_ringbuf_reserve 用于在环形缓冲区申请连续的内存块,第一个参数为指向环形缓冲区的指针,第二个参数为要在环形缓冲区中预留的字节数,第三个参数必须为 0。
BPF_CORE_READ
1 | /* fill out the sample with data */ |
BPF_CORE_READ 用于简化 BPF CO-RE 可重定位读取操作,尤其适用于指针追踪步骤较少的场景。BPF_CORE_READ(task, real_parent, tgid) 相当于 task->real_parent->tgid。
ctx->__data_loc_filename
1 | fname_off = ctx->__data_loc_filename & 0xFFFF; |
libbpf 访问 tracepoint 上下文字段的格式: __data_loc_<some_field>。(低 16 位偏移 + 高 16 位长度)
& 0xFFFF 用于提取低 16 位的偏移量。
参考:
每个跟踪点(tracepoint)都有一个格式(format),该格式用于描述从该跟踪点中会被跟踪输出的字段:
1 | $ cat /sys/kernel/tracing/events/sched/sched_process_exec/format |
bpf_ringbuf_submit
1 | /* successfully submit it to user-space for post-processing */ |
bpf_ringbuf_submit 会使环形缓冲区中预留的数据变为可读取状态。
- 0:发送自适应的新数据可用通知
- BPF_RB_NO_WAKEUP:不发送新数据可用通知
- BPF_RB_FORCE_WAKEUP:无条件发送新数据可用通知
The user-space side
open & load
1 | /* Load and verify BPF application */ |
只读全局变量需要在 BPF skeleton 加载到内核之前完成设置,因此需要分 __open 和 __load 两步进行。当 BPF skeleton 完成 __load 之后,不论是在 BPF side,还是在 user-space side 都只能读该变量。
attach
1 | /* Attach tracepoints */ |
ring_buffer__new
1 | struct ring_buffer *rb = NULL; |
环形缓冲区(ring buffer) 指的是一种循环缓冲区,其中 eBPF 程序作为生产者,用户空间(userspace)作为消费者。尽管返回值的数据类型是 struct ring_buffer *,但它实际上是用于管理多个环形缓冲区的管理器。
ring_buffer__poll
1 | /* Process events */ |
轮询属于环形缓冲区管理器(ring buffer manager)的任一环形缓冲区(ring buffer)上的可用数据。
- 若存在可用数据,已注册的回调函数(callback functions)将被调用;
- 若不存在可用数据,该函数会等待
timeout_ms(毫秒)以等待数据到达,并在此期间处于阻塞(block)状态。
clean_up
1 | cleanup: |