update exitsnoop and bootstrap

This commit is contained in:
yunwei37
2023-05-07 03:38:43 +08:00
parent 52895346d5
commit 57e0164e90
6 changed files with 777 additions and 323 deletions

View File

@@ -166,72 +166,72 @@ int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action)
char LICENSE[] SEC("license") = "GPL"; char LICENSE[] SEC("license") = "GPL";
``` ```
这段代码是一个 eBPF 程序用于捕获和分析内核中硬件中断处理程序hardirqs的执行信息。程序的主要目的是获取中断处理程序的名称、执行次数和执行时间并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。 这段代码是一个 eBPF 程序用于捕获和分析内核中硬件中断处理程序hardirqs的执行信息。程序的主要目的是获取中断处理程序的名称、执行次数和执行时间并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。
1. 包含必要的头文件和定义数据结构: 1. 包含必要的头文件和定义数据结构:
```c ```c
#include <vmlinux.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "hardirqs.h"
#include "bits.bpf.h"
#include "maps.bpf.h"
```
#include <vmlinux.h> 该程序包含了 eBPF 开发所需的标准头文件,以及用于定义数据结构和映射的自定义头文件。
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include "hardirqs.h"
#include "bits.bpf.h"
#include "maps.bpf.h"
```
该程序包含了 eBPF 开发所需的标准头文件,以及用于定义数据结构和映射的自定义头文件。
2. 定义全局变量和映射: 2. 定义全局变量和映射:
```c ```c
#define MAX_ENTRIES 256 #define MAX_ENTRIES 256
const volatile bool filter_cg = false; const volatile bool filter_cg = false;
const volatile bool targ_dist = false; const volatile bool targ_dist = false;
const volatile bool targ_ns = false; const volatile bool targ_ns = false;
const volatile bool do_count = false; const volatile bool do_count = false;
... ...
``` ```
该程序定义了一些全局变量,用于配置程序的行为。例如,`filter_cg` 控制是否过滤 cgroup`targ_dist` 控制是否显示执行时间的分布等。此外,程序还定义了三个映射,分别用于存储 cgroup 信息、开始时间戳和中断处理程序的信息。 该程序定义了一些全局变量,用于配置程序的行为。例如,`filter_cg` 控制是否过滤 cgroup`targ_dist` 控制是否显示执行时间的分布等。此外,程序还定义了三个映射,分别用于存储 cgroup 信息、开始时间戳和中断处理程序的信息。
3. 定义两个辅助函数 `handle_entry` 和 `handle_exit` 3. 定义两个辅助函数 `handle_entry` 和 `handle_exit`
这两个函数分别在中断处理程序的入口和出口处被调用。`handle_entry` 记录开始时间戳或更新中断计数,`handle_exit` 计算中断处理程序的执行时间,并将结果存储到相应的信息映射中。 这两个函数分别在中断处理程序的入口和出口处被调用。`handle_entry` 记录开始时间戳或更新中断计数,`handle_exit` 计算中断处理程序的执行时间,并将结果存储到相应的信息映射中。
4. 定义 eBPF 程序的入口点: 4. 定义 eBPF 程序的入口点:
```c ```c
SEC("tp_btf/irq_handler_entry") SEC("tp_btf/irq_handler_entry")
int BPF_PROG(irq_handler_entry_btf, int irq, struct irqaction *action) int BPF_PROG(irq_handler_entry_btf, int irq, struct irqaction *action)
{ {
return handle_entry(irq, action); return handle_entry(irq, action);
} }
SEC("tp_btf/irq_handler_exit") SEC("tp_btf/irq_handler_exit")
int BPF_PROG(irq_handler_exit_btf, int irq, struct irqaction *action) int BPF_PROG(irq_handler_exit_btf, int irq, struct irqaction *action)
{ {
return handle_exit(irq, action); return handle_exit(irq, action);
} }
SEC("raw_tp/irq_handler_entry") SEC("raw_tp/irq_handler_entry")
int BPF_PROG(irq_handler_entry, int irq, struct irqaction *action) int BPF_PROG(irq_handler_entry, int irq, struct irqaction *action)
{ {
return handle_entry(irq, action); return handle_entry(irq, action);
} }
SEC("raw_tp/irq_handler_exit") SEC("raw_tp/irq_handler_exit")
int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action) int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action)
{ {
return handle_exit(irq, action); return handle_exit(irq, action);
} }
``` ```
这里定义了四个 eBPF 程序入口点,分别用于捕获中断处理程序的入口和出口事件。`tp_btf``raw_tp` 分别代表使用 BPF Type FormatBTF和原始 tracepoints 捕获事件。这样可以确保程序在不同内核版本上可以移植和运行。 这里定义了四个 eBPF 程序入口点,分别用于捕获中断处理程序的入口和出口事件。`tp_btf` 和 `raw_tp` 分别代表使用 BPF Type FormatBTF和原始 tracepoints 捕获事件。这样可以确保程序在不同内核版本上可以移植和运行。
Softirq 代码也类似,这里就不再赘述了。 Softirq 代码也类似,这里就不再赘述了。
@@ -259,4 +259,4 @@ sudo ecli run ./package.json
通过学习本章节内容,您应该已经掌握了如何在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件的方法,以及如何分析这些事件以识别内核中的性能问题和其他与中断处理相关的问题。这些技能对于分析和优化 Linux 内核的性能至关重要。 通过学习本章节内容,您应该已经掌握了如何在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件的方法,以及如何分析这些事件以识别内核中的性能问题和其他与中断处理相关的问题。这些技能对于分析和优化 Linux 内核的性能至关重要。
为了更好地理解和实践 eBPF 编程,我们建议您阅读 eunomia-bpf 的官方文档https://github.com/eunomia-bpf/eunomia-bpf。此外我们还为您提供了完整的教程和源代码您可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看和学习。希望本教程能够帮助您顺利入门 eBPF 开发,并为您的进一步学习和实践提供有益的参考。 为了更好地理解和实践 eBPF 编程,我们建议您阅读 eunomia-bpf 的官方文档:<https://github.com/eunomia-bpf/eunomia-bpf> 。此外,我们还为您提供了完整的教程和源代码,您可以在 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 中查看和学习。希望本教程能够帮助您顺利入门 eBPF 开发,并为您的进一步学习和实践提供有益的参考。

View File

@@ -5,3 +5,4 @@ package.json
*.skel.yaml *.skel.yaml
package.yaml package.yaml
ecli ecli
bootstrap

View File

