bcc

原理

从内核中获取数据。

整个工具的核心原理是利用 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" 作为内核探针与用户空间程序之间通信和存储状态的桥梁。

  • start (类型: BPF_MAP_TYPE_HASH):这是测量延迟最关键的 map。

    • 键 (Key): struct request * — 一个指向内核 I/O 请求结构体的指针。当一个 I/O 请求在处理过程中时,这个指针是它的唯一标识。
    • 值 (Value): u64 — 时间戳(纳秒),记录请求开始的精确时间。
    • 作用: 当一个 I/O 请求开始时,eBPF 程序会把它的指针和当前时间戳存入这个 map。
  • hists (类型: BPF_MAP_TYPE_HASH):这个 map 存储最终的延迟直方图数据。

    • 键 (Key): struct hist_key — 一个结构体,可以包含设备号 (dev) 或请求标志 (cmd_flags)。这使得工具可以按磁盘、按操作类型(如读 vs. 写)分别统计延迟。如果用户没有指定分组,这个键就是空的,所有延迟都会被聚合到一起。
    • 值 (Value): struct hist — 一个包含名为 slots 数组的结构体。这个数组就是直方图本身,每个元素都是一个计数器,对应一个特定的延迟范围(桶)。

追踪 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
    • 核心操作:
      1. 获取当前时间戳: ts = bpf_ktime_get_ns();
      2. 使用 I/O 请求的指针 rq 作为键,将时间戳 ts 存入 start map 中: bpf_map_update_elem(&start, &rq, &ts, 0);
  • 请求完成阶段:

    • 探针: block_rq_complete。当块设备完成 I/O 操作后触发。
    • 处理函数: handle_block_rq_complete
    • 核心操作:
      1. 查找开始时间: 使用完成请求的指针 rq 在 start map 中查找对应的开始时间戳 tsp。如果找不到,说明没有追踪到这个请求的开始,直接忽略。
      2. 计算延迟: delta = (s64)(ts - *tsp);,其中 ts 是当前的完成时间。delta 就是 I/O 请求的精确延迟(纳秒)。
      3. 确定直方图桶:
        • 首先,根据用户指定的单位(-m 标志),将纳秒 delta 转换为毫秒或微秒。
        • 然后,使用 slot = log2l(delta); 计算延迟应该归入哪个桶。这是一种高效的分组方式,可以将延迟按 2 的幂次进行分组(例如 0-1, 2-3, 4-7, 8-15, 16-31... 微秒)。
      4. 更新计数:
        • 根据设备或标志,在 hists map 中找到对应的直方图。
        • 使用 __sync_fetch_and_add(&histp->slots[slot], 1); 对相应桶的计数器执行原子加一操作。原子操作确保了即使在多核 CPU 上有多个 I/O 同时完成,计数也是准确的。
      5. 清理: 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 中的所有条目。对每个条目(即每个直方图),它会:
      1. 读取 slots 数组中的所有计数器。
      2. 遍历这些计数器。如果某个计数器不为零,就意味着有 I/O 操作的延迟落在了这个范围内。
      3. 打印出该范围对应的延迟区间,并用一个 ASCII 字符组成的条形图来可视化该范围内的 I/O 操作数量。
    • 一个非常关键的细节是:在读取并打印完数据后,用户空间程序会清空内核中 hists map 的计数器。这就是为什么你在终端上看到每隔一段时间,统计数据就会刷新一次。它读取、打印,然后将计数器归零,为下一个统计周期做准备。

总结流程

  1. 设置 (用户空间): biolatency 程序解析命令行参数,然后调用 biolatency_bpf__open() 和 biolatency_bpf__load() 打开并加载 eBPF 对象文件。
  2. 附加 (用户空间): 调用 biolatency_bpf__attach() 将 eBPF 程序附加到 block_rq_issue 和 block_rq_complete 等内核 tracepoint 上。
  3. 记录开始 (内核): 一个 I/O 请求被调度 -> block_rq_issue 探针触发 -> trace_rq_start 函数运行 -> 在 start map 中存入 (请求指针, 开始时间)
  4. 记录结束并计算 (内核): I/O 请求完成 -> block_rq_complete 探针触发 -> handle_block_rq_complete 函数运行:
    • 从 start map 中取出开始时间。
    • 计算 延迟 = 完成时间 - 开始时间
    • 通过 log2(延迟) 计算出归属的直方图桶。
    • 在 hists map 中为对应的桶增加计数。
    • 从 start map 中删除该请求的条目。
  5. 显示 (用户空间): main 函数的循环调用 print_log2_hists
    • 遍历内核中的 hists map。
    • 读取每个直方图的 slots 数组。
    • 将数据格式化为条形图并打印到终端。
    • 将 hists map 中的计数器清零。
  6. 清理 (用户空间): 当用户按下 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; }


