diff --git a/src/10-hardirqs/README.md b/src/10-hardirqs/README.md index cfdbcd5..518b337 100644 --- a/src/10-hardirqs/README.md +++ b/src/10-hardirqs/README.md @@ -2,19 +2,36 @@ eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。 -本文是 eBPF 入门开发实践教程的第十篇,在 eBPF 中。 +本文是 eBPF 入门开发实践教程的第十篇,在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件。 +hardirqs 和 softirqs 是 Linux 内核中两种不同类型的中断处理程序。它们用于处理硬件设备产生的中断请求,以及内核中的异步事件。在 eBPF 中,我们可以使用同名的 eBPF 工具 hardirqs 和 softirqs 来捕获和分析内核中与中断处理相关的信息。 -## hardirqs是什么? +## hardirqs 和 softirqs 是什么? -hardirqs 是 bcc-tools 工具包的一部分,该工具包是一组用于在 Linux 系统上执行系统跟踪和分析的实用程序。 -hardirqs 是一种用于跟踪和分析 Linux 内核中的中断处理程序的工具。它使用 BPF(Berkeley Packet Filter)程序来收集有关中断处理程序的数据, -并可用于识别内核中的性能问题和其他与中断处理相关的问题。 +hardirqs 是硬件中断处理程序。当硬件设备产生一个中断请求时,内核会将该请求映射到一个特定的中断向量,然后执行与之关联的硬件中断处理程序。硬件中断处理程序通常用于处理设备驱动程序中的事件,例如设备数据传输完成或设备错误。 + +softirqs 是软件中断处理程序。它们是内核中的一种底层异步事件处理机制,用于处理内核中的高优先级任务。softirqs 通常用于处理网络协议栈、磁盘子系统和其他内核组件中的事件。与硬件中断处理程序相比,软件中断处理程序具有更高的灵活性和可配置性。 ## 实现原理 -在 Linux 内核中,每个中断处理程序都有一个唯一的名称,称为中断向量。hardirqs 通过检查每个中断处理程序的中断向量,来监控内核中的中断处理程序。当内核接收到一个中断时,它会查找与该中断相关的中断处理程序,并执行该程序。hardirqs 通过检查内核中执行的中断处理程序,来监控内核中的中断处理程序。另外,hardirqs 还可以通过注入 BPF 程序到内核中,来捕获内核中的中断处理程序。这样,hardirqs 就可以监控内核中执行的中断处理程序,并收集有关它们的信息。 +在 eBPF 中,我们可以通过挂载特定的 kprobe 或者 tracepoint 来捕获和分析 hardirqs 和 softirqs。为了捕获 hardirqs 和 softirqs,需要在相关的内核函数上放置 eBPF 程序。这些函数包括: -## 代码实现 +- 对于 hardirqs:irq_handler_entry 和 irq_handler_exit。 +- 对于 softirqs:softirq_entry 和 softirq_exit。 + +当内核处理 hardirqs 或 softirqs 时,这些 eBPF 程序会被执行,从而收集相关信息,如中断向量、中断处理程序的执行时间等。收集到的信息可以用于分析内核中的性能问题和其他与中断处理相关的问题。 + +为了捕获 hardirqs 和 softirqs,可以遵循以下步骤: + +1. 在 eBPF 程序中定义用于存储中断信息的数据结构和映射。 +2. 编写 eBPF 程序,将其挂载到相应的内核函数上,以捕获 hardirqs 或 softirqs。 +3. 在 eBPF 程序中,收集中断处理程序的相关信息,并将这些信息存储在映射中。 +4. 在用户空间应用程序中,读取映射中的数据以分析和展示中断处理信息。 + +通过上述方法,我们可以在 eBPF 中使用 hardirqs 和 softirqs 捕获和分析内核中的中断事件,以识别潜在的性能问题和与中断处理相关的问题。 + +## hardirqs 代码实现 + +hardirqs 程序的主要目的是获取中断处理程序的名称、执行次数和执行时间,并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。 ```c // SPDX-License-Identifier: GPL-2.0 @@ -149,7 +166,74 @@ int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action) char LICENSE[] SEC("license") = "GPL"; ``` -这是一个 BPF(Berkeley Packet Filter)程序。BPF 程序是小型程序,可以直接在 Linux 内核中运行,用于过滤和操纵网络流量。这个特定的程序似乎旨在收集内核中中断处理程序的统计信息。它定义了一些 maps (可以在 BPF 程序和内核的其他部分之间共享的数据结构)和两个函数:handle_entry 和 handle_exit。当内核进入和退出中断处理程序时,分别执行这些函数。handle_entry 函数用于跟踪中断处理程序被执行的次数,而 handle_exit 则用于测量中断处理程序中花费的时间。 +这段代码是一个 eBPF 程序,用于捕获和分析内核中硬件中断处理程序(hardirqs)的执行信息。程序的主要目的是获取中断处理程序的名称、执行次数和执行时间,并以直方图的形式展示执行时间的分布。让我们一步步分析这段代码。 + +1. 包含必要的头文件和定义数据结构: + +```c + +#include +#include +#include +#include +#include "hardirqs.h" +#include "bits.bpf.h" +#include "maps.bpf.h" +``` + +该程序包含了 eBPF 开发所需的标准头文件,以及用于定义数据结构和映射的自定义头文件。 + +2. 定义全局变量和映射: + +```c + +#define MAX_ENTRIES 256 + +const volatile bool filter_cg = false; +const volatile bool targ_dist = false; +const volatile bool targ_ns = false; +const volatile bool do_count = false; + +... +``` + +该程序定义了一些全局变量,用于配置程序的行为。例如,`filter_cg` 控制是否过滤 cgroup,`targ_dist` 控制是否显示执行时间的分布等。此外,程序还定义了三个映射,分别用于存储 cgroup 信息、开始时间戳和中断处理程序的信息。 + +3. 定义两个辅助函数 `handle_entry` 和 `handle_exit`: + +这两个函数分别在中断处理程序的入口和出口处被调用。`handle_entry` 记录开始时间戳或更新中断计数,`handle_exit` 计算中断处理程序的执行时间,并将结果存储到相应的信息映射中。 +4. 定义 eBPF 程序的入口点: + +```c + +SEC("tp_btf/irq_handler_entry") +int BPF_PROG(irq_handler_entry_btf, int irq, struct irqaction *action) +{ + return handle_entry(irq, action); +} + +SEC("tp_btf/irq_handler_exit") +int BPF_PROG(irq_handler_exit_btf, int irq, struct irqaction *action) +{ + return handle_exit(irq, action); +} + +SEC("raw_tp/irq_handler_entry") +int BPF_PROG(irq_handler_entry, int irq, struct irqaction *action) +{ + return handle_entry(irq, action); +} + +SEC("raw_tp/irq_handler_exit") +int BPF_PROG(irq_handler_exit, int irq, struct irqaction *action) +{ + return handle_exit(irq, action); +} +``` + +这里定义了四个 eBPF 程序入口点,分别用于捕获中断处理程序的入口和出口事件。`tp_btf` 和 `raw_tp` 分别代表使用 BPF Type Format(BTF)和原始 tracepoints 捕获事件。这样可以确保程序在不同内核版本上可以移植和运行。 + +Softirq 代码也类似,这里就不再赘述了。 ## 运行代码 @@ -171,6 +255,8 @@ sudo ecli run ./package.json ## 总结 -更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档: +在本章节(eBPF 入门开发实践教程十:在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件)中,我们学习了如何使用 eBPF 程序捕获和分析内核中硬件中断处理程序(hardirqs)的执行信息。我们详细讲解了示例代码,包括如何定义数据结构、映射以及 eBPF 程序入口点,以及如何在中断处理程序的入口和出口处调用辅助函数来记录执行信息。 -完整的教程和源代码已经全部开源,可以在 中查看。 +通过学习本章节内容,您应该已经掌握了如何在 eBPF 中使用 hardirqs 或 softirqs 捕获中断事件的方法,以及如何分析这些事件以识别内核中的性能问题和其他与中断处理相关的问题。这些技能对于分析和优化 Linux 内核的性能至关重要。 + +为了更好地理解和实践 eBPF 编程,我们建议您阅读 eunomia-bpf 的官方文档:https://github.com/eunomia-bpf/eunomia-bpf。此外,我们还为您提供了完整的教程和源代码,您可以在 https://github.com/eunomia-bpf/bpf-developer-tutorial 中查看和学习。希望本教程能够帮助您顺利入门 eBPF 开发,并为您的进一步学习和实践提供有益的参考。 \ No newline at end of file diff --git a/src/10-hardirqs/softirqs.bpf.c b/src/10-hardirqs/softirqs.bpf.c new file mode 100644 index 0000000..dde1e8d --- /dev/null +++ b/src/10-hardirqs/softirqs.bpf.c @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: GPL-2.0 +// Copyright (c) 2020 Wenbo Zhang +#include +#include +#include +#include "softirqs.h" +#include "bits.bpf.h" +#include "maps.bpf.h" + +const volatile bool targ_dist = false; +const volatile bool targ_ns = false; + +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, u32); + __type(value, u64); +} start SEC(".maps"); + +__u64 counts[NR_SOFTIRQS] = {}; +__u64 time[NR_SOFTIRQS] = {}; +struct hist hists[NR_SOFTIRQS] = {}; + +static int handle_entry(unsigned int vec_nr) +{ + u64 ts = bpf_ktime_get_ns(); + u32 key = 0; + + bpf_map_update_elem(&start, &key, &ts, BPF_ANY); + return 0; +} + +static int handle_exit(unsigned int vec_nr) +{ + u64 delta, *tsp; + u32 key = 0; + + if (vec_nr >= NR_SOFTIRQS) + return 0; + tsp = bpf_map_lookup_elem(&start, &key); + if (!tsp) + return 0; + delta = bpf_ktime_get_ns() - *tsp; + if (!targ_ns) + delta /= 1000U; + + if (!targ_dist) { + __sync_fetch_and_add(&counts[vec_nr], 1); + __sync_fetch_and_add(&time[vec_nr], delta); + } else { + struct hist *hist; + u64 slot; + + hist = &hists[vec_nr]; + slot = log2(delta); + if (slot >= MAX_SLOTS) + slot = MAX_SLOTS - 1; + __sync_fetch_and_add(&hist->slots[slot], 1); + } + + return 0; +} + +SEC("tp_btf/softirq_entry") +int BPF_PROG(softirq_entry_btf, unsigned int vec_nr) +{ + return handle_entry(vec_nr); +} + +SEC("tp_btf/softirq_exit") +int BPF_PROG(softirq_exit_btf, unsigned int vec_nr) +{ + return handle_exit(vec_nr); +} + +SEC("raw_tp/softirq_entry") +int BPF_PROG(softirq_entry, unsigned int vec_nr) +{ + return handle_entry(vec_nr); +} + +SEC("raw_tp/softirq_exit") +int BPF_PROG(softirq_exit, unsigned int vec_nr) +{ + return handle_exit(vec_nr); +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/src/10-hardirqs/softirqs.h b/src/10-hardirqs/softirqs.h new file mode 100644 index 0000000..efc02a4 --- /dev/null +++ b/src/10-hardirqs/softirqs.h @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ +#ifndef __SOFTIRQS_H +#define __SOFTIRQS_H + +#define MAX_SLOTS 20 + +struct hist { + __u32 slots[MAX_SLOTS]; +}; + +#endif /* __SOFTIRQS_H */ diff --git a/src/9-runqlat/README.md b/src/9-runqlat/README.md index 513293a..e8b8caa 100755 --- a/src/9-runqlat/README.md +++ b/src/9-runqlat/README.md @@ -1,17 +1,17 @@ -# eBPF 入门开发实践教程九:一个 Linux 内核 BPF 程序,通过柱状图来总结调度程序运行队列延迟,显示任务等待运行在 CPU 上的时间长度 +# eBPF 入门开发实践教程九:捕获进程调度延迟,以直方图方式记录 eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。 -## runqlat是什么? +runqlat 是一个 eBPF 工具,用于分析 Linux 系统的调度性能。具体来说,runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。 -bcc-tools 是一组用于在 Linux 系统上使用 BPF 程序的工具。runqlat 是 bcc-tools 中的一个工具,用于分析 Linux 系统的调度性能。具体来说,runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。 +# runqlat 原理 -## runqlat 原理 - -runqlat 使用内核跟踪点和函数探针的结合来测量进程在运行队列中的时间。当进程被排队时,trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时,handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta)然后用于更新进程的直方图,该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。 +runqlat 的实现利用了 eBPF 程序,它通过内核跟踪点和函数探针来测量进程在运行队列中的时间。当进程被排队时,trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时,handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta)被用于更新进程的直方图,该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。 ## runqlat 代码实现 +### runqlat.bpf.c + 首先我们需要编写一个源代码文件 runqlat.bpf.c: ```c @@ -169,6 +169,121 @@ int BPF_PROG(handle_sched_switch, bool preempt, struct task_struct *prev, struct char LICENSE[] SEC("license") = "GPL"; ``` +首先,定义了一些常量和全局变量: + +```c +#define MAX_ENTRIES 10240 +#define TASK_RUNNING 0 + +const volatile bool filter_cg = false; +const volatile bool targ_per_process = false; +const volatile bool targ_per_thread = false; +const volatile bool targ_per_pidns = false; +const volatile bool targ_ms = false; +const volatile pid_t targ_tgid = 0; +``` +这些变量包括最大映射项数量、任务状态、过滤选项和目标选项。这些选项可以通过用户空间程序设置,以定制 eBPF 程序的行为。 + +接下来,定义了一些 eBPF 映射: + +```c +struct { + __uint(type, BPF_MAP_TYPE_CGROUP_ARRAY); + __type(key, u32); + __type(value, u32); + __uint(max_entries, 1); +} cgroup_map SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_ENTRIES); + __type(key, u32); + __type(value, u64); +} start SEC(".maps"); + +static struct hist zero; + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_ENTRIES); + __type(key, u32); + __type(value, struct hist); +} hists SEC(".maps"); +``` +这些映射包括: + +- cgroup_map 用于过滤 cgroup; +- start 用于存储进程入队时的时间戳; +- hists 用于存储直方图数据,记录进程调度延迟。 + +接下来是一些辅助函数: + +trace_enqueue 函数用于在进程入队时记录其时间戳: + +```c +static int trace_enqueue(u32 tgid, u32 pid) +{ + u64 ts; + + if (!pid) + return 0; + if (targ_tgid && targ_tgid != tgid) + return 0; + + ts = bpf_ktime_get_ns(); + bpf_map_update_elem(&start, &pid, &ts, BPF_ANY); + return 0; +} +``` +pid_namespace 函数用于获取进程所属的 PID namespace: + +```c +static unsigned int pid_namespace(struct task_struct *task) +{ + struct pid *pid; + unsigned int level; + struct upid upid; + unsigned int inum; + + /* get the pid namespace by following task_active_pid_ns(), + * pid->numbers[pid->level].ns + */ + pid = BPF_CORE_READ(task, thread_pid); + level = BPF_CORE_READ(pid, level); + bpf_core_read(&upid, sizeof(upid), &pid->numbers[level]); + inum = BPF_CORE_READ(upid.ns, ns.inum); + + return inum; +} +``` + +handle_switch 函数是核心部分,用于处理调度切换事件,计算进程调度延迟并更新直方图数据: + +```c +static int handle_switch(bool preempt, struct task_struct *prev, struct task_struct *next) +{ + ... +} +``` +首先,函数根据 filter_cg 的设置判断是否需要过滤 cgroup。然后,如果之前的进程状态为 TASK_RUNNING,则调用 trace_enqueue 函数记录进程的入队时间。接着,函数查找下一个进程的入队时间戳,如果找不到,直接返回。计算调度延迟(delta),并根据不同的选项设置(targ_per_process,targ_per_thread,targ_per_pidns),确定直方图映射的键(hkey)。然后查找或初始化直方图映射,更新直方图数据,最后删除进程的入队时间戳记录。 + +接下来是 eBPF 程序的入口点。程序使用三个入口点来捕获不同的调度事件: + +- handle_sched_wakeup:用于处理 sched_wakeup 事件,当一个进程从睡眠状态被唤醒时触发。 +- handle_sched_wakeup_new:用于处理 sched_wakeup_new 事件,当一个新创建的进程被唤醒时触发。 +- handle_sched_switch:用于处理 sched_switch 事件,当调度器选择一个新的进程运行时触发。 + +这些入口点分别处理不同的调度事件,但都会调用 handle_switch 函数来计算进程的调度延迟并更新直方图数据。 + +最后,程序包含一个许可证声明: + +```c +char LICENSE[] SEC("license") = "GPL"; +``` +这一声明指定了 eBPF 程序的许可证类型,这里使用的是 "GPL"。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。 + +### runqlat.h + 然后我们需要定义一个头文件`runqlat.h`,用来给用户态处理从内核态上报的事件: ```c @@ -187,8 +302,6 @@ struct hist { #endif /* __RUNQLAT_H */ ``` -这是一个 Linux 内核 BPF 程序,旨在收集和报告运行队列的延迟。BPF 是 Linux 内核中一项技术,它允许将程序附加到内核中的特定点并进行安全高效的执行。这些程序可用于收集有关内核行为的信息,并实现自定义行为。这个 BPF 程序使用 BPF maps 来收集有关任务何时从内核的运行队列中排队和取消排队的信息,并记录任务在被安排执行之前在运行队列上等待的时间。然后,它使用这些信息生成直方图,显示不同组任务的运行队列延迟分布。这些直方图可用于识别和诊断内核调度行为中的性能问题。 - ## 编译运行 eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。