@@ -1,17 +1,41 @@
# eBPF 入门开发实践教程十一:在 eBPF 中使用 bootstrap 开发用户态程序并跟踪 exec() 和 exit() 系统调用 # eBPF 入门开发实践教程十一:在 eBPF 中使用 libbpf 开发用户态程序并跟踪 exec() 和 exit() 系统调用
eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。 eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。
## 什么是bootstrap? 在本教程中,我们将了解内核态和用户态的 eBPF 程序是如何协同工作的。我们还将学习如何使用原生的 libbpf 开发用户态程序,将 eBPF 应用打包为可执行文件,实现跨内核版本分发。
## libbpf 库,以及为什么需要使用它
Bootstrap是一个工具它使用BPFBerkeley Packet Filter程序跟踪执行exec()系统调用使用SEC“tp/sched/sched_process_exec”handle_exit BPF程序这大致对应于新进程的生成忽略fork部分。此外它还跟踪exit使用SEC“tp/sched/sched_process_exit”handle_exit BPF程序以了解每个进程何时退出。这两个BPF程序共同工作允许捕获有关任何新进程的有趣信息例如二进制文件的文件名以及测量进程的生命周期并在进程死亡时收集有趣的统计信息例如退出代码或消耗的资源量等。我认为这是深入了解内核内部并观察事物如何真正运作的良好起点 libbpf 是一个 C 语言库,伴随内核版本分发,用于辅助 eBPF 程序的加载和运行。它提供了用于与 eBPF 系统交互的一组 C API使开发者能够更轻松地编写用户态程序来加载和管理 eBPF 程序。这些用户态程序通常用于分析、监控或优化系统性能
Bootstrap还使用argp APIlibc的一部分进行命令行参数解析。 使用 libbpf 库有以下优势:
- 它简化了 eBPF 程序的加载、更新和运行过程。
- 它提供了一组易于使用的 API使开发者能够专注于编写核心逻辑而不是处理底层细节。
- 它能够确保与内核中的 eBPF 子系统的兼容性,降低了维护成本。
同时libbpf 和 BTFBPF Type Format都是 eBPF 生态系统的重要组成部分。它们各自在实现跨内核版本兼容方面发挥着关键作用。BTFBPF Type Format是一种元数据格式用于描述 eBPF 程序中的类型信息。BTF 的主要目的是提供一种结构化的方式,以描述内核中的数据结构,以便 eBPF 程序可以更轻松地访问和操作它们。
BTF 在实现跨内核版本兼容方面的关键作用如下:
- BTF 允许 eBPF 程序访问内核数据结构的详细类型信息,而无需对特定内核版本进行硬编码。这使得 eBPF 程序可以适应不同版本的内核,从而实现跨内核版本兼容。
- 通过使用 BPF CO-RECompile Once, Run Everywhere技术eBPF 程序可以利用 BTF 在编译时解析内核数据结构的类型信息,进而生成可以在不同内核版本上运行的 eBPF 程序。
结合 libbpf 和 BTFeBPF 程序可以在各种不同版本的内核上运行,而无需为每个内核版本单独编译。这极大地提高了 eBPF 生态系统的可移植性和兼容性,降低了开发和维护的难度。
## 什么是 bootstrap
Bootstrap 是一个使用 libbpf 的完整应用,它利用 eBPF 程序来跟踪内核中的 exec() 系统调用(通过 SEC("tp/sched/sched_process_exec") handle_exec BPF 程序),这主要对应于新进程的创建(不包括 fork() 部分)。此外,它还跟踪进程的 exit() 系统调用(通过 SEC("tp/sched/sched_process_exit") handle_exit BPF 程序),以了解每个进程何时退出。
这两个 BPF 程序共同工作,允许捕获关于新进程的有趣信息,例如二进制文件的文件名,以及测量进程的生命周期,并在进程结束时收集有趣的统计信息,例如退出代码或消耗的资源量等。这是深入了解内核内部并观察事物如何真正运作的良好起点。
Bootstrap 还使用 argp APIlibc 的一部分)进行命令行参数解析,使得用户可以通过命令行选项配置应用行为。这种方式提供了灵活性,让用户能够根据实际需求自定义程序行为。虽然这些功能使用 eunomia-bpf 工具也可以实现,但是这里我们使用 libbpf 可以在用户态提供更高的可扩展性,不过也带来了不少额外的复杂度。
## Bootstrap ## Bootstrap
TODO: 添加关于用户态的应用部分,以及关于 libbpf-boostrap 的完整介绍。也许可以参考类似http://cn-sec.com/archives/1267522.html 的文档 Bootstrap 分为两个部分:内核态和用户态。内核态部分是一个 eBPF 程序,它跟踪 exec() 和 exit() 系统调用。用户态部分是一个 C 语言程序,它使用 libbpf 库来加载和运行内核态程序,并处理从内核态程序收集的数据
### 内核态 eBPF 程序 bootstrap.bpf.c
```c ```c
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause // SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
@@ -25,15 +49,15 @@ TODO: 添加关于用户态的应用部分,以及关于 libbpf-boostrap 的完
char LICENSE[] SEC("license") = "Dual BSD/GPL"; char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct { struct {
__uint(type, BPF_MAP_TYPE_HASH); __uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192); __uint(max_entries, 8192);
__type(key, pid_t); __type(key, pid_t);
__type(value, u64); __type(value, u64);
} exec_start SEC(".maps"); } exec_start SEC(".maps");
struct { struct {
__uint(type, BPF_MAP_TYPE_RINGBUF); __uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); __uint(max_entries, 256 * 1024);
} rb SEC(".maps"); } rb SEC(".maps");
const volatile unsigned long long min_duration_ns = 0; const volatile unsigned long long min_duration_ns = 0;
@@ -41,129 +65,536 @@ const volatile unsigned long long min_duration_ns = 0;
SEC("tp/sched/sched_process_exec") SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx) int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{ {
struct task_struct *task; struct task_struct *task;
unsigned fname_off; unsigned fname_off;
struct event *e; struct event *e;
pid_t pid; pid_t pid;
u64 ts; u64 ts;
/* remember time exec() was executed for this PID */ /* remember time exec() was executed for this PID */
pid = bpf_get_current_pid_tgid() >> 32; pid = bpf_get_current_pid_tgid() >> 32;
ts = bpf_ktime_get_ns(); ts = bpf_ktime_get_ns();
bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY); bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);
/* don't emit exec events when minimum duration is specified */ /* don't emit exec events when minimum duration is specified */
if (min_duration_ns) if (min_duration_ns)
return 0; return 0;
/* reserve sample from BPF ringbuf */ /* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e) if (!e)
return 0; return 0;
/* fill out the sample with data */ /* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task(); task = (struct task_struct *)bpf_get_current_task();
e->exit_event = false; e->exit_event = false;
e->pid = pid; e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid); e->ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&e->comm, sizeof(e->comm)); bpf_get_current_comm(&e->comm, sizeof(e->comm));
fname_off = ctx->__data_loc_filename & 0xFFFF; fname_off = ctx->__data_loc_filename & 0xFFFF;
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off); bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);
/* successfully submit it to user-space for post-processing */ /* successfully submit it to user-space for post-processing */
bpf_ringbuf_submit(e, 0); bpf_ringbuf_submit(e, 0);
return 0; return 0;
} }
SEC("tp/sched/sched_process_exit") SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx) int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{ {
struct task_struct *task; struct task_struct *task;
struct event *e; struct event *e;
pid_t pid, tid; pid_t pid, tid;
u64 id, ts, *start_ts, duration_ns = 0; u64 id, ts, *start_ts, duration_ns = 0;
/* get PID and TID of exiting thread/process */ /* get PID and TID of exiting thread/process */
id = bpf_get_current_pid_tgid(); id = bpf_get_current_pid_tgid();
pid = id >> 32; pid = id >> 32;
tid = (u32)id; tid = (u32)id;
/* ignore thread exits */ /* ignore thread exits */
if (pid != tid) if (pid != tid)
return 0; return 0;
/* if we recorded start of the process, calculate lifetime duration */ /* if we recorded start of the process, calculate lifetime duration */
start_ts = bpf_map_lookup_elem(&exec_start, &pid); start_ts = bpf_map_lookup_elem(&exec_start, &pid);
if (start_ts) if (start_ts)
duration_ns = bpf_ktime_get_ns() - *start_ts; duration_ns = bpf_ktime_get_ns() - *start_ts;
else if (min_duration_ns) else if (min_duration_ns)
return 0; return 0;
bpf_map_delete_elem(&exec_start, &pid); bpf_map_delete_elem(&exec_start, &pid);
/* if process didn't live long enough, return early */ /* if process didn't live long enough, return early */
if (min_duration_ns && duration_ns < min_duration_ns) if (min_duration_ns && duration_ns < min_duration_ns)
return 0; return 0;
/* reserve sample from BPF ringbuf */ /* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e) if (!e)
return 0; return 0;
/* fill out the sample with data */ /* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task(); task = (struct task_struct *)bpf_get_current_task();
e->exit_event = true; e->exit_event = true;
e->duration_ns = duration_ns; e->duration_ns = duration_ns;
e->pid = pid; e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid); e->ppid = BPF_CORE_READ(task, real_parent, tgid);
e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff; e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
bpf_get_current_comm(&e->comm, sizeof(e->comm)); bpf_get_current_comm(&e->comm, sizeof(e->comm));
/* send data to user-space for post-processing */ /* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0); bpf_ringbuf_submit(e, 0);
return 0; return 0;
}
```
这段代码是一个内核态 eBPF 程序bootstrap.bpf.c主要用于跟踪 exec() 和 exit() 系统调用。它通过 eBPF 程序捕获进程的创建和退出事件,并将相关信息发送到用户态程序进行处理。下面是对代码的详细解释。
首先,我们引入所需的头文件,定义 eBPF 程序的许可证以及两个 eBPF mapsexec_start 和 rb。exec_start 是一个哈希类型的 eBPF map用于存储进程开始执行时的时间戳。rb 是一个环形缓冲区类型的 eBPF map用于存储捕获的事件数据并将其发送到用户态程序。
```c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
#include "bootstrap.h"
char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 8192);
__type(key, pid_t);
__type(value, u64);
} exec_start SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024);
} rb SEC(".maps");
const volatile unsigned long long min_duration_ns = 0;
```
接下来,我们定义了一个名为 handle_exec 的 eBPF 程序,它会在进程执行 exec() 系统调用时触发。首先,我们从当前进程中获取 PID记录进程开始执行的时间戳然后将其存储在 exec_start map 中。
```c
SEC("tp/sched/sched_process_exec")
int handle_exec(struct trace_event_raw_sched_process_exec *ctx)
{
// ...
pid = bpf_get_current_pid_tgid() >> 32;
ts = bpf_ktime_get_ns();
bpf_map_update_elem(&exec_start, &pid, &ts, BPF_ANY);
// ...
}
```
然后,我们从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据,如进程 ID、父进程 ID、进程名等。之后我们将这些数据发送到用户态程序进行处理。
```c
// reserve sample from BPF ringbuf
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
// fill out the sample with data
task = (struct task_struct *)bpf_get_current_task();
e->exit_event = false;
e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
bpf_get_current_comm(&e->comm, sizeof(e->comm));
fname_off = ctx->__data_loc_filename & 0xFFFF;
bpf_probe_read_str(&e->filename, sizeof(e->filename), (void *)ctx + fname_off);
// successfully submit it to user-space for post-processing
bpf_ringbuf_submit(e, 0);
return 0;
```
最后,我们定义了一个名为 handle_exit 的 eBPF 程序,它会在进程执行 exit() 系统调用时触发。首先,我们从当前进程中获取 PID 和 TID线程 ID。如果 PID 和 TID 不相等,说明这是一个线程退出,我们将忽略此事件。
```c
SEC("tp/sched/sched_process_exit")
int handle_exit(struct trace_event_raw_sched_process_template* ctx)
{
// ...
id = bpf_get_current_pid_tgid();
pid = id >> 32;
tid = (u32)id;
/* ignore thread exits */
if (pid != tid)
return 0;
// ...
}
```
接着,我们查找之前存储在 exec_start map 中的进程开始执行的时间戳。如果找到了时间戳,我们将计算进程的生命周期(持续时间),然后从 exec_start map 中删除该记录。如果未找到时间戳且指定了最小持续时间,则直接返回。
```c
// if we recorded start of the process, calculate lifetime duration
start_ts = bpf_map_lookup_elem(&exec_start, &pid);
if (start_ts)
duration_ns = bpf_ktime_get_ns() - *start_ts;
else if (min_duration_ns)
return 0;
bpf_map_delete_elem(&exec_start, &pid);
// if process didn't live long enough, return early
if (min_duration_ns && duration_ns < min_duration_ns)
return 0;
```
然后,我们从环形缓冲区 map rb 中预留一个事件结构,并填充相关数据,如进程 ID、父进程 ID、进程名、进程持续时间等。最后我们将这些数据发送到用户态程序进行处理。
```c
/* reserve sample from BPF ringbuf */
e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
if (!e)
return 0;
/* fill out the sample with data */
task = (struct task_struct *)bpf_get_current_task();
e->exit_event = true;
e->duration_ns = duration_ns;
e->pid = pid;
e->ppid = BPF_CORE_READ(task, real_parent, tgid);
e->exit_code = (BPF_CORE_READ(task, exit_code) >> 8) & 0xff;
bpf_get_current_comm(&e->comm, sizeof(e->comm));
/* send data to user-space for post-processing */
bpf_ringbuf_submit(e, 0);
return 0;
}
```
这样,当进程执行 exec() 或 exit() 系统调用时,我们的 eBPF 程序会捕获相应的事件,并将详细信息发送到用户态程序进行后续处理。这使得我们可以轻松地监控进程的创建和退出,并获取有关进程的详细信息。
除此之外,在 bootstrap.h 中,我们还定义了和用户态交互的数据结构:
```c
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
/* Copyright (c) 2020 Facebook */
#ifndef __BOOTSTRAP_H
#define __BOOTSTRAP_H
#define TASK_COMM_LEN 16
#define MAX_FILENAME_LEN 127
struct event {
int pid;
int ppid;
unsigned exit_code;
unsigned long long duration_ns;
char comm[TASK_COMM_LEN];
char filename[MAX_FILENAME_LEN];
bool exit_event;
};
#endif /* __BOOTSTRAP_H */
```
### 用户态bootstrap.c
```c
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2020 Facebook */
#include <argp.h>
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "bootstrap.h"
#include "bootstrap.skel.h"
static struct env {
bool verbose;
long min_duration_ms;
} env;
const char *argp_program_version = "bootstrap 0.0";
const char *argp_program_bug_address = "<bpf@vger.kernel.org>";
const char argp_program_doc[] =
"BPF bootstrap demo application.\n"
"\n"
"It traces process start and exits and shows associated \n"
"information (filename, process duration, PID and PPID, etc).\n"
"\n"
"USAGE: ./bootstrap [-d <min-duration-ms>] [-v]\n";
static const struct argp_option opts[] = {
{ "verbose", 'v', NULL, 0, "Verbose debug output" },
{ "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
{},
};
static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
switch (key) {
case 'v':
env.verbose = true;
break;
case 'd':
errno = 0;
env.min_duration_ms = strtol(arg, NULL, 10);
if (errno || env.min_duration_ms <= 0) {
fprintf(stderr, "Invalid duration: %s\n", arg);
argp_usage(state);
}
break;
case ARGP_KEY_ARG:
argp_usage(state);
break;
default:
return ARGP_ERR_UNKNOWN;
}
return 0;
} }
static const struct argp argp = {
.options = opts,
.parser = parse_arg,
.doc = argp_program_doc,
};
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
if (level == LIBBPF_DEBUG && !env.verbose)
return 0;
return vfprintf(stderr, format, args);
}
static volatile bool exiting = false;
static void sig_handler(int sig)
{
exiting = true;
}
static int handle_event(void *ctx, void *data, size_t data_sz)
{
const struct event *e = data;
struct tm *tm;
char ts[32];
time_t t;
time(&t);
tm = localtime(&t);
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
if (e->exit_event) {
printf("%-8s %-5s %-16s %-7d %-7d [%u]",
ts, "EXIT", e->comm, e->pid, e->ppid, e->exit_code);
if (e->duration_ns)
printf(" (%llums)", e->duration_ns / 1000000);
printf("\n");
} else {
printf("%-8s %-5s %-16s %-7d %-7d %s\n",
ts, "EXEC", e->comm, e->pid, e->ppid, e->filename);
}
return 0;
}
int main(int argc, char **argv)
{
struct ring_buffer *rb = NULL;
struct bootstrap_bpf *skel;
int err;
/* Parse command line arguments */
err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
if (err)
return err;
/* Set up libbpf errors and debug info callback */
libbpf_set_print(libbpf_print_fn);
/* Cleaner handling of Ctrl-C */
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler);
/* Load and verify BPF application */
skel = bootstrap_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
/* Parameterize BPF code with minimum duration parameter */
skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;
/* Load & verify BPF programs */
err = bootstrap_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
/* Attach tracepoints */
err = bootstrap_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
/* Set up ring buffer polling */
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
/* Process events */
printf("%-8s %-5s %-16s %-7s %-7s %s\n",
"TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE");
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
/* Ctrl-C will cause -EINTR */
if (err == -EINTR) {
err = 0;
break;
}
if (err < 0) {
printf("Error polling perf buffer: %d\n", err);
break;
}
}
cleanup:
/* Clean up */
ring_buffer__free(rb);
bootstrap_bpf__destroy(skel);
return err < 0 ? -err : 0;
}
``` ```
是一段使用BPFBerkeley Packet Filter的C程序用于跟踪进程启动和退出事件并显示有关它们的信息。BPF是一种强大的机制允许您将称为BPF程序的小程序附加到Linux内核的各个部分。这些程序可用于过滤监视或修改内核的行为 个用户态程序主要用于加载、验证、附加 eBPF 程序,以及接收 eBPF 程序收集的事件数据,并将其打印出来。我们将分析一些关键部分
程序首先定义一些常量,并包含一些头文件。然后定义了一个名为env的struct用于存储一些程序选项例如详细模式和进程报告的最小持续时间。 首先,我们定义了一个 env 结构,用于存储命令行参数:
然后程序定义了一个名为parse_arg的函数用于解析传递给程序的命令行参数。它接受三个参数一个表示正在解析的选项的整数key一个表示选项参数的字符指针arg和一个表示当前解析状态的struct argp_state指针state。该函数处理选项并在env struct中设置相应的值。 ```c
static struct env {
然后程序定义了一个名为sig_handler的函数当被调用时会将全局标志exiting设置为true。这用于在接收到信号时允许程序干净地退出。 bool verbose;
long min_duration_ms;
接下来,我们将继续描述这段代码中的其他部分。 } env;
程序定义了一个名为exec_start的BPF map它的类型为BPF_MAP_TYPE_HASH最大条目数为8192键类型为pid_t值类型为u64。
另外程序还定义了一个名为rb的BPF map它的类型为BPF_MAP_TYPE_RINGBUF最大条目数为256 * 1024。
程序还定义了一个名为min_duration_ns的常量其值为0。
程序定义了一个名为handle_exec的SECstatic evaluator of code函数它被附加到跟踪进程执行的BPF程序上。该函数记录为该PID执行exec的时间并在指定了最小持续时间时不发出exec事件。如果未指定最小持续时间则会从BPF ringbuf保留样本并使用数据填充样本然后将其提交给用户空间进行后处理。
程序还定义了一个名为handle_exit的SEC函数它被附加到跟踪进程退出的BPF程序上。该函数会在确定PID和TID后计算进程的生命周期然后根据min_duration_ns的值决定是否发出退出事件。如果进程的生命周期足够长则会从BPF ringbuf保留样本并使用数据填充样本然后将其提交给用户空间进行后处理。
最后主函数调用bpf_ringbuf_poll来轮询BPF ringbuf并在接收到新的事件时处理该事件。这个函数会持续运行直到全局标志exiting被设置为true此时它会清理资源并退出。
## Install Dependencies
You will need `clang`, `libelf` and `zlib` to build the examples, package names may vary across distros.
On Ubuntu/Debian, you need:
```shell
$ apt install clang libelf1 libelf-dev zlib1g-dev
``` ```
On CentOS/Fedora, you need: 接下来,我们使用 argp 库来解析命令行参数:
```c
static const struct argp_option opts[] = {
{ "verbose", 'v', NULL, 0, "Verbose debug output" },
{ "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" },
{},
};
static error_t parse_arg(int key, char *arg, struct argp_state *state)
{
// ...
}
static const struct argp argp = {
.options = opts,
.parser = parse_arg,
.doc = argp_program_doc,
};
```
main() 函数中,首先解析命令行参数,然后设置 libbpf 的打印回调函数 libbpf_print_fn以便在需要时输出调试信息
```c
err = argp_parse(&argp, argc, argv, 0, NULL, NULL);
if (err)
return err;
libbpf_set_print(libbpf_print_fn);
```
接下来,我们打开 eBPF 脚手架skeleton文件将最小持续时间参数传递给 eBPF 程序,并加载和附加 eBPF 程序:
```c
skel = bootstrap_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open and load BPF skeleton\n");
return 1;
}
skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL;
err = bootstrap_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load and verify BPF skeleton\n");
goto cleanup;
}
err = bootstrap_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton\n");
goto cleanup;
}
```
然后我们创建一个环形缓冲区ring buffer用于接收 eBPF 程序发送的事件数据:
```c
rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL);
if (!rb) {
err = -1;
fprintf(stderr, "Failed to create ring buffer\n");
goto cleanup;
}
```
handle_event() 函数会处理从 eBPF 程序收到的事件。根据事件类型(进程执行或退出),它会提取并打印事件信息,如时间戳、进程名、进程 ID、父进程 ID、文件名或退出代码等。
最后,我们使用 ring_buffer__poll() 函数轮询环形缓冲区,处理收到的事件数据:
```c
while (!exiting) {
err = ring_buffer__poll(rb, 100 /* timeout, ms */);
// ...
}
```
当程序收到 SIGINT 或 SIGTERM 信号时,它会最后完成清理、退出操作,关闭和卸载 eBPF 程序:
```c
cleanup:
/* Clean up */
ring_buffer__free(rb);
bootstrap_bpf__destroy(skel);
return err < 0 ? -err : 0;
}
```
## 安装依赖
构建示例需要 clang、libelf 和 zlib。包名在不同的发行版中可能会有所不同。
在 Ubuntu/Debian 上,你需要执行以下命令:
```shell ```shell
$ dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel sudo apt install clang libelf1 libelf-dev zlib1g-dev
```
在 CentOS/Fedora 上,你需要执行以下命令:
```shell
sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel
``` ```
## 编译运行 ## 编译运行
@@ -171,15 +602,28 @@ $ dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel
编译运行上述代码: 编译运行上述代码:
```console ```console
$ ecc bootstrap.bpf.c bootstrap.h $ make
Compiling bpf object... BPF .output/bootstrap.bpf.o
Packing ebpf object and config into package.json... GEN-SKEL .output/bootstrap.skel.h
$ sudo ecli run package.json CC .output/bootstrap.o
Runing eBPF program... BINARY bootstrap
$ sudo ./bootstrap
[sudo] password for yunwei:
TIME EVENT COMM PID PPID FILENAME/EXIT CODE
03:16:41 EXEC sh 110688 80168 /bin/sh
03:16:41 EXEC which 110689 110688 /usr/bin/which
03:16:41 EXIT which 110689 110688 [0] (0ms)
03:16:41 EXIT sh 110688 80168 [0] (0ms)
03:16:41 EXEC sh 110690 80168 /bin/sh
03:16:41 EXEC ps 110691 110690 /usr/bin/ps
03:16:41 EXIT ps 110691 110690 [0] (49ms)
03:16:41 EXIT sh 110690 80168 [0] (51ms)
``` ```
## 总结 ## 总结
这是一个使用BPF的C程序用于跟踪进程的启动和退出事件并显示有关这些事件的信息。它通过使用argp API来解析命令行参数并使用BPF maps 存储进程的信息包括进程的PID和执行文件的文件名。程序还使用了SEC函数来附加BPF程序以监视进程的执行和退出事件。最后程序在终端中打印出启动和退出的进程信息 通过这个实例,我们了解了如何将 eBPF 程序与用户态程序结合使用。这种结合为开发者提供了一个强大的工具集,可以实现跨内核和用户空间的高效数据收集和处理。通过使用 eBPF 和 libbpf您可以构建更高效、可扩展和安全的监控和性能分析工具
编译这个程序可以使用 ecc 工具,运行时可以使用 ecli 命令。更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档https://github.com/eunomia-bpf/eunomia-bpf 在接下来的教程中,我们将继续深入探讨 eBPF 的高级特性,分享更多关于 eBPF 开发实践的内容。通过不断学习和实践,您将更好地理解和掌握 eBPF 技术,并将其应用于解决实际问题。
如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档:<https://github.com/eunomia-bpf/eunomia-bpf> 。您还可以访问我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 以获取更多示例和完整的教程。