核心原理:三步测量延迟


biolatency 的核心思想是通过在内核中设置“起点”和“终点”两个检查点来测量 I/O 请求的耗时。


1. 起点:记录请求开始时间
2. 终点:记录请求完成时间,并计算差值
3. 聚合:将计算出的延迟时间存入直方图


这个过程完全由 biolatency.bpf.c 中的 eBPF 程序在内核态完成,效率极高。用户态程序 biolatency.c 只负责加载 eBPF 程序和读取最终的统计结果。


---


详细实现步骤


1. 如何得到内核数据:挂载追踪点 (Tracepoints)


eBPF 程序通过挂载到内核的追踪点(Tracepoint)来捕获 I/O 事件。追踪点是内核中预设的、稳定的探测点。biolatency 主要使用了以下三个追踪点:


* block_rq_issue: 当一个 I/O 请求被发送到块设备驱动时触发。这是计算延迟的默认起点。
* block_rq_insert: 当一个 I/O 请求被插入到 I/O 调度器队列时触发。如果用户使用了 -Q (queued)
参数,这个点会作为起点,这样测量的延迟就包含了请求在队列中等待的时间。
* block_rq_complete: 当一个 I/O 请求完成时触发。这是所有测量的终点。


在 biolatency.bpf.c 中,你可以看到如下代码,它们将 eBPF 函数挂载到这些追踪点上:


1 SEC("tp_btf/block_rq_issue")
2 int block_rq_issue_btf(u64 *ctx) { ... }
3
4 SEC("tp_btf/block_rq_insert")
5 int block_rq_insert_btf(u64 *ctx) { ... }
6
7 SEC("tp_btf/block_rq_complete")
8 int BPF_PROG(block_rq_complete_btf, struct request *rq, ...) { ... }


2. 如何计数延迟:利用 eBPF Map


eBPF 程序使用两种类型的 BPF Map 来存储和计算数据:


* `start` (BPF_MAP_TYPE_HASH): 这是一个哈希映射,用来临时存储一个 I/O 请求的开始时间。
* Key: struct request * (指向内核中 I/O 请求结构体的指针,是每个请求的唯一标识)。
* Value: u64 (纳秒级的时间戳)。


* `hists` (BPF_MAP_TYPE_HASH of HISTOGRAM): 这是一个哈希映射,其值是一个直方图结构体。它用来最终存储延迟分布。
* Key: struct hist_key (可以包含设备号、I/O 标志等,用于分类统计)。
* Value: struct hist (一个包含多个槽位的数组,每个槽位代表一个延迟范围的计数值)。


延迟计数流程如下:


1. 请求开始时 (触发 block_rq_issue 或 block_rq_insert):
* eBPF 程序获取当前的内核时间戳 (bpf_ktime_get_ns())。
* 它以 I/O 请求的指针 struct request *rq 为键,时间戳为值,存入 start 哈希映射中。
* 代码片段: bpf_map_update_elem(&start, &rq, &ts, 0);


2. 请求完成时 (触发 block_rq_complete):
* eBPF 程序再次获取 I/O 请求的指针 struct request *rq。
* 用这个指针作为键,去 start 哈希映射中查找对应的开始时间戳。
* 代码片段: tsp = bpf_map_lookup_elem(&start, &rq);
* 如果找到了开始时间戳,就计算延迟:delta = bpf_ktime_get_ns() - *tsp;。
* 计算完成后,将该请求的条目从 start 映射中删除,以防内存泄漏。
* 代码片段: bpf_map_delete_elem(&start, &rq);


3. 如何聚合数据:更新直方图


为了避免将海量的单个延迟数据发送到用户空间造成性能瓶颈,eBPF 程序在内核中就地完成了数据聚合。


