用bpf获取rpc执行时间戳,计算函数执行时间这个代码是记录每调用一次rpc,就记录调用的rpc时间。代码具体用到了hashmap去存储每个进程号、当前时间戳。用数组去记录进程的pid和对应的耗时时间。
nfs.bpf.c
// SPDX-License-Identifier: GPL-2.0
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
char LICENSE[] SEC("license") = "GPL";
struct event {
u32 pid; //pid进程
u64 delta_ns; //耗时时间
char comm[16]; //进程名
};
struct {
__uint(type, BPF_MAP_TYPE_HASH); //指定类型是BPF_MAP_TYPE_HASH ----哈希表
__uint(max_entries, 4096); //最大key数量=4096
__type(key, u64);
__type(value, u64);
} start SEC(".maps"); //SEC是指定将这个结构体放在BPF的.maps段中,在BPF中,段是一种组织代码和数据的方式
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); //数组
} events SEC(".maps");
SEC("kprobe/rpc_call_start") //表示这个 BPF 程序会被加载到内核中,并在每次rpc_call_start内核函数被调用时触发执行,也就是rpc_start开始执行时触发
int BPF_KPROBE(handle_rpc_start) //BPF_KPROBE是一个宏用于定义一个 kprobe 类型的 BPF 程序
{
u64 tid = bpf_get_current_pid_tgid(); //获取当前PID和TGID
u64 ts = bpf_ktime_get_ns(); //获取当前的时间戳
bpf_map_update_elem(&start, &tid, &ts, BPF_ANY); //向start哈希表里面更新数据,PF_ANY 表示如果键已存在,则更新值;如果键不存在,则插入新键值对
return 0;
}
SEC("kretprobe/rpc_call_done")
int BPF_KRETPROBE(handle_rpc_done)
{
u64 tid = bpf_get_current_pid_tgid();
u64 *tsp = bpf_map_lookup_elem(&start, &tid);
if (!tsp)
return 0;
u64 delta = bpf_ktime_get_ns() - *tsp; //当前时间戳与hashmap中的时间戳进行计算,得到执行时间。
bpf_map_delete_elem(&start, &tid);
struct event e = {};
e.pid = tid >> 32;
e.delta_ns = delta;
bpf_get_current_comm(&e.comm, sizeof(e.comm)); //获取数据存储到event结构体里面
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU, &e, sizeof(e)); //
return 0;
}
nfs.c
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <bpf/libbpf.h>
#include "nfs.skel.h"
static volatile bool exiting = false;
//用户态
void handle_event(void *ctx, int cpu, void *data, __u32 size) //BPF 程序输出性能事件时,这个函数会被调用。
{
struct event *e = data;
printf("PID %d (%s): %.2f ms\n", e->pid, e->comm, e->delta_ns / 1e6);
}
void handle_lost(void *ctx, int cpu, __u64 lost) //当有事件数据丢失时,这个函数会被调用
{
fprintf(stderr, "Lost %llu events on CPU %d\n", lost, cpu);
}
void sig_handler(int sig)
{
exiting = true;
}
int main()
{
struct nfs_bpf *skel;
int err;
signal(SIGINT, sig_handler);
signal(SIGTERM, sig_handler); //注册信号处理程序,以便在收到 SIGINT 或 SIGTERM 时调用 sig_handler,
skel = nfs_bpf__open();
if (!skel) {
fprintf(stderr, "Failed to open skeleton\n");
return 1;
}
err = nfs_bpf__load(skel);
if (err) {
fprintf(stderr, "Failed to load skeleton\n");
return 1;
}
err = nfs_bpf__attach(skel); //将 BPF 程序附加到内核中的 kprobe 和 kretprobe 点
if (err) {
fprintf(stderr, "Failed to attach\n");
return 1;
}
printf("Tracing NFS client RPC latency... Ctrl+C to exit\n");
struct perf_buffer *pb = NULL;
pb = perf_buffer__new(bpf_map__fd(skel->maps.events), 8,
handle_event, handle_lost, NULL, NULL); //创建缓冲区来接收程序输出的事件数据
if (!pb) {
fprintf(stderr, "Failed to open perf buffer\n");
return 1;
}
while (!exiting)
perf_buffer__poll(pb, 100); //轮询缓冲区,等待事件数据的到来, poll轮询可以快速响应。
perf_buffer__free(pb);
nfs_bpf__destroy(skel);
return 0;
}sudo bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h 这条命令它从运行中的 Linux 内核中提取类型信息,并将其转换为 C 语言头文件格式
struct event {
__u32 pid; // 进程 ID
__u64 delta_ns; // 耗时时间
char comm[16]; // 进程名
};加入到nfs.c中。
perf_buffer__new(int map_fd, size_t page_cnt,
const struct perf_buffer_opts *opts);来看一下perf_buffer_opts结构体
struct perf_buffer_opts {
/* if specified, sample_cb is called for each sample */
perf_buffer_sample_fn sample_cb;
/* if specified, lost_cb is called for each batch of lost samples */
perf_buffer_lost_fn lost_cb;
/* ctx is provided to sample_cb and lost_cb */
void *ctx;
};其实就可以看出,调用的API函数已经更新了,只有三个参数,新版本的API,是把事件回调、丢失事件回调、用户上下文放到结构体里面,然后进行结构体指针传参。
同样可能是内核版本不同,将 rpc_call_done 改为 rpc_task_call_done
map 'events': failed to create: Invalid argument 从这句提示可以看出 events的结构体定义不对,map定义,添加更多特定参数以满足内核要求:
struct {
__uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); //数组
__uint(key_size, sizeof(int));
__uint(value_size, sizeof(int));
__uint(max_entries, 128);
} events SEC(".maps");sudo ls -la /sys/kernel/debug/tracing/events/sunrpc/ 输入指令后发现了 /sys/kernel/debug/tracing/events/sunrpc/rpc_task_call_done,这表明我们可以使用 tracepoint 而不是 kprobe,因此修改。
# 挂载本地共享 (使用NFSv4协议)
sudo mount -t nfs -o vers=4.1 localhost:/s_test /mnt
# 验证挂载
df -hT | grep nfs4
# 应输出类似:
# localhost:/s_test 526802432 43963904 456005120 9% /mnt
# 写操作
cd /mnt
touch file
dd if=/dev/urandom of=/mnt/file bs=1M count=10 status=progress
sudo make
sudo ./nfs
#先运行./nfs 自去进行写操作
#输出结果如下:
Tracing NFS client RPC latency... Ctrl+C to exit
PID 14807 (ls): 49.01 ms