View File

@@ -47,7 +47,7 @@ Userspace:
profile 工具由两个部分组成,内核态中的 eBPF 程序和用户态中的 `profile` 符号处理程序。`profile` 符号处理程序负责加载 eBPF 程序,以及处理 eBPF 程序输出的数据。 profile 工具由两个部分组成,内核态中的 eBPF 程序和用户态中的 `profile` 符号处理程序。`profile` 符号处理程序负责加载 eBPF 程序,以及处理 eBPF 程序输出的数据。
### 内核态部分 ### 内核态部分
内核态 eBPF 程序的实现逻辑主要是借助 perf event对程序的堆栈进行定时采样从而捕获程序的执行流程。 内核态 eBPF 程序的实现逻辑主要是借助 perf event对程序的堆栈进行定时采样从而捕获程序的执行流程。
@@ -64,118 +64,116 @@ profile 工具由两个部分组成,内核态中的 eBPF 程序和用户态中
char LICENSE[] SEC("license") = "Dual BSD/GPL"; char LICENSE[] SEC("license") = "Dual BSD/GPL";
struct { struct {
__uint(type, BPF_MAP_TYPE_RINGBUF); __uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); __uint(max_entries, 256 * 1024);
} events SEC(".maps"); } events SEC(".maps");
SEC("perf_event") SEC("perf_event")
int profile(void *ctx) int profile(void *ctx)
{ {
int pid = bpf_get_current_pid_tgid() >> 32; int pid = bpf_get_current_pid_tgid() >> 32;
int cpu_id = bpf_get_smp_processor_id(); int cpu_id = bpf_get_smp_processor_id();
struct stacktrace_event *event; struct stacktrace_event *event;
int cp; int cp;
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
if (!event) if (!event)
return 1; return 1;
event->pid = pid; event->pid = pid;
event->cpu_id = cpu_id; event->cpu_id = cpu_id;
if (bpf_get_current_comm(event->comm, sizeof(event->comm))) if (bpf_get_current_comm(event->comm, sizeof(event->comm)))
event->comm[0] = 0; event->comm[0] = 0;
event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0); event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0);
event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK); event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK);
bpf_ringbuf_submit(event, 0); bpf_ringbuf_submit(event, 0);
return 0; return 0;
} }
``` ```
接下来,我们将重点讲解内核态代码的关键部分。 接下来,我们将重点讲解内核态代码的关键部分。
1. 定义 eBPF maps `events` 1. 定义 eBPF maps `events`
```c ```c
struct { struct {
__uint(type, BPF_MAP_TYPE_RINGBUF); __uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 256 * 1024); __uint(max_entries, 256 * 1024);
} events SEC(".maps"); } events SEC(".maps");
``` ```
这里定义了一个类型为 `BPF_MAP_TYPE_RINGBUF` 的 eBPF maps 。Ring Buffer 是一种高性能的循环缓冲区,用于在内核和用户空间之间传输数据。`max_entries` 设置了 Ring Buffer 的最大大小。 这里定义了一个类型为 `BPF_MAP_TYPE_RINGBUF` 的 eBPF maps 。Ring Buffer 是一种高性能的循环缓冲区,用于在内核和用户空间之间传输数据。`max_entries` 设置了 Ring Buffer 的最大大小。
2. 定义 `perf_event` eBPF 程序: 2. 定义 `perf_event` eBPF 程序:
```c ```c
SEC("perf_event") SEC("perf_event")
int profile(void *ctx) int profile(void *ctx)
``` ```
这里定义了一个名为 `profile` 的 eBPF 程序,它将在 perf 事件触发时执行。
这里定义了一个名为 `profile` 的 eBPF 程序,它将在 perf 事件触发时执行。
3. 获取进程 ID 和 CPU ID 3. 获取进程 ID 和 CPU ID
```c ```c
int pid = bpf_get_current_pid_tgid() >> 32; int pid = bpf_get_current_pid_tgid() >> 32;
int cpu_id = bpf_get_smp_processor_id(); int cpu_id = bpf_get_smp_processor_id();
``` ```
`bpf_get_current_pid_tgid()` 函数返回当前进程的 PID 和 TID通过右移 32 位,我们得到 PID。`bpf_get_smp_processor_id()` 函数返回当前 CPU 的 ID。 `bpf_get_current_pid_tgid()` 函数返回当前进程的 PID 和 TID通过右移 32 位,我们得到 PID。`bpf_get_smp_processor_id()` 函数返回当前 CPU 的 ID。
4. 预留 Ring Buffer 空间: 4. 预留 Ring Buffer 空间:
```c ```c
event = bpf_ringbuf_reserve(&events, sizeof(*event), 0); event = bpf_ringbuf_reserve(&events, sizeof(*event), 0);
if (!event) if (!event)
return 1; return 1;
``` ```
通过 `bpf_ringbuf_reserve()` 函数预留 Ring Buffer 空间,用于存储采集的栈信息。若预留失败,返回错误. 通过 `bpf_ringbuf_reserve()` 函数预留 Ring Buffer 空间,用于存储采集的栈信息。若预留失败,返回错误.
5. 获取当前进程名: 5. 获取当前进程名:
```c ```c
if (bpf_get_current_comm(event->comm, sizeof(event->comm))) if (bpf_get_current_comm(event->comm, sizeof(event->comm)))
event->comm[0] = 0; event->comm[0] = 0;
``` ```
使用 `bpf_get_current_comm()` 函数获取当前进程名并将其存储到 `event->comm` 使用 `bpf_get_current_comm()` 函数获取当前进程名并将其存储到 `event->comm`。
6. 获取内核栈信息: 6. 获取内核栈信息:
```c ```c
event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0); event->kstack_sz = bpf_get_stack(ctx, event->kstack, sizeof(event->kstack), 0);
``` ```
使用 `bpf_get_stack()` 函数获取内核栈信息。将结果存储在 `event->kstack`,并将其大小存储在 `event->kstack_sz` 使用 `bpf_get_stack()` 函数获取内核栈信息。将结果存储在 `event->kstack`,并将其大小存储在 `event->kstack_sz`。
7. 获取用户空间栈信息: 7. 获取用户空间栈信息:
```c ```c
event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK); event->ustack_sz = bpf_get_stack(ctx, event->ustack, sizeof(event->ustack), BPF_F_USER_STACK);
``` ```
同样使用 `bpf_get_stack()` 函数,但传递 `BPF_F_USER_STACK` 标志以获取用户空间栈信息。将结果存储在 `event->ustack`,并将其大小存储在 `event->ustack_sz` 同样使用 `bpf_get_stack()` 函数,但传递 `BPF_F_USER_STACK` 标志以获取用户空间栈信息。将结果存储在 `event->ustack`,并将其大小存储在 `event->ustack_sz`。
8. 将事件提交到 Ring Buffer 8. 将事件提交到 Ring Buffer
```c ```c
bpf_ringbuf_submit(event, 0); bpf_ringbuf_submit(event, 0);
``` ```
最后,使用 `bpf_ringbuf_submit()` 函数将事件提交到 Ring Buffer以便用户空间程序可以读取和处理。 最后,使用 `bpf_ringbuf_submit()` 函数将事件提交到 Ring Buffer以便用户空间程序可以读取和处理。
这个内核态 eBPF 程序通过定期采样程序的内核栈和用户空间栈来捕获程序的执行流程。这些数据将存储在 Ring Buffer 中,以便用户态的 `profile` 程序能读取 这个内核态 eBPF 程序通过定期采样程序的内核栈和用户空间栈来捕获程序的执行流程。这些数据将存储在 Ring Buffer 中,以便用户态的 `profile` 程序能读取
### 用户态部分 ### 用户态部分
@@ -183,37 +181,37 @@ bpf_ringbuf_submit(event, 0);
```c ```c
static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid, static long perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
int cpu, int group_fd, unsigned long flags) int cpu, int group_fd, unsigned long flags)
{ {
int ret; int ret;
ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags); ret = syscall(__NR_perf_event_open, hw_event, pid, cpu, group_fd, flags);
return ret; return ret;
} }
int main(){ int main(){
... ...
for (cpu = 0; cpu < num_cpus; cpu++) { for (cpu = 0; cpu < num_cpus; cpu++) {
/* skip offline/not present CPUs */ /* skip offline/not present CPUs */
if (cpu >= num_online_cpus || !online_mask[cpu]) if (cpu >= num_online_cpus || !online_mask[cpu])
continue; continue;
/* Set up performance monitoring on a CPU/Core */ /* Set up performance monitoring on a CPU/Core */
pefd = perf_event_open(&attr, pid, cpu, -1, PERF_FLAG_FD_CLOEXEC); pefd = perf_event_open(&attr, pid, cpu, -1, PERF_FLAG_FD_CLOEXEC);
if (pefd < 0) { if (pefd < 0) {
fprintf(stderr, "Fail to set up performance monitor on a CPU/Core\n"); fprintf(stderr, "Fail to set up performance monitor on a CPU/Core\n");
err = -1; err = -1;
goto cleanup; goto cleanup;
} }
pefds[cpu] = pefd; pefds[cpu] = pefd;
/* Attach a BPF program on a CPU */ /* Attach a BPF program on a CPU */
links[cpu] = bpf_program__attach_perf_event(skel->progs.profile, pefd); links[cpu] = bpf_program__attach_perf_event(skel->progs.profile, pefd);
if (!links[cpu]) { if (!links[cpu]) {
err = -1; err = -1;
goto cleanup; goto cleanup;
} }
} }
... ...
} }
``` ```
@@ -224,7 +222,7 @@ int main(){
```c ```c
for (cpu = 0; cpu < num_cpus; cpu++) { for (cpu = 0; cpu < num_cpus; cpu++) {
// ... // ...
} }
``` ```
@@ -237,86 +235,86 @@ for (cpu = 0; cpu < num_cpus; cpu++) {
```c ```c
static void show_stack_trace(__u64 *stack, int stack_sz, pid_t pid) static void show_stack_trace(__u64 *stack, int stack_sz, pid_t pid)
{ {
const struct blazesym_result *result; const struct blazesym_result *result;
const struct blazesym_csym *sym; const struct blazesym_csym *sym;
sym_src_cfg src; sym_src_cfg src;
int i, j; int i, j;
if (pid) { if (pid) {
src.src_type = SRC_T_PROCESS; src.src_type = SRC_T_PROCESS;
src.params.process.pid = pid; src.params.process.pid = pid;
} else { } else {
src.src_type = SRC_T_KERNEL; src.src_type = SRC_T_KERNEL;
src.params.kernel.kallsyms = NULL; src.params.kernel.kallsyms = NULL;
src.params.kernel.kernel_image = NULL; src.params.kernel.kernel_image = NULL;
} }
result = blazesym_symbolize(symbolizer, &src, 1, (const uint64_t *)stack, stack_sz); result = blazesym_symbolize(symbolizer, &src, 1, (const uint64_t *)stack, stack_sz);
for (i = 0; i < stack_sz; i++) { for (i = 0; i < stack_sz; i++) {
if (!result || result->size <= i || !result->entries[i].size) { if (!result || result->size <= i || !result->entries[i].size) {
printf(" %d [<%016llx>]\n", i, stack[i]); printf(" %d [<%016llx>]\n", i, stack[i]);
continue; continue;
} }
if (result->entries[i].size == 1) { if (result->entries[i].size == 1) {
sym = &result->entries[i].syms[0]; sym = &result->entries[i].syms[0];
if (sym->path && sym->path[0]) { if (sym->path && sym->path[0]) {
printf(" %d [<%016llx>] %s+0x%llx %s:%ld\n", printf(" %d [<%016llx>] %s+0x%llx %s:%ld\n",
i, stack[i], sym->symbol, i, stack[i], sym->symbol,
stack[i] - sym->start_address, stack[i] - sym->start_address,
sym->path, sym->line_no); sym->path, sym->line_no);
} else { } else {
printf(" %d [<%016llx>] %s+0x%llx\n", printf(" %d [<%016llx>] %s+0x%llx\n",
i, stack[i], sym->symbol, i, stack[i], sym->symbol,
stack[i] - sym->start_address); stack[i] - sym->start_address);
} }
continue; continue;
} }
printf(" %d [<%016llx>]\n", i, stack[i]); printf(" %d [<%016llx>]\n", i, stack[i]);
for (j = 0; j < result->entries[i].size; j++) { for (j = 0; j < result->entries[i].size; j++) {
sym = &result->entries[i].syms[j]; sym = &result->entries[i].syms[j];
if (sym->path && sym->path[0]) { if (sym->path && sym->path[0]) {
printf(" %s+0x%llx %s:%ld\n", printf(" %s+0x%llx %s:%ld\n",
sym->symbol, stack[i] - sym->start_address, sym->symbol, stack[i] - sym->start_address,
sym->path, sym->line_no); sym->path, sym->line_no);
} else { } else {
printf(" %s+0x%llx\n", sym->symbol, printf(" %s+0x%llx\n", sym->symbol,
stack[i] - sym->start_address); stack[i] - sym->start_address);
} }
} }
} }
blazesym_result_free(result); blazesym_result_free(result);
} }
/* Receive events from the ring buffer. */ /* Receive events from the ring buffer. */
static int event_handler(void *_ctx, void *data, size_t size) static int event_handler(void *_ctx, void *data, size_t size)
{ {
struct stacktrace_event *event = data; struct stacktrace_event *event = data;
if (event->kstack_sz <= 0 && event->ustack_sz <= 0) if (event->kstack_sz <= 0 && event->ustack_sz <= 0)
return 1; return 1;
printf("COMM: %s (pid=%d) @ CPU %d\n", event->comm, event->pid, event->cpu_id); printf("COMM: %s (pid=%d) @ CPU %d\n", event->comm, event->pid, event->cpu_id);
if (event->kstack_sz > 0) { if (event->kstack_sz > 0) {
printf("Kernel:\n"); printf("Kernel:\n");
show_stack_trace(event->kstack, event->kstack_sz / sizeof(__u64), 0); show_stack_trace(event->kstack, event->kstack_sz / sizeof(__u64), 0);
} else { } else {
printf("No Kernel Stack\n"); printf("No Kernel Stack\n");
} }
if (event->ustack_sz > 0) { if (event->ustack_sz > 0) {
printf("Userspace:\n"); printf("Userspace:\n");
show_stack_trace(event->ustack, event->ustack_sz / sizeof(__u64), event->pid); show_stack_trace(event->ustack, event->ustack_sz / sizeof(__u64), event->pid);
} else { } else {
printf("No Userspace Stack\n"); printf("No Userspace Stack\n");
} }
printf("\n"); printf("\n");
return 0; return 0;
} }
``` ```
@@ -330,6 +328,6 @@ static int event_handler(void *_ctx, void *data, size_t size)
通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 程序进行性能分析。在这个过程中,我们详细讲解了如何创建 eBPF 程序,监控进程的性能,并从 ring buffer 中获取数据以分析栈回溯。我们还学习了如何使用 perf_event_open() 函数设置性能监控,并将 BPF 程序附加到性能事件上。在本教程中,我们还展示了如何编写 eBPF 程序来捕获进程的内核和用户空间栈信息,进而分析程序性能瓶颈。通过这个例子,您可以了解到 eBPF 在性能分析方面的强大功能。 通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 程序进行性能分析。在这个过程中,我们详细讲解了如何创建 eBPF 程序,监控进程的性能,并从 ring buffer 中获取数据以分析栈回溯。我们还学习了如何使用 perf_event_open() 函数设置性能监控,并将 BPF 程序附加到性能事件上。在本教程中,我们还展示了如何编写 eBPF 程序来捕获进程的内核和用户空间栈信息,进而分析程序性能瓶颈。通过这个例子,您可以了解到 eBPF 在性能分析方面的强大功能。
如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档https://github.com/eunomia-bpf/eunomia-bpf。您还可以访问我们的教程代码仓库 https://github.com/eunomia-bpf/bpf-developer-tutorial 以获取更多示例和完整的教程。 如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档:<https://github.com/eunomia-bpf/eunomia-bpf> 。您还可以访问我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 以获取更多示例和完整的教程。
接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术,希望这些内容对您在 eBPF 开发道路上的学习和实践有所帮助。 接下来的教程将进一步探讨 eBPF 的高级特性,我们会继续分享更多有关 eBPF 开发实践的内容,帮助您更好地理解和掌握 eBPF 技术,希望这些内容对您在 eBPF 开发道路上的学习和实践有所帮助。

View File

@@ -6,11 +6,11 @@ eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网
## ring buffer ## ring buffer
现在有一个新的 BPF 数据结构可用BPF 环形缓冲区ring buffer。它解决了 BPF perf buffer当今从内核向用户空间发送数据的事实上的标准的内存效率和事件重排问题同时达到或超过了它的性能。它既提供了与 perf buffer 兼容以方便迁移,又有新的保留/提交API具有更好的可用性。另外合成和真实世界的基准测试表明在几乎所有的情况下所以考虑将其作为从BPF程序向用户空间发送数据的默认选择。 现在有一个新的 BPF 数据结构可用eBPF 环形缓冲区ring buffer。它解决了 BPF perf buffer当今从内核向用户空间发送数据的事实上的标准的内存效率和事件重排问题同时达到或超过了它的性能。它既提供了与 perf buffer 兼容以方便迁移,又有新的保留/提交API具有更好的可用性。另外合成和真实世界的基准测试表明在几乎所有的情况下所以考虑将其作为从BPF程序向用户空间发送数据的默认选择。
### BPF ringbuf vs BPF perfbuf ### eBPF ringbuf vs eBPF perfbuf
今天,只要BPF程序需要将收集到的数据发送到用户空间进行后处理和记录它通常会使用BPF perf bufferperfbuf来实现。Perfbuf 是每个CPU循环缓冲区的集合它允许在内核和用户空间之间有效地交换数据。它在实践中效果很好但由于其按CPU设计它有两个主要的缺点在实践中被证明是不方便的内存的低效使用和事件的重新排序。 只要 BPF 程序需要将收集到的数据发送到用户空间进行后处理和记录,它通常会使用 BPF perf bufferperfbuf来实现。Perfbuf 是每个CPU循环缓冲区的集合它允许在内核和用户空间之间有效地交换数据。它在实践中效果很好但由于其按CPU设计它有两个主要的缺点在实践中被证明是不方便的内存的低效使用和事件的重新排序。
为了解决这些问题从Linux 5.8开始BPF提供了一个新的BPF数据结构BPF map。BPF环形缓冲区ringbuf。它是一个多生产者、单消费者MPSC队列可以同时在多个CPU上安全共享。 为了解决这些问题从Linux 5.8开始BPF提供了一个新的BPF数据结构BPF map。BPF环形缓冲区ringbuf。它是一个多生产者、单消费者MPSC队列可以同时在多个CPU上安全共享。
@@ -42,11 +42,11 @@ BPF ringbuf 支持来自 BPF perfbuf 的熟悉的功能:
#define MAX_FILENAME_LEN 127 #define MAX_FILENAME_LEN 127
struct event { struct event {
int pid; int pid;
int ppid; int ppid;
unsigned exit_code; unsigned exit_code;
unsigned long long duration_ns; unsigned long long duration_ns;
char comm[TASK_COMM_LEN]; char comm[TASK_COMM_LEN];
}; };
#endif /* __BOOTSTRAP_H */ #endif /* __BOOTSTRAP_H */
@@ -104,13 +104,19 @@ int handle_exit(struct trace_event_raw_sched_process_template* ctx)
} }
``` ```
这段代码是一个 BPF 程序,用于监控 Linux 系统中的进程退出事件。 这段代码展示了如何使用 exitsnoop 监控进程退出事件并使用 ring buffer 向用户态打印输出:
该程序通过注册一个 tracepoint来监控进程退出事件。Tracepoint 是一种内核特性,允许内核模块获取特定事件的通知。在本程序中,注册的 tracepoint 是“tp/sched/sched_process_exit”表示该程序监控的是进程退出事件 1. 首先,我们引入所需的头文件和 exitsnoop.h
2. 定义一个名为 "LICENSE" 的全局变量,内容为 "Dual BSD/GPL",这是 eBPF 程序的许可证要求。
3. 定义一个名为 rb 的 BPF_MAP_TYPE_RINGBUF 类型的映射,它将用于将内核空间的数据传输到用户空间。指定 max_entries 为 256 * 1024代表 ring buffer 的最大容量。
4. 定义一个名为 handle_exit 的 eBPF 程序,它将在进程退出事件触发时执行。传入一个名为 ctx 的 trace_event_raw_sched_process_template 结构体指针作为参数。
5. 使用 bpf_get_current_pid_tgid() 函数获取当前任务的 PID 和 TID。对于主线程PID 和 TID 相同;对于子线程,它们是不同的。我们只关心进程(主线程)的退出,因此在 PID 和 TID 不同时返回 0忽略子线程退出事件。
6. 使用 bpf_ringbuf_reserve 函数为事件结构体 e 在 ring buffer 中预留空间。如果预留失败,返回 0。
7. 使用 bpf_get_current_task() 函数获取当前任务的 task_struct 结构指针。
8. 将进程相关信息填充到预留的事件结构体 e 中包括进程持续时间、PID、PPID、退出代码以及进程名称。
9. 最后,使用 bpf_ringbuf_submit 函数将填充好的事件结构体 e 提交到 ring buffer之后在用户空间进行处理和输出。
当系统中发生进程退出事件时BPF 程序会捕获该事件并调用“handle_exit”函数来处理它。该函数首先检查当前退出事件是否是进程退出事件而不是线程退出事件然后在 BPF 环形缓冲区“rb”中保留一个事件结构体并填充该结构体中的其他信息例如进程 ID、进程名称、退出代码和退出信号等信息。最后该函数还会调用 BPF 的“perf_event_output”函数将捕获的事件发送给用户空间程序 这个示例展示了如何使用 exitsnoop 和 ring buffer 在 eBPF 程序中捕获进程退出事件并将相关信息传输到用户空间。这对于分析进程退出原因和监控系统行为非常有用
总而言之,这段代码是一个 BPF 程序,用于监控 Linux 系统中的进程退出事件.
## Compile and Run ## Compile and Run
@@ -149,6 +155,6 @@ TIME PID PPID EXIT_CODE DURATION_NS COMM
## 总结 ## 总结
本文介绍了如何使用 eunomia-bpf 开发一个简单的 BPF 程序,该程序可以监控 Linux 系统中的进程退出事件, 并将捕获的事件通过 ring buffer 发送给用户空间程序。在本文中,我们使用 eunomia-bpf 编译运行了这个例子。如果你想了解更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:<https://github.com/eunomia-bpf/eunomia-bpf> 本文介绍了如何使用 eunomia-bpf 开发一个简单的 BPF 程序,该程序可以监控 Linux 系统中的进程退出事件, 并将捕获的事件通过 ring buffer 发送给用户空间程序。在本文中,我们使用 eunomia-bpf 编译运行了这个例子。
完整的教程和源代码已经全部开源,可以在 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 中查看。 为了更好地理解和实践 eBPF 编程,我们建议您阅读 eunomia-bpf 的官方文档:<https://github.com/eunomia-bpf/eunomia-bpf> 。此外,我们还为您提供了完整的教程和源代码,可以在 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 中查看和学习。希望本教程能够帮助您顺利入门 eBPF 开发,并为您的进一步学习和实践提供有益的参考