1. 计算出延迟 delta (单位是纳秒) 后,程序会将其转换为微秒或毫秒,并计算它应该落入哪个直方图槽位 (slot)。通常使用 log2
来实现,这样可以有效地覆盖从几微秒到几秒的巨大范围。
* 代码片段: slot = log2l(delta);
2. 然后,程序更新 hists 映射中对应槽位的计数器,使其加一。
* 代码片段: __sync_fetch_and_add(&histp->slots[slot], 1);


4. 用户空间的角色


biolatency.c 中的用户空间程序相对简单:


1. 加载和附加:解析命令行参数,打开、加载 eBPF 对象文件 (.o),并将其中的程序附加到内核的追踪点上。
2. 循环读取:进入一个循环,定期 (sleep) 从内核的 hists BPF 映射中读取已经聚合好的直方图数据。
3. 显示结果:将直方图数据格式化并打印到终端上,形成我们看到的延迟分布图。
4. 清理:当程序退出时(例如按 Ctrl-C),它会负责分离 eBPF 程序并销毁 BPF 对象,释放所有资源。


总结


biolatency 的实现原理可以精炼为:


* 数据源:通过 eBPF 挂载到内核块设备层的追踪点,无侵入地获取 I/O 请求的开始和完成事件。
* 延迟计算:利用一个哈希映射 (`start`),以 I/O 请求指针为键,来配对开始和结束事件,并计算时间差。
* 数据聚合:在内核中直接使用一个直方图映射 (`hists`) 对延迟数据进行聚合,只将统计结果传递给用户空间。
* 用户接口:用户空间程序负责控制、参数配置

 

 

handle_block_rq_complete 函数并不是在 C 代码中被直接调用的,而是作为 eBPF 程序的一部分,在内核的特定事件发生时被触发执行。

具体来说,它被以下两个 eBPF 程序调用,这两个程序都附加到了内核的 block_rq_complete tracepoint(跟踪点)上:

  1. block_rq_complete_btf (使用 BTF 的版本)
  2. block_rq_complete (使用原始 tracepoint 的版本)

您可以在 /smb/save/code/bcc/bcc/libbpf-tools/biolatency.bpf.c 文件的末尾找到这些调用点:

/smb/save/code/bcc/bcc/libbpf-tools/biolatency.bpf.c
 
 
 
 
 
Apply
 
// ... existing code ...
SEC("tp_btf/block_rq_complete")
int BPF_PROG(block_rq_complete_btf, struct request *rq, int error, unsigned int nr_bytes)
{
	return handle_block_rq_complete(rq, error, nr_bytes);
}
// ... existing code ...
SEC("raw_tp/block_rq_complete")
int BPF_PROG(block_rq_complete, struct request *rq, int error, unsigned int nr_bytes)
{
	return handle_block_rq_complete(rq, error, nr_bytes);
}

char LICENSE[] SEC("license") = "GPL";

调用流程

  1. 当一个块设备(如硬盘或 SSD)完成了 I/O 操作(例如,读或写数据完成)时,Linux 内核会触发一个名为 block_rq_complete 的内部事件。

  2. biolatency 工具的用户空间部分在启动时,会通过 libbpf 库将 block_rq_complete_btf 或 block_rq_complete 这两个 eBPF 程序中的一个附加(attach)到这个内核事件上。选择哪个版本取决于内核是否支持 BTF (BPF Type Format),BTF 提供了更丰富的类型信息,是更现代的方式。

  3. 一旦内核事件被触发,附加的 eBPF 程序就会被立即执行。

  4. eBPF 程序(block_rq_complete_btf 或 block_rq_complete)的唯一任务就是调用 handle_block_rq_complete 函数,并将内核传递给 tracepoint 的参数(如 struct request *rq)原封不动地传递给它。

所以,handle_block_rq_complete 的调用是由内核事件驱动的,而不是由程序中的其他 C 函数直接调用的。这是 eBPF 工作模式的核心:将小型、高效的程序注入到内核的特定路径中,以响应内核事件。

 

 

Linux探测工具BCC(可观测性) - charlieroro - 博客园

posted on 2025-10-24 17:00  文心雕虫  阅读(2)  评论(0)    收藏  举报