原理
从内核中获取数据。
整个工具的核心原理是利用 eBPF 技术,在内核中与块设备 I/O 相关的关键函数上设置探针(Tracepoint),从而在几乎无开销的情况下捕获 I/O 请求的开始和结束时间,计算出延迟,并将结果存入 eBPF map 中。用户空间程序则负责读取这个 map 并将数据格式化输出。
我将分步为您解释:
1. 内核态 eBPF 程序 (biolatency.bpf.c):数据采集
首先,我需要查看 eBPF 内核态程序的源代码 biolatency.bpf.c,这是数据采集的核心。
\smb\save\code\bcc\bcc\libbpf-tools\biolatency.bpf.c
好的,我们来深入分析 biolatency 的实现原理,详细解释它如何测量延迟、如何计数,以及如何从内核中获取数据。
整个工具的核心原理是利用 eBPF 技术,在内核中与块设备 I/O 相关的关键函数上设置探针(Tracepoint),从而在几乎无开销的情况下捕获 I/O 请求的开始和结束时间,计算出延迟,并将结果存入 eBPF map 中。用户空间程序则负责读取这个 map 并将数据格式化输出。
1. 内核态 eBPF 程序 (biolatency.bpf.c):数据采集的核心
这是 biolatency 的心脏,负责在内核中直接采集数据。
关键数据结构 (eBPF Maps)
eBPF 程序使用 "maps" 作为内核探针与用户空间程序之间通信和存储状态的桥梁。
追踪 I/O 请求的生命周期 (探针)
biolatency 将 eBPF 程序附加到内核的 tracepoint 上。Tracepoint 是内核中稳定且高效的事件钩子。
-
请求开始阶段:
- 探针:
block_rq_insert 或 block_rq_issue。
block_rq_insert: 当一个 I/O 请求被加入到调度器队列时触发。从这里开始测量,会包含排队延迟和设备处理延迟。
block_rq_issue: 当 I/O 调度器将请求发送给设备驱动时触发。从这里开始测量,主要衡量设备处理延迟。这是 biolatency 的默认行为。
- 处理函数:
trace_rq_start
- 核心操作:
- 获取当前时间戳:
ts = bpf_ktime_get_ns();。
- 使用 I/O 请求的指针
rq 作为键,将时间戳 ts 存入 start map 中: bpf_map_update_elem(&start, &rq, &ts, 0);。
-
请求完成阶段:
- 探针:
block_rq_complete。当块设备完成 I/O 操作后触发。
- 处理函数:
handle_block_rq_complete
- 核心操作:
- 查找开始时间: 使用完成请求的指针
rq 在 start map 中查找对应的开始时间戳 tsp。如果找不到,说明没有追踪到这个请求的开始,直接忽略。
- 计算延迟:
delta = (s64)(ts - *tsp);,其中 ts 是当前的完成时间。delta 就是 I/O 请求的精确延迟(纳秒)。
- 确定直方图桶:
- 首先,根据用户指定的单位(
-m 标志),将纳秒 delta 转换为毫秒或微秒。
- 然后,使用
slot = log2l(delta); 计算延迟应该归入哪个桶。这是一种高效的分组方式,可以将延迟按 2 的幂次进行分组(例如 0-1, 2-3, 4-7, 8-15, 16-31... 微秒)。
- 更新计数:
- 根据设备或标志,在
hists map 中找到对应的直方图。
- 使用
__sync_fetch_and_add(&histp->slots[slot], 1); 对相应桶的计数器执行原子加一操作。原子操作确保了即使在多核 CPU 上有多个 I/O 同时完成,计数也是准确的。
- 清理:
bpf_map_delete_elem(&start, &rq);。从 start map 中删除已完成的请求,防止 map 被占满。
2. 用户空间程序 (biolatency.c):数据读取与展示
用户空间程序负责控制 eBPF 程序的加载、运行,并从 map 中读取数据进行展示。
-
main 函数循环: 在 biolatency.c 的 main 函数中,程序会设置好 eBPF 环境,然后进入一个循环。在这个循环里,它会定期(默认为每秒)调用 print_log2_hists 函数。
-
print_log2_hists(obj->maps.hists, ...):
- 这个函数是你提到的关键。它的职责是从内核的
hists map 中读取数据,并以人类可读的格式打印出来。
obj->maps.hists 是用户空间程序用来访问内核 hists map 的文件描述符。
- 函数会遍历
hists map 中的所有条目。对每个条目(即每个直方图),它会:
- 读取
slots 数组中的所有计数器。
- 遍历这些计数器。如果某个计数器不为零,就意味着有 I/O 操作的延迟落在了这个范围内。
- 打印出该范围对应的延迟区间,并用一个 ASCII 字符组成的条形图来可视化该范围内的 I/O 操作数量。
- 一个非常关键的细节是:在读取并打印完数据后,用户空间程序会清空内核中
hists map 的计数器。这就是为什么你在终端上看到每隔一段时间,统计数据就会刷新一次。它读取、打印,然后将计数器归零,为下一个统计周期做准备。
总结流程
- 设置 (用户空间):
biolatency 程序解析命令行参数,然后调用 biolatency_bpf__open() 和 biolatency_bpf__load() 打开并加载 eBPF 对象文件。
- 附加 (用户空间): 调用
biolatency_bpf__attach() 将 eBPF 程序附加到 block_rq_issue 和 block_rq_complete 等内核 tracepoint 上。
- 记录开始 (内核): 一个 I/O 请求被调度 ->
block_rq_issue 探针触发 -> trace_rq_start 函数运行 -> 在 start map 中存入 (请求指针, 开始时间)。
- 记录结束并计算 (内核): I/O 请求完成 ->
block_rq_complete 探针触发 -> handle_block_rq_complete 函数运行:
- 从
start map 中取出开始时间。
- 计算
延迟 = 完成时间 - 开始时间。
- 通过
log2(延迟) 计算出归属的直方图桶。
- 在
hists map 中为对应的桶增加计数。
- 从
start map 中删除该请求的条目。
- 显示 (用户空间):
main 函数的循环调用 print_log2_hists:
- 遍历内核中的
hists map。
- 读取每个直方图的
slots 数组。
- 将数据格式化为条形图并打印到终端。
- 将
hists map 中的计数器清零。
- 清理 (用户空间): 当用户按下
Ctrl-C 时,程序会调用 biolatency_bpf__destroy(),卸载 eBPF 程序,释放所有资源。
通过这个流程,biolatency 实现了一个高效、低开销的机制,能够直接从 Linux 内核获取并实时展示块设备 I/O 的延迟分布情况。
biolatency_bpf__attach(struct biolatency_bpf *obj)
{return bpf_object__attach_skeleton(obj->skeleton);
}static inline void
biolatency_bpf__detach(struct biolatency_bpf *obj)
{bpf_object__detach_skeleton(obj->skeleton);
}static inline const void *biolatency_bpf__elf_bytes(size_t *sz);static inline int
biolatency_bpf__create_skeleton(struct biolatency_bpf *obj)
{struct bpf_object_skeleton *s;int err;s = (struct bpf_object_skeleton *)calloc(1, sizeof(*s));if (!s) {err = -ENOMEM;goto err;}s->sz = sizeof(*s);s->name = "biolatency_bpf";s->obj = &obj->obj;/* maps */s->map_cnt = 5;s->map_skel_sz = sizeof(*s->maps);s->maps = (struct bpf_map_skeleton *)calloc(s->map_cnt, s->map_skel_sz);if (!s->maps) {err = -ENOMEM;goto err;}s->maps[0].name = "cgroup_map";s->maps[0].map = &obj->maps.cgroup_map;s->maps[1].name = "start";s->maps[1].map = &obj->maps.start;s->maps[2].name = "hists";s->maps[2].map = &obj->maps.hists;s->maps[3].name = "biolaten.rodata";s->maps[3].map = &obj->maps.rodata;s->maps[3].mmaped = (void **)&obj->rodata;s->maps[4].name = "biolaten.bss";s->maps[4].map = &obj->maps.bss;s->maps[4].mmaped = (void **)&obj->bss;/* programs */s->prog_cnt = 6;s->prog_skel_sz = sizeof(*s->progs);s->progs = (struct bpf_prog_skeleton *)calloc(s->prog_cnt, s->prog_skel_sz);if (!s->progs) {err = -ENOMEM;goto err;}s->progs[0].name = "block_rq_insert_btf";s->progs[0].prog = &obj->progs.block_rq_insert_btf;s->progs[0].link = &obj->links.block_rq_insert_btf;s->progs[1].name = "block_rq_issue_btf";s->progs[1].prog = &obj->progs.block_rq_issue_btf;s->progs[1].link = &obj->links.block_rq_issue_btf;s->progs[2].name = "block_rq_complete_btf";s->progs[2].prog = &obj->progs.block_rq_complete_btf;s->progs[2].link = &obj->links.block_rq_complete_btf;s->progs[3].name = "block_rq_insert";s->progs[3].prog = &obj->progs.block_rq_insert;s->progs[3].link = &obj->links.block_rq_insert;s->progs[4].name = "block_rq_issue";s->progs[4].prog = &obj->progs.block_rq_issue;s->progs[4].link = &obj->links.block_rq_issue;s->progs[5].name = "block_rq_complete";s->progs[5].prog = &obj->progs.block_rq_complete;s->progs[5].link = &obj->links.block_rq_complete;s->data = (void *)biolatency_bpf__elf_bytes(&s->data_sz);obj->skeleton = s;return 0;
err:bpf_object__destroy_skeleton(s);return err;
}
Linux探测工具BCC(可观测性) - charlieroro - 博客园