View File

@@ -4,7 +4,7 @@ eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网
runqlat 是一个 eBPF 工具,用于分析 Linux 系统的调度性能。具体来说runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。 runqlat 是一个 eBPF 工具,用于分析 Linux 系统的调度性能。具体来说runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。
# runqlat 原理 ## runqlat 原理
runqlat 的实现利用了 eBPF 程序它通过内核跟踪点和函数探针来测量进程在运行队列中的时间。当进程被排队时trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta被用于更新进程的直方图该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。 runqlat 的实现利用了 eBPF 程序它通过内核跟踪点和函数探针来测量进程在运行队列中的时间。当进程被排队时trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta被用于更新进程的直方图该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。
@@ -182,6 +182,7 @@ const volatile bool targ_per_pidns = false;
const volatile bool targ_ms = false; const volatile bool targ_ms = false;
const volatile pid_t targ_tgid = 0; const volatile pid_t targ_tgid = 0;
``` ```
这些变量包括最大映射项数量、任务状态、过滤选项和目标选项。这些选项可以通过用户空间程序设置,以定制 eBPF 程序的行为。 这些变量包括最大映射项数量、任务状态、过滤选项和目标选项。这些选项可以通过用户空间程序设置,以定制 eBPF 程序的行为。
接下来,定义了一些 eBPF 映射: 接下来,定义了一些 eBPF 映射:
@@ -210,6 +211,7 @@ struct {
__type(value, struct hist); __type(value, struct hist);
} hists SEC(".maps"); } hists SEC(".maps");
``` ```
这些映射包括: 这些映射包括:
- cgroup_map 用于过滤 cgroup - cgroup_map 用于过滤 cgroup
@@ -235,6 +237,7 @@ static int trace_enqueue(u32 tgid, u32 pid)
return 0; return 0;
} }
``` ```
pid_namespace 函数用于获取进程所属的 PID namespace pid_namespace 函数用于获取进程所属的 PID namespace
```c ```c
@@ -265,6 +268,7 @@ static int handle_switch(bool preempt, struct task_struct *prev, struct task_str
... ...
} }
``` ```
首先,函数根据 filter_cg 的设置判断是否需要过滤 cgroup。然后如果之前的进程状态为 TASK_RUNNING则调用 trace_enqueue 函数记录进程的入队时间。接着函数查找下一个进程的入队时间戳如果找不到直接返回。计算调度延迟delta并根据不同的选项设置targ_per_processtarg_per_threadtarg_per_pidns确定直方图映射的键hkey。然后查找或初始化直方图映射更新直方图数据最后删除进程的入队时间戳记录。 首先,函数根据 filter_cg 的设置判断是否需要过滤 cgroup。然后如果之前的进程状态为 TASK_RUNNING则调用 trace_enqueue 函数记录进程的入队时间。接着函数查找下一个进程的入队时间戳如果找不到直接返回。计算调度延迟delta并根据不同的选项设置targ_per_processtarg_per_threadtarg_per_pidns确定直方图映射的键hkey。然后查找或初始化直方图映射更新直方图数据最后删除进程的入队时间戳记录。
接下来是 eBPF 程序的入口点。程序使用三个入口点来捕获不同的调度事件: 接下来是 eBPF 程序的入口点。程序使用三个入口点来捕获不同的调度事件:
@@ -280,6 +284,7 @@ static int handle_switch(bool preempt, struct task_struct *prev, struct task_str
```c ```c
char LICENSE[] SEC("license") = "GPL"; char LICENSE[] SEC("license") = "GPL";
``` ```
这一声明指定了 eBPF 程序的许可证类型,这里使用的是 "GPL"。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。 这一声明指定了 eBPF 程序的许可证类型,这里使用的是 "GPL"。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。
### runqlat.h ### runqlat.h