bpf学习笔记
1. 基于libbpf开发bpf程序的工作流
在libbpf中的应用程序中,往往有两个部分是必要的:
app.bpf.c.对应的是内核空间的代码逻辑.其中一般有定义的断点,探针及其执行的handle,可能还有“.map”等内核数据结构.这一部分的代码是面向内核的,因为我们知道bpf技术一种可以向内核注册一系列的事件,并且对其事件定义相应的回调代码的技术,这种技术无需对内核代码进行修改或者重新编译.
app.c,对应的是用户空间的代码逻辑.将bpf代码打开并加载,并和内核事件进行交互.
其实用户空间对应的程序完全可以用C++写.
关于其编译到执行的工作流:
file(GLOB apps *.bpf.c)
foreach(app ${apps})
get_filename_component(app_stem ${app} NAME_WE)
# Build object skeleton and depend skeleton on libbpf build
bpf_object(${app_stem} ${app_stem}.bpf.c)
add_dependencies(${app_stem}_skel libbpf-build)
add_executable(${app_stem} ${app_stem}.cc)
target_link_libraries(${app_stem} ${app_stem}_skel Nanolog)
install(TARGETS ${app_stem} DESTINATION lib)
endforeach()
首先根据app.bpf.c生成一个app_skel的头文件,该文件将会被用户态程序中作为头文件.之后才生成最终的可执行的程序.
其中关于生成的app_skel文件,其中模式大体如下:
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* THIS FILE IS AUTOGENERATED! */
#ifndef __MINIMAL_BPF_SKEL_H__
#define __MINIMAL_BPF_SKEL_H_
#include <stdlib.h>
#include <bpf/libbpf.h>
struct minimal_bpf {
struct bpf_object_skeleton *skeleton;
struct bpf_object *obj;
struct {
struct bpf_map *bss;
} maps;
struct {
struct bpf_program *handle_tp;
} progs;
struct {
struct bpf_link *handle_tp;
} links;
struct minimal_bpf__bss {
int my_pid;
} *bss;
};
static inline void minimal_bpf__destroy(struct minimal_bpf *obj) { ... }
static inline struct minimal_bpf *minimal_bpf__open_opts(const struct bpf_object_open_opts *opts) { ... }
static inline struct minimal_bpf *minimal_bpf__open(void) { ... }
static inline int minimal_bpf__load(struct minimal_bpf *obj) { ... }
static inline struct minimal_bpf *minimal_bpf__open_and_load(void) { ... }
static inline int minimal_bpf__attach(struct minimal_bpf *obj) { ... }
static inline void minimal_bpf__detach(struct minimal_bpf *obj) { ... }
#endif /* __MINIMAL_BPF_SKEL_H__ */
其中用户态空间可以通过maps中的bss来访问.bpf.c程序中定义的“.map”数据结构,比如说ring
buffer,map等.关于progs
可以通过它对.bpf.c中的handle进行访问,links的作用类似于此.而对于minimal_bpf__bss
这一部分,则可以通过它在用户空间访问到它所定义的所有全局变量.
结合很多bpf demo的分析和上述的API,我们整理一下一个常规的bpf程序的模式:
struct minimal_bpf *skel;
int err;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Bump RLIMIT_MEMLOCK to allow BPF sub-system to do anything */
bump_memlock_rlimit();
/* Open BPF application */
skel = minimal_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open BPF skeleton\n");
return 1;
}
/* ensure BPF program only handles write() syscalls from our process */
skel->bss->my_pid = getpid(); // 在用户空间修改bpf程序中的全局变量
/* Load & verify BPF programs */
err = minimal_bpf__load(skel); // 将bpf程序载入并且通过内核的verify,返回值如果为0,就说明已经通过ok
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
/* Attach tracepoint handler */
err = minimal_bpf__attach(skel); // 将其中的handle加入
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
"to see output of the BPF programs.\n");
for (;;) {
/* trigger our BPF program */
fprintf(stderr, ".");
sleep(1);
}
cleanup:
minimal_bpf__destroy(skel); // 对skel进行清理
return -err;
简而言之:
libbpf_set_print
和bump_memlock_rlimit
.skel = *_bpf__open()
,返回一个对象,该对象对应了我们之前定义的bpf.*_bpf__load(skel)
,将skel对象载入到载入到内核中,并且还会涉及到内核的verify操作,如果不能通过verify将会返回err(非0).*_bpf__attach(skel)
,将其中的事件(tracepoint,probe等)及其handle载入到内核.- while循环.
- clean代码段,用于程序结束或者出错情况下执行,一般有
*_bpf__destroy, ring_buffer__free
等API.
2. libbpf中的常用API
libbpf_set_print(libbpf_output)
,设置libbpf
中的日志输出函数,libbpf
执行过程中的日志会通过 libbpf_output
函数进行输出.bpf_get_current_pid_tgid
该函数通常在bpf中的内核态代码中执行,该程序往往用来获取触发某种事件的进程的pid.比如说,如下的方式:
int pid = bpf_get_current_pid_tgid() >> 32;
bpf_ktime_get_ns
该API用来获取系统中的时间戳,其单位为ns.
bpf_get_current_task
获取触发该事件的进程其对应的task
,一种内核中表示进程的数据结构.其中示例代码如下:
struct task_struct *task;
task = (struct task_struct *) bpf_get_current_task();
BPF_CORE_READ
,这个API和下面所要说的bpf_probe_read_str
的API都涉及到内核数据字段的拷贝.比如说从内核中读取某个字段的值.比如说bpf_core_read(dst, sz, src)
.其中有代码如下:
struct task_struct *task = (void *)bpf_get_current_task();
struct task_struct *parent_task;
int err = bpf_core_read(&parent_task, sizeof(void *), &task->parent);
if (err) {
/* handle error */
}
bpf_probe_read_str
,用于拷贝字符串,将内容拷贝到指定的缓冲区中.
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);
bpf_get_current_comm
,用来获取当前该进程的进程名字,其中的参数是一个缓冲区,及其size.比如说如下所示:
bpf_get_current_comm(&e->comm, sizeof(e->comm));
bpf_printk
3. 其他要点
1) BPF maps
这个东西的作用十分类似于STL,是一种可以在内核中使用的抽象数据容器.有BPF_MAP_TYPE_HASH
,BPF_MAP_TYPE_RINGBUF
等.下面是关于BPF_MAP_TYPE_HASH
.这个东西和C++中的map颇为相似.其中实例如下:
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, pid_t);
__type(value, u64);
} exec_start SEC(".maps");
其中分别对应的了.maps的类型,最大条目数,key,value,其中关于max_entries的设置,一般必须得是内核页size的大小,知道了基本的定义方式之后,我们需要搞明白的就是对于操作这么一个数据结构都需要哪些API.其中主要有bpf_map_update_elem,bpf_map_lookup_elem,bpf_map_delete_elem
.其中详细的使用情况如下:
bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY); // BPF_ANY表示的更新现有这对map,或者增加新的map.
bpf_map_delete_elem(&exec_start, &pid); // 移除exec_start中pid对应的map
start_ts = bpf_map_lookup_elem(&exec_start, &pid); // 其返回值是一个value
2) BPF Ring Buffer
buffer这种数据结构的使用时常被用于做数据传递,尤其是一些“生产者-消费者”的场景,Ring
buffer就是这么一种数据结构,可以用来给内核空间和用户空间做数据交换.其中相比之前perf buffer
来说,有更高的效率使用内存,并且可以保证事件顺序.
其中对其定义一半如下:
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps"); // 这个buf往往是作为内核态到用户态的桥梁的.
其中关于ring buffer
,最常用的API是bpf_ringbuf_reserve()/pf_ringbuf_commit()
.其中使用情况如下:
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
bpf_ringbuf_submit(e, 0);
通过reserve
的好处在于可以通过返回结果判断是否还有足够的空间加入数据.一旦返回是一个非NULL,就表示这个数据成功地在buffer中分配了空间,后续的commit也保证了一定会成功.当执行commit的时候,用户空间可以看到传递过来的数据,这个放置于ring buffer
中的数据往往是用户自己定义的,比如说像下面这样子.
struct event {
int pid;
int ppid;
unsigned exit_code;
unsigned long long duration_ns;
char comm[TASK_COMM_LEN]; // 进程的name
char filename[MAX_FILENAME_LEN];
bool exit_event;
};
如此之外,还有一个ring_buffer__new
.这表示的是对于一个ring buffer
的初始化操作,其中的第一个参数对应的是我们在内核空间中定义的ring buffer
,第二参数对应了commit
事件的回调函数,这个函数往往是我们在用户态空间定义的,在用户态运行的.其返回值是ring buffer
的指针.
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
此外关于ring_buffer__poll
这一个函数,这个函数时常被用在用户态程序的主循环之中的.比如说:
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
if (err == -EINTR) {
err = 0;
break;
}
if (err < 0) {
printf("Error polling perf buffer: %d\n", err);
break;
}
}