This commit is contained in:
Officeyutong
2024-02-22 13:14:00 +00:00
parent 403aff5b66
commit 55d5e641bf
47 changed files with 1483 additions and 1918 deletions

View File

@@ -177,7 +177,7 @@
<p>eBPF (Extended Berkeley Packet Filter) 是 Linux 内核上的一个强大的网络和性能分析工具。它允许开发者在内核运行时动态加载、更新和运行用户定义的代码。</p>
<p>runqlat 是一个 eBPF 工具,用于分析 Linux 系统的调度性能。具体来说runqlat 用于测量一个任务在被调度到 CPU 上运行之前在运行队列中等待的时间。这些信息对于识别性能瓶颈和提高 Linux 内核调度算法的整体效率非常有用。</p>
<h2 id="runqlat-原理"><a class="header" href="#runqlat-原理">runqlat 原理</a></h2>
<p>本教程是 eBPF 入门开发实践系列的第九部分,主题是 &quot;捕获进程调度延迟&quot;。在此,我们将介绍一个名为 runqlat 的程序,其作用是以直方图的形式记录进程调度延迟。</p>
<p>本教程是 eBPF 入门开发实践系列的第九部分,主题是 "捕获进程调度延迟"。在此,我们将介绍一个名为 runqlat 的程序,其作用是以直方图的形式记录进程调度延迟。</p>
<p>Linux 操作系统使用进程来执行所有的系统和用户任务。这些进程可能被阻塞、杀死、运行,或者正在等待运行。处在后两种状态的进程数量决定了 CPU 运行队列的长度。</p>
<p>进程有几种可能的状态,如:</p>
<ul>
@@ -190,7 +190,7 @@
<p>等待资源或其他函数信号的进程会处在可中断或不可中断的睡眠状态:进程被置入睡眠状态,直到它需要的资源变得可用。然后,根据睡眠的类型,进程可以转移到可运行状态,或者保持睡眠。</p>
<p>即使进程拥有它需要的所有资源它也不会立即开始运行。它会转移到可运行状态与其他处在相同状态的进程一起排队。CPU可以在接下来的几秒钟或毫秒内执行这些进程。调度器为 CPU 排列进程,并决定下一个要执行的进程。</p>
<p>根据系统的硬件配置,这个可运行队列(称为 CPU 运行队列)的长度可以短也可以长。短的运行队列长度表示 CPU 没有被充分利用。另一方面,如果运行队列长,那么可能意味着 CPU 不够强大,无法执行所有的进程,或者 CPU 的核心数量不足。在理想的 CPU 利用率下,运行队列的长度将等于系统中的核心数量。</p>
<p>进程调度延迟,也被称为 &quot;run queue latency&quot;,是衡量线程从变得可运行(例如,接收到中断,促使其处理更多工作)到实际在 CPU 上运行的时间。在 CPU 饱和的情况下,你可以想象线程必须等待其轮次。但在其他奇特的场景中,这也可能发生,而且在某些情况下,它可以通过调优减少,从而提高整个系统的性能。</p>
<p>进程调度延迟,也被称为 "run queue latency",是衡量线程从变得可运行(例如,接收到中断,促使其处理更多工作)到实际在 CPU 上运行的时间。在 CPU 饱和的情况下,你可以想象线程必须等待其轮次。但在其他奇特的场景中,这也可能发生,而且在某些情况下,它可以通过调优减少,从而提高整个系统的性能。</p>
<p>我们将通过一个示例来阐述如何使用 runqlat 工具。这是一个负载非常重的系统:</p>
<pre><code class="language-shell"># runqlat
Tracing run queue latency... Hit Ctrl-C to end.
@@ -213,7 +213,7 @@ Tracing run queue latency... Hit Ctrl-C to end.
16384 -&gt; 32767 : 809 |****************************************|
32768 -&gt; 65535 : 64 |*** |
</code></pre>
<p>在这个输出中我们看到了一个双模分布一个模在0到15微秒之间另一个模在16到65毫秒之间。这些模式在分布它仅仅是 &quot;count&quot; 列的视觉表示中显示为尖峰。例如读取一行在追踪过程中809个事件落入了16384到32767微秒的范围16到32毫秒</p>
<p>在这个输出中我们看到了一个双模分布一个模在0到15微秒之间另一个模在16到65毫秒之间。这些模式在分布它仅仅是 "count" 列的视觉表示中显示为尖峰。例如读取一行在追踪过程中809个事件落入了16384到32767微秒的范围16到32毫秒</p>
<p>在后续的教程中,我们将深入探讨如何利用 eBPF 对此类指标进行深度跟踪和分析,以更好地理解和优化系统性能。同时,我们也将学习更多关于 Linux 内核调度器、中断处理和 CPU 饱</p>
<p>runqlat 的实现利用了 eBPF 程序它通过内核跟踪点和函数探针来测量进程在运行队列中的时间。当进程被排队时trace_enqueue 函数会在一个映射中记录时间戳。当进程被调度到 CPU 上运行时handle_switch 函数会检索时间戳,并计算当前时间与排队时间之间的时间差。这个差值(或 delta被用于更新进程的直方图该直方图记录运行队列延迟的分布。该直方图可用于分析 Linux 内核的调度性能。</p>
<h2 id="runqlat-代码实现"><a class="header" href="#runqlat-代码实现">runqlat 代码实现</a></h2>
@@ -225,10 +225,10 @@ Tracing run queue latency... Hit Ctrl-C to end.
#include &lt;bpf/bpf_helpers.h&gt;
#include &lt;bpf/bpf_core_read.h&gt;
#include &lt;bpf/bpf_tracing.h&gt;
#include &quot;runqlat.h&quot;
#include &quot;bits.bpf.h&quot;
#include &quot;maps.bpf.h&quot;
#include &quot;core_fixes.bpf.h&quot;
#include "runqlat.h"
#include "bits.bpf.h"
#include "maps.bpf.h"
#include "core_fixes.bpf.h"
#define MAX_ENTRIES 10240
#define TASK_RUNNING 0
@@ -245,24 +245,24 @@ struct {
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} cgroup_map SEC(&quot;.maps&quot;);
} cgroup_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, u64);
} start SEC(&quot;.maps&quot;);
} start SEC(".maps");
static struct hist zero;
/// @sample {&quot;interval&quot;: 1000, &quot;type&quot; : &quot;log2_hist&quot;}
/// @sample {"interval": 1000, "type" : "log2_hist"}
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, struct hist);
} hists SEC(&quot;.maps&quot;);
} hists SEC(".maps");
static int trace_enqueue(u32 tgid, u32 pid)
{
@@ -346,7 +346,7 @@ cleanup:
return 0;
}
SEC(&quot;raw_tp/sched_wakeup&quot;)
SEC("raw_tp/sched_wakeup")
int BPF_PROG(handle_sched_wakeup, struct task_struct *p)
{
if (filter_cg &amp;&amp; !bpf_current_task_under_cgroup(&amp;cgroup_map, 0))
@@ -355,7 +355,7 @@ int BPF_PROG(handle_sched_wakeup, struct task_struct *p)
return trace_enqueue(BPF_CORE_READ(p, tgid), BPF_CORE_READ(p, pid));
}
SEC(&quot;raw_tp/sched_wakeup_new&quot;)
SEC("raw_tp/sched_wakeup_new")
int BPF_PROG(handle_sched_wakeup_new, struct task_struct *p)
{
if (filter_cg &amp;&amp; !bpf_current_task_under_cgroup(&amp;cgroup_map, 0))
@@ -364,13 +364,13 @@ int BPF_PROG(handle_sched_wakeup_new, struct task_struct *p)
return trace_enqueue(BPF_CORE_READ(p, tgid), BPF_CORE_READ(p, pid));
}
SEC(&quot;raw_tp/sched_switch&quot;)
SEC("raw_tp/sched_switch")
int BPF_PROG(handle_sched_switch, bool preempt, struct task_struct *prev, struct task_struct *next)
{
return handle_switch(preempt, prev, next);
}
char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
char LICENSE[] SEC("license") = "GPL";
</code></pre>
<p>这其中定义了一些常量和全局变量,用于过滤对应的追踪目标:</p>
<pre><code class="language-c">#define MAX_ENTRIES 10240
@@ -390,14 +390,14 @@ const volatile pid_t targ_tgid = 0;
__type(key, u32);
__type(value, u32);
__uint(max_entries, 1);
} cgroup_map SEC(&quot;.maps&quot;);
} cgroup_map SEC(".maps");
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, u64);
} start SEC(&quot;.maps&quot;);
} start SEC(".maps");
static struct hist zero;
@@ -406,7 +406,7 @@ struct {
__uint(max_entries, MAX_ENTRIES);
__type(key, u32);
__type(value, struct hist);
} hists SEC(&quot;.maps&quot;);
} hists SEC(".maps");
</code></pre>
<p>这些映射包括:</p>
<ul>
@@ -464,9 +464,9 @@ struct {
</ul>
<p>这些入口点分别处理不同的调度事件,但都会调用 handle_switch 函数来计算进程的调度延迟并更新直方图数据。</p>
<p>最后,程序包含一个许可证声明:</p>
<pre><code class="language-c">char LICENSE[] SEC(&quot;license&quot;) = &quot;GPL&quot;;
<pre><code class="language-c">char LICENSE[] SEC("license") = "GPL";
</code></pre>
<p>这一声明指定了 eBPF 程序的许可证类型,这里使用的是 &quot;GPL&quot;。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。</p>
<p>这一声明指定了 eBPF 程序的许可证类型,这里使用的是 "GPL"。这对于许多内核功能是必需的,因为它们要求 eBPF 程序遵循 GPL 许可证。</p>
<h3 id="runqlath"><a class="header" href="#runqlath">runqlat.h</a></h3>
<p>然后我们需要定义一个头文件<code>runqlat.h</code>,用来给用户态处理从内核态上报的事件:</p>
<pre><code class="language-c">/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */