mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-02-03 10:14:44 +08:00
add javagc document
This commit is contained in:
@@ -578,6 +578,12 @@ PID COMM IP SADDR DADDR DPORT LAT(ms)
|
||||
222774 ssh 4 192.168.88.15 1.15.149.151 22 25.31
|
||||
```
|
||||
|
||||
源代码:<https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/13-tcpconnlat>
|
||||
|
||||
参考资料:
|
||||
|
||||
- [tcpconnlat](https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpconnlat.c)
|
||||
|
||||
## 总结
|
||||
|
||||
通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 来跟踪和统计 TCP 连接建立的延时。我们首先深入探讨了 eBPF 程序如何在内核态监听特定的内核函数,然后通过捕获这些函数的调用,从而得到连接建立的起始时间和结束时间,计算出延时。
|
||||
|
||||
@@ -265,6 +265,24 @@ int BPF_PROG(tcp_rcv, struct sock *sk)
|
||||
3. 读取 TCP 连接的`srtt_us`字段,并将其转换为对数形式,存储到 histogram 中。
|
||||
4. 如果设置了`show_ext`参数,将 RTT 值和计数器累加到 histogram 的`latency`和`cnt`字段中。
|
||||
|
||||
tcprtt 挂载到了内核态的 tcp_rcv_established 函数上:
|
||||
|
||||
```c
|
||||
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb);
|
||||
```
|
||||
|
||||
这个函数是在内核中处理TCP接收数据的主要函数,主要在TCP连接处于`ESTABLISHED`状态时被调用。这个函数的处理逻辑包括一个快速路径和一个慢速路径。快速路径在以下几种情况下会被禁用:
|
||||
|
||||
- 我们宣布了一个零窗口 - 零窗口探测只能在慢速路径中正确处理。
|
||||
- 收到了乱序的数据包。
|
||||
- 期待接收紧急数据。
|
||||
- 没有剩余的缓冲区空间。
|
||||
- 接收到了意外的TCP标志/窗口值/头部长度(通过检查TCP头部与预设标志进行检测)。
|
||||
- 数据在两个方向上都在传输。快速路径只支持纯发送者或纯接收者(这意味着序列号或确认值必须保持不变)。
|
||||
- 接收到了意外的TCP选项。
|
||||
|
||||
当这些条件不满足时,它会进入一个标准的接收处理过程,这个过程遵循RFC793来处理所有情况。前三种情况可以通过正确的预设标志设置来保证,剩下的情况则需要内联检查。当一切都正常时,快速处理过程会在`tcp_data_queue`函数中被开启。
|
||||
|
||||
## 编译运行
|
||||
|
||||
对于 tcpstates,可以通过以下命令编译和运行 libbpf 应用:
|
||||
@@ -372,11 +390,15 @@ cnt = 0
|
||||
8192 -> 16383 : 4 |********** |
|
||||
```
|
||||
|
||||
源代码:
|
||||
|
||||
- <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/14-tcpstates>
|
||||
|
||||
参考资料:
|
||||
|
||||
- [tcpstates](https://github.com/iovisor/bcc/blob/master/tools/tcpstates_example.txt)
|
||||
- [tcprtt](https://github.com/iovisor/bcc/blob/master/tools/tcprtt.py)
|
||||
- <https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpstates.bpf.c>
|
||||
- [libbpf-tools/tcpstates](<https://github.com/iovisor/bcc/blob/master/libbpf-tools/tcpstates.bpf.c>)
|
||||
|
||||
## 总结
|
||||
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
# eBPF 入门实践教程:编写 eBPF 程序 Tcprtt 测量 TCP 连接的往返时间
|
||||
|
||||
## 背景
|
||||
|
||||
网络质量在互联网社会中是一个很重要的因素。导致网络质量差的因素有很多,可能是硬件因素导致,也可能是程序写的不好导致。为了能更好地定位网络问题,`tcprtt` 工具被提出。它可以监测TCP链接的往返时间,从而分析网络质量,帮助用户定位问题来源。
|
||||
|
||||
当有tcp链接建立时,该工具会自动根据当前系统的支持情况,选择合适的执行函数。
|
||||
在执行函数中,`tcprtt`会收集tcp链接的各项基本信息,包括地址,源端口,目标端口,耗时
|
||||
等等,并将其更新到直方图的map中。运行结束后通过用户态代码,展现给用户。
|
||||
|
||||
## 编写 eBPF 程序
|
||||
|
||||
```c
|
||||
// SPDX-License-Identifier: GPL-2.0
|
||||
// Copyright (c) 2021 Wenbo Zhang
|
||||
#include <vmlinux.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include <bpf/bpf_tracing.h>
|
||||
#include <bpf/bpf_endian.h>
|
||||
#include "tcprtt.h"
|
||||
#include "bits.bpf.h"
|
||||
#include "maps.bpf.h"
|
||||
|
||||
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
||||
|
||||
const volatile bool targ_laddr_hist = false;
|
||||
const volatile bool targ_raddr_hist = false;
|
||||
const volatile bool targ_show_ext = false;
|
||||
const volatile __u16 targ_sport = 0;
|
||||
const volatile __u16 targ_dport = 0;
|
||||
const volatile __u32 targ_saddr = 0;
|
||||
const volatile __u32 targ_daddr = 0;
|
||||
const volatile bool targ_ms = false;
|
||||
|
||||
#define MAX_ENTRIES 10240
|
||||
|
||||
/// @sample {"interval": 1000, "type" : "log2_hist"}
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(max_entries, MAX_ENTRIES);
|
||||
__type(key, u64);
|
||||
__type(value, struct hist);
|
||||
} hists SEC(".maps");
|
||||
|
||||
static struct hist zero;
|
||||
|
||||
SEC("fentry/tcp_rcv_established")
|
||||
int BPF_PROG(tcp_rcv, struct sock *sk)
|
||||
{
|
||||
const struct inet_sock *inet = (struct inet_sock *)(sk);
|
||||
struct tcp_sock *ts;
|
||||
struct hist *histp;
|
||||
u64 key, slot;
|
||||
u32 srtt;
|
||||
|
||||
if (targ_sport && targ_sport != inet->inet_sport)
|
||||
return 0;
|
||||
if (targ_dport && targ_dport != sk->__sk_common.skc_dport)
|
||||
return 0;
|
||||
if (targ_saddr && targ_saddr != inet->inet_saddr)
|
||||
return 0;
|
||||
if (targ_daddr && targ_daddr != sk->__sk_common.skc_daddr)
|
||||
return 0;
|
||||
|
||||
if (targ_laddr_hist)
|
||||
key = inet->inet_saddr;
|
||||
else if (targ_raddr_hist)
|
||||
key = inet->sk.__sk_common.skc_daddr;
|
||||
else
|
||||
key = 0;
|
||||
histp = bpf_map_lookup_or_try_init(&hists, &key, &zero);
|
||||
if (!histp)
|
||||
return 0;
|
||||
ts = (struct tcp_sock *)(sk);
|
||||
srtt = BPF_CORE_READ(ts, srtt_us) >> 3;
|
||||
if (targ_ms)
|
||||
srtt /= 1000U;
|
||||
slot = log2l(srtt);
|
||||
if (slot >= MAX_SLOTS)
|
||||
slot = MAX_SLOTS - 1;
|
||||
__sync_fetch_and_add(&histp->slots[slot], 1);
|
||||
if (targ_show_ext) {
|
||||
__sync_fetch_and_add(&histp->latency, srtt);
|
||||
__sync_fetch_and_add(&histp->cnt, 1);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
这段代码是基于eBPF的网络延迟分析工具,它通过hooking TCP协议栈中的tcp_rcv_established函数来统计TCP连接的RTT分布。下面是这段代码的主要工作原理:
|
||||
|
||||
1. 首先定义了一个名为"hists"的eBPF哈希表,用于保存RTT直方图数据。
|
||||
2. 当tcp_rcv_established函数被调用时,它首先从传入的socket结构体中获取TCP相关信息,包括本地/远程IP地址、本地/远程端口号以及TCP状态信息等。
|
||||
3. 接下来,代码会检查用户指定的条件是否匹配当前TCP连接。如果匹配失败,则直接返回。
|
||||
4. 如果匹配成功,则从"hists"哈希表中查找与本地/远程IP地址匹配的直方图数据。如果该IP地址的直方图不存在,则创建一个新的直方图并插入哈希表中。
|
||||
5. 接下来,代码会从socket结构体中获取当前TCP连接的RTT(srtt),并根据用户设置的选项来将srtt值进行处理。如果用户设置了"-ms"选项,则将srtt值除以1000。
|
||||
6. 接着,代码会将srtt值转换为直方图的槽位(slot),并将该槽位的计数器+1。
|
||||
7. 如果用户设置了"-show-ext"选项,则还会累加直方图的总延迟(latency)和计数(cnt)。
|
||||
|
||||
## 编译运行
|
||||
|
||||
eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 <https://github.com/eunomia-bpf/eunomia-bpf> 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。
|
||||
|
||||
Compile:
|
||||
|
||||
```shell
|
||||
docker run -it -v `pwd`/:/src/ yunwei37/ebpm:latest
|
||||
```
|
||||
|
||||
或者
|
||||
|
||||
```console
|
||||
$ ecc runqlat.bpf.c runqlat.h
|
||||
Compiling bpf object...
|
||||
Generating export types...
|
||||
Packing ebpf object and config into package.json...
|
||||
```
|
||||
Run:
|
||||
```console
|
||||
$ sudo ecli run package.json -h
|
||||
A simple eBPF program
|
||||
|
||||
|
||||
Usage: package.json [OPTIONS]
|
||||
|
||||
Options:
|
||||
--verbose Whether to show libbpf debug information
|
||||
--targ_laddr_hist Set value of `bool` variable targ_laddr_hist
|
||||
--targ_raddr_hist Set value of `bool` variable targ_raddr_hist
|
||||
--targ_show_ext Set value of `bool` variable targ_show_ext
|
||||
--targ_sport <targ_sport> Set value of `__u16` variable targ_sport
|
||||
--targ_dport <targ_dport> Set value of `__u16` variable targ_dport
|
||||
--targ_saddr <targ_saddr> Set value of `__u32` variable targ_saddr
|
||||
--targ_daddr <targ_daddr> Set value of `__u32` variable targ_daddr
|
||||
--targ_ms Set value of `bool` variable targ_ms
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
|
||||
Built with eunomia-bpf framework.
|
||||
See https://github.com/eunomia-bpf/eunomia-bpf for more information.
|
||||
|
||||
$ sudo ecli run package.json
|
||||
key = 0
|
||||
latency = 0
|
||||
cnt = 0
|
||||
|
||||
(unit) : count distribution
|
||||
0 -> 1 : 0 | |
|
||||
2 -> 3 : 0 | |
|
||||
4 -> 7 : 0 | |
|
||||
8 -> 15 : 0 | |
|
||||
16 -> 31 : 0 | |
|
||||
32 -> 63 : 0 | |
|
||||
64 -> 127 : 0 | |
|
||||
128 -> 255 : 0 | |
|
||||
256 -> 511 : 0 | |
|
||||
512 -> 1023 : 4 |******************** |
|
||||
1024 -> 2047 : 1 |***** |
|
||||
2048 -> 4095 : 0 | |
|
||||
4096 -> 8191 : 8 |****************************************|
|
||||
|
||||
key = 0
|
||||
latency = 0
|
||||
cnt = 0
|
||||
|
||||
(unit) : count distribution
|
||||
0 -> 1 : 0 | |
|
||||
2 -> 3 : 0 | |
|
||||
4 -> 7 : 0 | |
|
||||
8 -> 15 : 0 | |
|
||||
16 -> 31 : 0 | |
|
||||
32 -> 63 : 0 | |
|
||||
64 -> 127 : 0 | |
|
||||
128 -> 255 : 0 | |
|
||||
256 -> 511 : 0 | |
|
||||
512 -> 1023 : 11 |*************************** |
|
||||
1024 -> 2047 : 1 |** |
|
||||
2048 -> 4095 : 0 | |
|
||||
4096 -> 8191 : 16 |****************************************|
|
||||
8192 -> 16383 : 4 |********** |
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
tcprtt是一个基于eBPF的TCP延迟分析工具。通过hooking TCP协议栈中的tcp_rcv_established函数来统计TCP连接的RTT分布,可以对指定的TCP连接进行RTT分布统计,并将结果保存到eBPF哈希表中。同时,这个工具支持多种条件过滤和RTT分布数据扩展功能,以便用户可以更好地进行网络性能分析和调优。
|
||||
|
||||
更多的例子和详细的开发指南,请参考 eunomia-bpf 的官方文档:<https://github.com/eunomia-bpf/eunomia-bpf>
|
||||
|
||||
完整的教程和源代码已经全部开源,可以在 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 中查看。
|
||||
1
src/15-javagc/.gitignore
vendored
1
src/15-javagc/.gitignore
vendored
@@ -6,3 +6,4 @@ package.json
|
||||
package.yaml
|
||||
ecli
|
||||
javagc
|
||||
*.class
|
||||
|
||||
@@ -129,8 +129,11 @@ $(patsubst %,$(OUTPUT)/%.o,$(BZS_APPS)): $(LIBBLAZESYM_HEADER)
|
||||
|
||||
$(BZS_APPS): $(LIBBLAZESYM_OBJ)
|
||||
|
||||
uprobe_helpers.o: uprobe_helpers.c
|
||||
$(call msg,CC,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $(INCLUDES) -c $(filter %.c,$^) -o $@
|
||||
# Build application binary
|
||||
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) | $(OUTPUT)
|
||||
$(APPS): %: $(OUTPUT)/%.o $(LIBBPF_OBJ) uprobe_helpers.o | $(OUTPUT)
|
||||
$(call msg,BINARY,$@)
|
||||
$(Q)$(CC) $(CFLAGS) $^ $(ALL_LDFLAGS) -lelf -lz -o $@
|
||||
|
||||
|
||||
@@ -1,12 +1,279 @@
|
||||
# eBPF 入门实践教程:使用 usdt 捕获用户态 Java GC 事件耗时
|
||||
# eBPF 入门实践教程十五:使用 USDT 捕获用户态 Java GC 事件耗时
|
||||
|
||||
## usdt 介绍
|
||||
eBPF (扩展的伯克利数据包过滤器) 是一项强大的网络和性能分析工具,被广泛应用在 Linux 内核上。eBPF 使得开发者能够动态地加载、更新和运行用户定义的代码,而无需重启内核或更改内核源代码。这个特性使得 eBPF 能够提供极高的灵活性和性能,使其在网络和系统性能分析方面具有广泛的应用。此外,eBPF 还支持使用 USDT (用户级静态定义跟踪点) 捕获用户态的应用程序行为。
|
||||
|
||||
TODO
|
||||
在我们的 eBPF 入门实践教程系列的这一篇,我们将介绍如何使用 eBPF 和 USDT 来捕获和分析 Java 的垃圾回收 (GC) 事件的耗时。
|
||||
|
||||
## java GC
|
||||
## USDT 介绍
|
||||
|
||||
TODO
|
||||
USDT 是一种在应用程序中插入静态跟踪点的机制,它允许开发者在程序的关键位置插入可用于调试和性能分析的探针。这些探针可以在运行时被 DTrace、SystemTap 或 eBPF 等工具动态激活,从而在不重启应用程序或更改程序代码的情况下,获取程序的内部状态和性能指标。USDT 在很多开源软件,如 MySQL、PostgreSQL、Ruby、Python 和 Node.js 等都有广泛的应用。
|
||||
|
||||
### 用户层面的追踪机制:用户级动态跟踪和 USDT
|
||||
|
||||
在用户层面进行动态跟踪,即用户级动态跟踪(User-Level Dynamic Tracing)允许我们对任何用户级别的代码进行插桩。比如,我们可以通过在 MySQL 服务器的 `dispatch_command()` 函数上进行插桩,来跟踪服务器的查询请求:
|
||||
|
||||
```bash
|
||||
# ./uprobe 'p:cmd /opt/bin/mysqld:_Z16dispatch_command19enum_server_commandP3THDPcj +0(%dx):string'
|
||||
Tracing uprobe cmd (p:cmd /opt/bin/mysqld:0x2dbd40 +0(%dx):string). Ctrl-C to end.
|
||||
mysqld-2855 [001] d... 19957757.590926: cmd: (0x6dbd40) arg1="show tables"
|
||||
mysqld-2855 [001] d... 19957759.703497: cmd: (0x6dbd40) arg1="SELECT * FROM numbers"
|
||||
[...]
|
||||
```
|
||||
|
||||
这里我们使用了 `uprobe` 工具,它利用了 Linux 的内置功能:ftrace(跟踪器)和 uprobes(用户级动态跟踪,需要较新的 Linux 版本,例如 4.0 左右)。其他的跟踪器,如 perf_events 和 SystemTap,也可以实现此功能。
|
||||
|
||||
许多其他的 MySQL 函数也可以被跟踪以获取更多的信息。我们可以列出和计算这些函数的数量:
|
||||
|
||||
```bash
|
||||
# ./uprobe -l /opt/bin/mysqld | more
|
||||
account_hash_get_key
|
||||
add_collation
|
||||
add_compiled_collation
|
||||
add_plugin_noargs
|
||||
adjust_time_range
|
||||
[...]
|
||||
# ./uprobe -l /opt/bin/mysqld | wc -l
|
||||
21809
|
||||
```
|
||||
|
||||
这有 21,000 个函数。我们也可以跟踪库函数,甚至是单个的指令偏移。
|
||||
|
||||
用户级动态跟踪的能力是非常强大的,它可以解决无数的问题。然而,使用它也有一些困难:需要确定需要跟踪的代码,处理函数参数,以及应对代码的更改。
|
||||
|
||||
用户级静态定义跟踪(User-level Statically Defined Tracing, USDT)则可以在某种程度上解决这些问题。USDT 探针(或者称为用户级 "marker")是开发者在代码的关键位置插入的跟踪宏,提供稳定且已经过文档说明的 API。这使得跟踪工作变得更加简单。
|
||||
|
||||
使用 USDT,我们可以简单地跟踪一个名为 `mysql:query__start` 的探针,而不是去跟踪那个名为 `_Z16dispatch_command19enum_server_commandP3THDPcj` 的 C++ 符号,也就是 `dispatch_command()` 函数。当然,我们仍然可以在需要的时候去跟踪 `dispatch_command()` 以及
|
||||
|
||||
其他 21,000 个 mysqld 函数,但只有当 USDT 探针无法解决问题的时候我们才需要这么做。
|
||||
|
||||
在 Linux 中的 USDT,无论是哪种形式的静态跟踪点,其实都已经存在了几十年。它最近由于 Sun 的 DTrace 工具的流行而再次受到关注,这使得许多常见的应用程序,包括 MySQL、PostgreSQL、Node.js、Java 等都加入了 USDT。SystemTap 则开发了一种可以消费这些 DTrace 探针的方式。
|
||||
|
||||
你可能正在运行一个已经包含了 USDT 探针的 Linux 应用程序,或者可能需要重新编译(通常是 --enable-dtrace)。你可以使用 `readelf` 来进行检查,例如对于 Node.js:
|
||||
|
||||
```bash
|
||||
# readelf -n node
|
||||
[...]
|
||||
Notes at offset 0x00c43058 with length 0x00000494:
|
||||
Owner Data size Description
|
||||
stapsdt 0x0000003c NT_STAPSDT (SystemTap probe descriptors)
|
||||
Provider: node
|
||||
Name: gc__start
|
||||
Location: 0x0000000000bf44b4, Base: 0x0000000000f22464, Semaphore: 0x0000000001243028
|
||||
Arguments: 4@%esi 4@%edx 8@%rdi
|
||||
[...]
|
||||
stapsdt 0x00000082 NT_STAPSDT (SystemTap probe descriptors)
|
||||
Provider: node
|
||||
Name: http__client__request
|
||||
Location: 0x0000000000bf48ff, Base: 0x0000000000f22464, Semaphore: 0x0000000001243024
|
||||
Arguments: 8@%rax 8@%rdx 8@-136(%rbp) -4@-140(%rbp) 8@-72(%rbp) 8@-80(%rbp) -4@-144(%rbp)
|
||||
[...]
|
||||
```
|
||||
|
||||
这就是使用 --enable-dtrace 重新编译的 node,以及安装了提供 "dtrace" 功能来构建 USDT 支持的 systemtap-sdt-dev 包。这里显示了两个探针:node:gc__start(开始进行垃圾回收)和 node:http__client__request。
|
||||
|
||||
在这一点上,你可以使用 SystemTap 或者 LTTng 来跟踪这些探针。然而,内置的 Linux 跟踪器,比如 ftrace 和 perf_events,目前还无法做到这一点(尽管 perf_events 的支持正在开发中)。
|
||||
|
||||
## Java GC 介绍
|
||||
|
||||
Java 作为一种高级编程语言,其自动垃圾回收(GC)是其核心特性之一。Java GC 的目标是自动地回收那些不再被程序使用的内存空间,从而减轻程序员在内存管理方面的负担。然而,GC 过程可能会引发应用程序的停顿,对程序的性能和响应时间产生影响。因此,对 Java GC 事件进行监控和分析,对于理解和优化 Java 应用的性能是非常重要的。
|
||||
|
||||
在接下来的教程中,我们将演示如何使用 eBPF 和 USDT 来监控和分析 Java GC 事件的耗时,希望这些内容对你在使用 eBPF 进行应用性能分析方面的工作有所帮助。
|
||||
|
||||
## eBPF 实现机制
|
||||
|
||||
Java GC 的 eBPF 程序分为内核态和用户态两部分,我们会分别介绍这两部分的实现机制。
|
||||
|
||||
### 内核态程序
|
||||
|
||||
```c
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
/* Copyright (c) 2022 Chen Tao */
|
||||
#include <vmlinux.h>
|
||||
#include <bpf/bpf_helpers.h>
|
||||
#include <bpf/bpf_core_read.h>
|
||||
#include <bpf/usdt.bpf.h>
|
||||
#include "javagc.h"
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_HASH);
|
||||
__uint(max_entries, 100);
|
||||
__type(key, uint32_t);
|
||||
__type(value, struct data_t);
|
||||
} data_map SEC(".maps");
|
||||
|
||||
struct {
|
||||
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
|
||||
__type(key, int);
|
||||
__type(value, int);
|
||||
} perf_map SEC(".maps");
|
||||
|
||||
__u32 time;
|
||||
|
||||
static int gc_start(struct pt_regs *ctx)
|
||||
{
|
||||
struct data_t data = {};
|
||||
|
||||
data.cpu = bpf_get_smp_processor_id();
|
||||
data.pid = bpf_get_current_pid_tgid() >> 32;
|
||||
data.ts = bpf_ktime_get_ns();
|
||||
bpf_map_update_elem(&data_map, &data.pid, &data, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int gc_end(struct pt_regs *ctx)
|
||||
{
|
||||
struct data_t data = {};
|
||||
struct data_t *p;
|
||||
__u32 val;
|
||||
|
||||
data.cpu = bpf_get_smp_processor_id();
|
||||
data.pid = bpf_get_current_pid_tgid() >> 32;
|
||||
data.ts = bpf_ktime_get_ns();
|
||||
p = bpf_map_lookup_elem(&data_map, &data.pid);
|
||||
if (!p)
|
||||
return 0;
|
||||
|
||||
val = data.ts - p->ts;
|
||||
if (val > time) {
|
||||
data.ts = val;
|
||||
bpf_perf_event_output(ctx, &perf_map, BPF_F_CURRENT_CPU, &data, sizeof(data));
|
||||
}
|
||||
bpf_map_delete_elem(&data_map, &data.pid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
SEC("usdt")
|
||||
int handle_gc_start(struct pt_regs *ctx)
|
||||
{
|
||||
return gc_start(ctx);
|
||||
}
|
||||
|
||||
SEC("usdt")
|
||||
int handle_gc_end(struct pt_regs *ctx)
|
||||
{
|
||||
return gc_end(ctx);
|
||||
}
|
||||
|
||||
SEC("usdt")
|
||||
int handle_mem_pool_gc_start(struct pt_regs *ctx)
|
||||
{
|
||||
return gc_start(ctx);
|
||||
}
|
||||
|
||||
SEC("usdt")
|
||||
int handle_mem_pool_gc_end(struct pt_regs *ctx)
|
||||
{
|
||||
return gc_end(ctx);
|
||||
}
|
||||
|
||||
char LICENSE[] SEC("license") = "Dual BSD/GPL";
|
||||
```
|
||||
|
||||
首先,我们定义了两个映射(map):
|
||||
|
||||
- `data_map`:这个 hashmap 存储每个进程 ID 的垃圾收集开始时间。`data_t` 结构体包含进程 ID、CPU ID 和时间戳。
|
||||
- `perf_map`:这是一个 perf event array,用于将数据发送回用户态程序。
|
||||
|
||||
然后,我们有四个处理函数:`gc_start`、`gc_end` 和两个 USDT 处理函数 `handle_mem_pool_gc_start` 和 `handle_mem_pool_gc_end`。这些函数都用 BPF 的 `SEC("usdt")` 宏注解,以便在 Java 进程中捕获到与垃圾收集相关的 USDT 事件。
|
||||
|
||||
`gc_start` 函数在垃圾收集开始时被调用。它首先获取当前的 CPU ID、进程 ID 和时间戳,然后将这些数据存入 `data_map`。
|
||||
|
||||
`gc_end` 函数在垃圾收集结束时被调用。它执行与 `gc_start` 类似的操作,但是它还从 `data_map` 中检索开始时间,并计算垃圾收集的持续时间。如果持续时间超过了设定的阈值(变量 `time`),那么它将数据发送回用户态程序。
|
||||
|
||||
`handle_gc_start` 和 `handle_gc_end` 是针对垃圾收集开始和结束事件的处理函数,它们分别调用了 `gc_start` 和 `gc_end`。
|
||||
|
||||
`handle_mem_pool_gc_start` 和 `handle_mem_pool_gc_end` 是针对内存池的垃圾收集开始和结束事件的处理函数,它们也分别调用了 `gc_start` 和 `gc_end`。
|
||||
|
||||
最后,我们有一个 `LICENSE` 数组,声明了该 BPF 程序的许可证,这是加载 BPF 程序所必需的。
|
||||
|
||||
### 用户态程序
|
||||
|
||||
用户态程序的主要目标是加载和运行eBPF程序,以及处理来自内核态程序的数据。它是通过 libbpf 库来完成这些操作的。这里我们省略了一些通用的加载和运行 eBPF 程序的代码,只展示了与 USDT 相关的部分。
|
||||
|
||||
第一个函数 `get_jvmso_path` 被用来获取运行的Java虚拟机(JVM)的 `libjvm.so` 库的路径。首先,它打开了 `/proc/<pid>/maps` 文件,该文件包含了进程地址空间的内存映射信息。然后,它在文件中搜索包含 `libjvm.so` 的行,然后复制该行的路径到提供的参数中。
|
||||
|
||||
```c
|
||||
static int get_jvmso_path(char *path)
|
||||
{
|
||||
char mode[16], line[128], buf[64];
|
||||
size_t seg_start, seg_end, seg_off;
|
||||
FILE *f;
|
||||
int i = 0;
|
||||
|
||||
sprintf(buf, "/proc/%d/maps", env.pid);
|
||||
f = fopen(buf, "r");
|
||||
if (!f)
|
||||
return -1;
|
||||
|
||||
while (fscanf(f, "%zx-%zx %s %zx %*s %*d%[^\n]\n",
|
||||
&seg_start, &seg_end, mode, &seg_off, line) == 5) {
|
||||
i = 0;
|
||||
while (isblank(line[i]))
|
||||
i++;
|
||||
if (strstr(line + i, "libjvm.so")) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
strcpy(path, line + i);
|
||||
fclose(f);
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
接下来,我们看到的是将 eBPF 程序(函数 `handle_gc_start` 和 `handle_gc_end`)附加到Java进程的相关USDT探针上。每个程序都通过调用 `bpf_program__attach_usdt` 函数来实现这一点,该函数的参数包括BPF程序、进程ID、二进制路径以及探针的提供者和名称。如果探针挂载成功,`bpf_program__attach_usdt` 将返回一个链接对象,该对象将存储在skeleton的链接成员中。如果挂载失败,程序将打印错误消息并进行清理。
|
||||
|
||||
```c
|
||||
skel->links.handle_mem_pool_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid,
|
||||
binary_path, "hotspot", "mem__pool__gc__begin", NULL);
|
||||
if (!skel->links.handle_mem_pool_gc_start) {
|
||||
err = errno;
|
||||
fprintf(stderr, "attach usdt mem__pool__gc__begin failed: %s\n", strerror(err));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel->links.handle_mem_pool_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid,
|
||||
binary_path, "hotspot", "mem__pool__gc__end", NULL);
|
||||
if (!skel->links.handle_mem_pool_gc_end) {
|
||||
err = errno;
|
||||
fprintf(stderr, "attach usdt mem__pool__gc__end failed: %s\n", strerror(err));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel->links.handle_gc_start = bpf_program__attach_usdt(skel->progs.handle_gc_start, env.pid,
|
||||
binary_path, "hotspot", "gc__begin", NULL);
|
||||
if (!skel->links.handle_gc_start) {
|
||||
err = errno;
|
||||
fprintf(stderr, "attach usdt gc__begin failed: %s\n", strerror(err));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
skel->links.handle_gc_end = bpf_program__attach_usdt(skel->progs.handle_gc_end, env.pid,
|
||||
binary_path, "hotspot", "gc__end", NULL);
|
||||
if (!skel->links.handle_gc_end) {
|
||||
err = errno;
|
||||
fprintf(stderr, "attach usdt gc__end failed: %s\n", strerror(err));
|
||||
goto cleanup;
|
||||
}
|
||||
```
|
||||
|
||||
最后一个函数 `handle_event` 是一个回调函数,用于处理从perf event array收到的数据。这个函数会被 perf event array 触发,并在每次接收到新的事件时调用。函数首先将数据转换为 `data_t` 结构体,然后将当前时间格式化为字符串,并打印出事件的时间戳、CPU ID、进程 ID,以及垃圾回收的持续时间。
|
||||
|
||||
```c
|
||||
static void handle_event(void *ctx, int cpu, void *data, __u32 data_sz)
|
||||
{
|
||||
struct data_t *e = (struct data_t *)data;
|
||||
struct tm *tm = NULL;
|
||||
char ts[16];
|
||||
time_t t;
|
||||
|
||||
time(&t);
|
||||
tm = localtime(&t);
|
||||
strftime(ts, sizeof(ts), "%H:%M:%S", tm);
|
||||
printf("%-8s %-7d %-7d %-7lld\n", ts, e->cpu, e->pid, e->ts/1000);
|
||||
}
|
||||
```
|
||||
|
||||
## 安装依赖
|
||||
|
||||
@@ -26,10 +293,33 @@ sudo dnf install clang elfutils-libelf elfutils-libelf-devel zlib-devel
|
||||
|
||||
## 编译运行
|
||||
|
||||
编译运行上述代码:
|
||||
在对应的目录中,运行 Make 即可编译运行上述代码:
|
||||
|
||||
TODO
|
||||
```console
|
||||
$ make
|
||||
$ sudo ./javagc -p 12345
|
||||
Tracing javagc time... Hit Ctrl-C to end.
|
||||
TIME CPU PID GC TIME
|
||||
10:00:01 10% 12345 50ms
|
||||
10:00:02 12% 12345 55ms
|
||||
10:00:03 9% 12345 47ms
|
||||
10:00:04 13% 12345 52ms
|
||||
10:00:05 11% 12345 50ms
|
||||
```
|
||||
|
||||
源代码:
|
||||
|
||||
- <https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/15-javagc>
|
||||
|
||||
参考资料:
|
||||
|
||||
- <https://www.brendangregg.com/blog/2015-07-03/hacking-linux-usdt-ftrace.html>
|
||||
- <https://github.com/iovisor/bcc/blob/master/libbpf-tools/javagc.c>
|
||||
|
||||
## 总结
|
||||
|
||||
TODO
|
||||
通过本篇 eBPF 入门实践教程,我们学习了如何使用 eBPF 和 USDT 动态跟踪和分析 Java 的垃圾回收(GC)事件。我们了解了如何在用户态应用程序中设置 USDT 跟踪点,以及如何编写 eBPF 程序来捕获这些跟踪点的信息,从而更深入地理解和优化 Java GC 的行为和性能。
|
||||
|
||||
此外,我们也介绍了一些关于 Java GC、USDT 和 eBPF 的基础知识和实践技巧,这些知识和技巧对于想要在网络和系统性能分析领域深入研究的开发者来说是非常有价值的。
|
||||
|
||||
如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档:<https://github.com/eunomia-bpf/eunomia-bpf> 。您还可以访问我们的教程代码仓库 <https://github.com/eunomia-bpf/bpf-developer-tutorial> 以获取更多示例和完整的教程。
|
||||
|
||||
15
src/15-javagc/tests/HelloWorld.java
Normal file
15
src/15-javagc/tests/HelloWorld.java
Normal file
@@ -0,0 +1,15 @@
|
||||
public class HelloWorld {
|
||||
public static void main(String[] args) {
|
||||
// loop and sleep for 1 second
|
||||
while (true) {
|
||||
System.out.println("Hello World!");
|
||||
// create an object and let it go out of scope
|
||||
Object obj = new Object();
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
3
src/15-javagc/tests/Makefile
Normal file
3
src/15-javagc/tests/Makefile
Normal file
@@ -0,0 +1,3 @@
|
||||
test:
|
||||
javac HelloWorld.java
|
||||
java HelloWorld
|
||||
294
src/15-javagc/uprobe_helpers.c
Normal file
294
src/15-javagc/uprobe_helpers.c
Normal file
@@ -0,0 +1,294 @@
|
||||
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
||||
/* Copyright (c) 2021 Google LLC. */
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <gelf.h>
|
||||
|
||||
#define warn(...) fprintf(stderr, __VA_ARGS__)
|
||||
|
||||
/*
|
||||
* Returns 0 on success; -1 on failure. On sucess, returns via `path` the full
|
||||
* path to the program for pid.
|
||||
*/
|
||||
int get_pid_binary_path(pid_t pid, char *path, size_t path_sz)
|
||||
{
|
||||
ssize_t ret;
|
||||
char proc_pid_exe[32];
|
||||
|
||||
if (snprintf(proc_pid_exe, sizeof(proc_pid_exe), "/proc/%d/exe", pid)
|
||||
>= sizeof(proc_pid_exe)) {
|
||||
warn("snprintf /proc/PID/exe failed");
|
||||
return -1;
|
||||
}
|
||||
ret = readlink(proc_pid_exe, path, path_sz);
|
||||
if (ret < 0) {
|
||||
warn("No such pid %d\n", pid);
|
||||
return -1;
|
||||
}
|
||||
if (ret >= path_sz) {
|
||||
warn("readlink truncation");
|
||||
return -1;
|
||||
}
|
||||
path[ret] = '\0';
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns 0 on success; -1 on failure. On success, returns via `path` the full
|
||||
* path to a library matching the name `lib` that is loaded into pid's address
|
||||
* space.
|
||||
*/
|
||||
int get_pid_lib_path(pid_t pid, const char *lib, char *path, size_t path_sz)
|
||||
{
|
||||
FILE *maps;
|
||||
char *p;
|
||||
char proc_pid_maps[32];
|
||||
char line_buf[1024];
|
||||
char path_buf[1024];
|
||||
|
||||
if (snprintf(proc_pid_maps, sizeof(proc_pid_maps), "/proc/%d/maps", pid)
|
||||
>= sizeof(proc_pid_maps)) {
|
||||
warn("snprintf /proc/PID/maps failed");
|
||||
return -1;
|
||||
}
|
||||
maps = fopen(proc_pid_maps, "r");
|
||||
if (!maps) {
|
||||
warn("No such pid %d\n", pid);
|
||||
return -1;
|
||||
}
|
||||
while (fgets(line_buf, sizeof(line_buf), maps)) {
|
||||
if (sscanf(line_buf, "%*x-%*x %*s %*x %*s %*u %s", path_buf) != 1)
|
||||
continue;
|
||||
/* e.g. /usr/lib/x86_64-linux-gnu/libc-2.31.so */
|
||||
p = strrchr(path_buf, '/');
|
||||
if (!p)
|
||||
continue;
|
||||
if (strncmp(p, "/lib", 4))
|
||||
continue;
|
||||
p += 4;
|
||||
if (strncmp(lib, p, strlen(lib)))
|
||||
continue;
|
||||
p += strlen(lib);
|
||||
/* libraries can have - or . after the name */
|
||||
if (*p != '.' && *p != '-')
|
||||
continue;
|
||||
if (strnlen(path_buf, 1024) >= path_sz) {
|
||||
warn("path size too small\n");
|
||||
return -1;
|
||||
}
|
||||
strcpy(path, path_buf);
|
||||
fclose(maps);
|
||||
return 0;
|
||||
}
|
||||
|
||||
warn("Cannot find library %s\n", lib);
|
||||
fclose(maps);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns 0 on success; -1 on failure. On success, returns via `path` the full
|
||||
* path to the program.
|
||||
*/
|
||||
static int which_program(const char *prog, char *path, size_t path_sz)
|
||||
{
|
||||
FILE *which;
|
||||
char cmd[100];
|
||||
|
||||
if (snprintf(cmd, sizeof(cmd), "which %s", prog) >= sizeof(cmd)) {
|
||||
warn("snprintf which prog failed");
|
||||
return -1;
|
||||
}
|
||||
which = popen(cmd, "r");
|
||||
if (!which) {
|
||||
warn("which failed");
|
||||
return -1;
|
||||
}
|
||||
if (!fgets(path, path_sz, which)) {
|
||||
warn("fgets which failed");
|
||||
pclose(which);
|
||||
return -1;
|
||||
}
|
||||
/* which has a \n at the end of the string */
|
||||
path[strlen(path) - 1] = '\0';
|
||||
pclose(which);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns 0 on success; -1 on failure. On success, returns via `path` the full
|
||||
* path to the binary for the given pid.
|
||||
* 1) pid == x, binary == "" : returns the path to x's program
|
||||
* 2) pid == x, binary == "foo" : returns the path to libfoo linked in x
|
||||
* 3) pid == 0, binary == "" : failure: need a pid or a binary
|
||||
* 4) pid == 0, binary == "bar" : returns the path to `which bar`
|
||||
*
|
||||
* For case 4), ideally we'd like to search for libbar too, but we don't support
|
||||
* that yet.
|
||||
*/
|
||||
int resolve_binary_path(const char *binary, pid_t pid, char *path, size_t path_sz)
|
||||
{
|
||||
if (!strcmp(binary, "")) {
|
||||
if (!pid) {
|
||||
warn("Uprobes need a pid or a binary\n");
|
||||
return -1;
|
||||
}
|
||||
return get_pid_binary_path(pid, path, path_sz);
|
||||
}
|
||||
if (pid)
|
||||
return get_pid_lib_path(pid, binary, path, path_sz);
|
||||
|
||||
if (which_program(binary, path, path_sz)) {
|
||||
/*
|
||||
* If the user is tracing a program by name, we can find it.
|
||||
* But we can't find a library by name yet. We'd need to parse
|
||||
* ld.so.cache or something similar.
|
||||
*/
|
||||
warn("Can't find %s (Need a PID if this is a library)\n", binary);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Opens an elf at `path` of kind ELF_K_ELF. Returns NULL on failure. On
|
||||
* success, close with close_elf(e, fd_close).
|
||||
*/
|
||||
Elf *open_elf(const char *path, int *fd_close)
|
||||
{
|
||||
int fd;
|
||||
Elf *e;
|
||||
|
||||
if (elf_version(EV_CURRENT) == EV_NONE) {
|
||||
warn("elf init failed\n");
|
||||
return NULL;
|
||||
}
|
||||
fd = open(path, O_RDONLY);
|
||||
if (fd < 0) {
|
||||
warn("Could not open %s\n", path);
|
||||
return NULL;
|
||||
}
|
||||
e = elf_begin(fd, ELF_C_READ, NULL);
|
||||
if (!e) {
|
||||
warn("elf_begin failed: %s\n", elf_errmsg(-1));
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
if (elf_kind(e) != ELF_K_ELF) {
|
||||
warn("elf kind %d is not ELF_K_ELF\n", elf_kind(e));
|
||||
elf_end(e);
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
*fd_close = fd;
|
||||
return e;
|
||||
}
|
||||
|
||||
Elf *open_elf_by_fd(int fd)
|
||||
{
|
||||
Elf *e;
|
||||
|
||||
if (elf_version(EV_CURRENT) == EV_NONE) {
|
||||
warn("elf init failed\n");
|
||||
return NULL;
|
||||
}
|
||||
e = elf_begin(fd, ELF_C_READ, NULL);
|
||||
if (!e) {
|
||||
warn("elf_begin failed: %s\n", elf_errmsg(-1));
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
if (elf_kind(e) != ELF_K_ELF) {
|
||||
warn("elf kind %d is not ELF_K_ELF\n", elf_kind(e));
|
||||
elf_end(e);
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
void close_elf(Elf *e, int fd_close)
|
||||
{
|
||||
elf_end(e);
|
||||
close(fd_close);
|
||||
}
|
||||
|
||||
/* Returns the offset of a function in the elf file `path`, or -1 on failure. */
|
||||
off_t get_elf_func_offset(const char *path, const char *func)
|
||||
{
|
||||
off_t ret = -1;
|
||||
int i, fd = -1;
|
||||
Elf *e;
|
||||
Elf_Scn *scn;
|
||||
Elf_Data *data;
|
||||
GElf_Ehdr ehdr;
|
||||
GElf_Shdr shdr[1];
|
||||
GElf_Phdr phdr;
|
||||
GElf_Sym sym[1];
|
||||
size_t shstrndx, nhdrs;
|
||||
char *n;
|
||||
|
||||
e = open_elf(path, &fd);
|
||||
|
||||
if (!gelf_getehdr(e, &ehdr))
|
||||
goto out;
|
||||
|
||||
if (elf_getshdrstrndx(e, &shstrndx) != 0)
|
||||
goto out;
|
||||
|
||||
scn = NULL;
|
||||
while ((scn = elf_nextscn(e, scn))) {
|
||||
if (!gelf_getshdr(scn, shdr))
|
||||
continue;
|
||||
if (!(shdr->sh_type == SHT_SYMTAB || shdr->sh_type == SHT_DYNSYM))
|
||||
continue;
|
||||
data = NULL;
|
||||
while ((data = elf_getdata(scn, data))) {
|
||||
for (i = 0; gelf_getsym(data, i, sym); i++) {
|
||||
n = elf_strptr(e, shdr->sh_link, sym->st_name);
|
||||
if (!n)
|
||||
continue;
|
||||
if (GELF_ST_TYPE(sym->st_info) != STT_FUNC)
|
||||
continue;
|
||||
if (!strcmp(n, func)) {
|
||||
ret = sym->st_value;
|
||||
goto check;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
check:
|
||||
if (ehdr.e_type == ET_EXEC || ehdr.e_type == ET_DYN) {
|
||||
if (elf_getphdrnum(e, &nhdrs) != 0) {
|
||||
ret = -1;
|
||||
goto out;
|
||||
}
|
||||
for (i = 0; i < (int)nhdrs; i++) {
|
||||
if (!gelf_getphdr(e, i, &phdr))
|
||||
continue;
|
||||
if (phdr.p_type != PT_LOAD || !(phdr.p_flags & PF_X))
|
||||
continue;
|
||||
if (phdr.p_vaddr <= ret && ret < (phdr.p_vaddr + phdr.p_memsz)) {
|
||||
ret = ret - phdr.p_vaddr + phdr.p_offset;
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
ret = -1;
|
||||
}
|
||||
out:
|
||||
close_elf(e, fd);
|
||||
return ret;
|
||||
}
|
||||
18
src/15-javagc/uprobe_helpers.h
Normal file
18
src/15-javagc/uprobe_helpers.h
Normal file
@@ -0,0 +1,18 @@
|
||||
/* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */
|
||||
/* Copyright (c) 2021 Google LLC. */
|
||||
#ifndef __UPROBE_HELPERS_H
|
||||
#define __UPROBE_HELPERS_H
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
#include <gelf.h>
|
||||
|
||||
int get_pid_binary_path(pid_t pid, char *path, size_t path_sz);
|
||||
int get_pid_lib_path(pid_t pid, const char *lib, char *path, size_t path_sz);
|
||||
int resolve_binary_path(const char *binary, pid_t pid, char *path, size_t path_sz);
|
||||
off_t get_elf_func_offset(const char *path, const char *func);
|
||||
Elf *open_elf(const char *path, int *fd_close);
|
||||
Elf *open_elf_by_fd(int fd);
|
||||
void close_elf(Elf *e, int fd_close);
|
||||
|
||||
#endif /* __UPROBE_HELPERS_H */
|
||||
@@ -433,6 +433,8 @@ comm = cpptools
|
||||
128 -> 255 : 3 |********** |
|
||||
```
|
||||
|
||||
完整源代码请见:<https://github.com/eunomia-bpf/bpf-developer-tutorial/tree/main/src/9-runqlat>
|
||||
|
||||
参考资料:
|
||||
|
||||
- <https://www.brendangregg.com/blog/2016-10-08/linux-bcc-runqlat.html>
|
||||
|
||||
Reference in New Issue
Block a user