bcc
bpf_prog_run_array 是 Linux 內(nèi)核用于執(zhí)行附加在某個(gè) hook(如 tracepoint/raw_tp)上的所有 BPF 程序的內(nèi)部機(jī)制。
當(dāng) block_rq_complete tracepoint 事件發(fā)生時(shí),內(nèi)核會(huì)通過(guò) bpf_prog_run_array 依次運(yùn)行所有附加的 BPF 程序(包括你用 BCC 或 libbpf 工具加載的 biolatency 程序)。
如果你加載的是 biolatency 的 block_rq_complete 相關(guān) BPF 程序(如 SEC("raw_tp/block_rq_complete") 或 SEC("tp_btf/block_rq_complete")),那么 bpf_prog_run_array 會(huì)執(zhí)行這些 BPF 程序體。
這些 BPF 程序體內(nèi)部會(huì)調(diào)用 handle_block_rq_complete 函數(shù)(已編譯進(jìn) eBPF 字節(jié)碼),所以最終會(huì)執(zhí)行 handle_block_rq_complete 的邏輯。
總結(jié):
原理
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; }
核心原理:三步測(cè)量延遲
biolatency 的核心思想是通過(guò)在內(nèi)核中設(shè)置“起點(diǎn)”和“終點(diǎn)”兩個(gè)檢查點(diǎn)來(lái)測(cè)量 I/O 請(qǐng)求的耗時(shí)。
1. 起點(diǎn):記錄請(qǐng)求開(kāi)始時(shí)間
2. 終點(diǎn):記錄請(qǐng)求完成時(shí)間,并計(jì)算差值
3. 聚合:將計(jì)算出的延遲時(shí)間存入直方圖
這個(gè)過(guò)程完全由 biolatency.bpf.c 中的 eBPF 程序在內(nèi)核態(tài)完成,效率極高。用戶態(tài)程序 biolatency.c 只負(fù)責(zé)加載 eBPF 程序和讀取最終的統(tǒng)計(jì)結(jié)果。
---
詳細(xì)實(shí)現(xiàn)步驟
1. 如何得到內(nèi)核數(shù)據(jù):掛載追蹤點(diǎn) (Tracepoints)
eBPF 程序通過(guò)掛載到內(nèi)核的追蹤點(diǎn)(Tracepoint)來(lái)捕獲 I/O 事件。追蹤點(diǎn)是內(nèi)核中預(yù)設(shè)的、穩(wěn)定的探測(cè)點(diǎn)。biolatency 主要使用了以下三個(gè)追蹤點(diǎn):
* block_rq_issue: 當(dāng)一個(gè) I/O 請(qǐng)求被發(fā)送到塊設(shè)備驅(qū)動(dòng)時(shí)觸發(fā)。這是計(jì)算延遲的默認(rèn)起點(diǎn)。
* block_rq_insert: 當(dāng)一個(gè) I/O 請(qǐng)求被插入到 I/O 調(diào)度器隊(duì)列時(shí)觸發(fā)。如果用戶使用了 -Q (queued)
參數(shù),這個(gè)點(diǎn)會(huì)作為起點(diǎn),這樣測(cè)量的延遲就包含了請(qǐng)求在隊(duì)列中等待的時(shí)間。
* block_rq_complete: 當(dāng)一個(gè) I/O 請(qǐng)求完成時(shí)觸發(fā)。這是所有測(cè)量的終點(diǎn)。
在 biolatency.bpf.c 中,你可以看到如下代碼,它們將 eBPF 函數(shù)掛載到這些追蹤點(diǎn)上:
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. 如何計(jì)數(shù)延遲:利用 eBPF Map
eBPF 程序使用兩種類型的 BPF Map 來(lái)存儲(chǔ)和計(jì)算數(shù)據(jù):
* `start` (BPF_MAP_TYPE_HASH): 這是一個(gè)哈希映射,用來(lái)臨時(shí)存儲(chǔ)一個(gè) I/O 請(qǐng)求的開(kāi)始時(shí)間。
* Key: struct request * (指向內(nèi)核中 I/O 請(qǐng)求結(jié)構(gòu)體的指針,是每個(gè)請(qǐng)求的唯一標(biāo)識(shí))。
* Value: u64 (納秒級(jí)的時(shí)間戳)。
* `hists` (BPF_MAP_TYPE_HASH of HISTOGRAM): 這是一個(gè)哈希映射,其值是一個(gè)直方圖結(jié)構(gòu)體。它用來(lái)最終存儲(chǔ)延遲分布。
* Key: struct hist_key (可以包含設(shè)備號(hào)、I/O 標(biāo)志等,用于分類統(tǒng)計(jì))。
* Value: struct hist (一個(gè)包含多個(gè)槽位的數(shù)組,每個(gè)槽位代表一個(gè)延遲范圍的計(jì)數(shù)值)。
延遲計(jì)數(shù)流程如下:
1. 請(qǐng)求開(kāi)始時(shí) (觸發(fā) block_rq_issue 或 block_rq_insert):
* eBPF 程序獲取當(dāng)前的內(nèi)核時(shí)間戳 (bpf_ktime_get_ns())。
* 它以 I/O 請(qǐng)求的指針 struct request *rq 為鍵,時(shí)間戳為值,存入 start 哈希映射中。
* 代碼片段: bpf_map_update_elem(&start, &rq, &ts, 0);
2. 請(qǐng)求完成時(shí) (觸發(fā) block_rq_complete):
* eBPF 程序再次獲取 I/O 請(qǐng)求的指針 struct request *rq。
* 用這個(gè)指針作為鍵,去 start 哈希映射中查找對(duì)應(yīng)的開(kāi)始時(shí)間戳。
* 代碼片段: tsp = bpf_map_lookup_elem(&start, &rq);
* 如果找到了開(kāi)始時(shí)間戳,就計(jì)算延遲:delta = bpf_ktime_get_ns() - *tsp;。
* 計(jì)算完成后,將該請(qǐng)求的條目從 start 映射中刪除,以防內(nèi)存泄漏。
* 代碼片段: bpf_map_delete_elem(&start, &rq);
3. 如何聚合數(shù)據(jù):更新直方圖
為了避免將海量的單個(gè)延遲數(shù)據(jù)發(fā)送到用戶空間造成性能瓶頸,eBPF 程序在內(nèi)核中就地完成了數(shù)據(jù)聚合。
1. 計(jì)算出延遲 delta (單位是納秒) 后,程序會(huì)將其轉(zhuǎn)換為微秒或毫秒,并計(jì)算它應(yīng)該落入哪個(gè)直方圖槽位 (slot)。通常使用 log2
來(lái)實(shí)現(xiàn),這樣可以有效地覆蓋從幾微秒到幾秒的巨大范圍。
* 代碼片段: slot = log2l(delta);
2. 然后,程序更新 hists 映射中對(duì)應(yīng)槽位的計(jì)數(shù)器,使其加一。
* 代碼片段: __sync_fetch_and_add(&histp->slots[slot], 1);
4. 用戶空間的角色
biolatency.c 中的用戶空間程序相對(duì)簡(jiǎn)單:
1. 加載和附加:解析命令行參數(shù),打開(kāi)、加載 eBPF 對(duì)象文件 (.o),并將其中的程序附加到內(nèi)核的追蹤點(diǎn)上。
2. 循環(huán)讀取:進(jìn)入一個(gè)循環(huán),定期 (sleep) 從內(nèi)核的 hists BPF 映射中讀取已經(jīng)聚合好的直方圖數(shù)據(jù)。
3. 顯示結(jié)果:將直方圖數(shù)據(jù)格式化并打印到終端上,形成我們看到的延遲分布圖。
4. 清理:當(dāng)程序退出時(shí)(例如按 Ctrl-C),它會(huì)負(fù)責(zé)分離 eBPF 程序并銷毀 BPF 對(duì)象,釋放所有資源。
總結(jié)
biolatency 的實(shí)現(xiàn)原理可以精煉為:
* 數(shù)據(jù)源:通過(guò) eBPF 掛載到內(nèi)核塊設(shè)備層的追蹤點(diǎn),無(wú)侵入地獲取 I/O 請(qǐng)求的開(kāi)始和完成事件。
* 延遲計(jì)算:利用一個(gè)哈希映射 (`start`),以 I/O 請(qǐng)求指針為鍵,來(lái)配對(duì)開(kāi)始和結(jié)束事件,并計(jì)算時(shí)間差。
* 數(shù)據(jù)聚合:在內(nèi)核中直接使用一個(gè)直方圖映射 (`hists`) 對(duì)延遲數(shù)據(jù)進(jìn)行聚合,只將統(tǒng)計(jì)結(jié)果傳遞給用戶空間。
* 用戶接口:用戶空間程序負(fù)責(zé)控制、參數(shù)配置
handle_block_rq_complete 函數(shù)并不是在 C 代碼中被直接調(diào)用的,而是作為 eBPF 程序的一部分,在內(nèi)核的特定事件發(fā)生時(shí)被觸發(fā)執(zhí)行。
具體來(lái)說(shuō),它被以下兩個(gè) eBPF 程序調(diào)用,這兩個(gè)程序都附加到了內(nèi)核的 block_rq_complete tracepoint(跟蹤點(diǎn))上:
block_rq_complete_btf(使用 BTF 的版本)block_rq_complete(使用原始 tracepoint 的版本)
您可以在 /smb/save/code/bcc/bcc/libbpf-tools/biolatency.bpf.c 文件的末尾找到這些調(diào)用點(diǎn):
// ... 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";
調(diào)用流程
-
當(dāng)一個(gè)塊設(shè)備(如硬盤或 SSD)完成了 I/O 操作(例如,讀或?qū)憯?shù)據(jù)完成)時(shí),Linux 內(nèi)核會(huì)觸發(fā)一個(gè)名為
block_rq_complete的內(nèi)部事件。 -
biolatency工具的用戶空間部分在啟動(dòng)時(shí),會(huì)通過(guò)libbpf庫(kù)將block_rq_complete_btf或block_rq_complete這兩個(gè) eBPF 程序中的一個(gè)附加(attach)到這個(gè)內(nèi)核事件上。選擇哪個(gè)版本取決于內(nèi)核是否支持 BTF (BPF Type Format),BTF 提供了更豐富的類型信息,是更現(xiàn)代的方式。 -
一旦內(nèi)核事件被觸發(fā),附加的 eBPF 程序就會(huì)被立即執(zhí)行。
-
eBPF 程序(
block_rq_complete_btf或block_rq_complete)的唯一任務(wù)就是調(diào)用handle_block_rq_complete函數(shù),并將內(nèi)核傳遞給 tracepoint 的參數(shù)(如struct request *rq)原封不動(dòng)地傳遞給它。
所以,handle_block_rq_complete 的調(diào)用是由內(nèi)核事件驅(qū)動(dòng)的,而不是由程序中的其他 C 函數(shù)直接調(diào)用的。這是 eBPF 工作模式的核心:將小型、高效的程序注入到內(nèi)核的特定路徑中,以響應(yīng)內(nèi)核事件。
bpftool 常見(jiàn)命令
bpftool prog list
2: tracing name hid_tail_call tag 7cc47bbf07148bfe gpl
loaded_at 2025-05-30T14:18:05+0800 uid 0
xlated 56B jited 133B memlock 4096B map_ids 2
btf_id 2
408: cgroup_device tag 03b4eaae2f14641a gpl
loaded_at 2025-06-30T19:57:38+0800 uid 1000
xlated 296B jited 164B memlock 4096B map_ids 4
435: cgroup_device tag 03b4eaae2f14641a gpl
loaded_at 2025-06-30T21:26:06+0800 uid 1000
xlated 296B jited 164B memlock 4096B map_ids 3
2763: cgroup_device tag 0ecd07b7b633809f gpl
2804: tracing name block_rq_complete_btf tag 44b7e455a95d77c2 gpl
loaded_at 2025-10-27T17:14:24+0800 uid 0
xlated 1072B jited 627B memlock 4096B map_ids 21,17,18,19,22
btf_id 185
bpftool prog dump xlated id 2804
int block_rq_complete_btf(unsigned long long * ctx):
; int BPF_PROG(block_rq_complete_btf, struct request *rq, int error, unsigned int nr_bytes)
0: (79) r1 = *(u64 *)(r1 +0)
; return handle_block_rq_complete(rq, error, nr_bytes);
1: (85) call pc+2#bpf_prog_b1cfd2a6e8c49b0d_handle_block_rq_complete
; int BPF_PROG(block_rq_complete_btf, struct request *rq, int error, unsigned int nr_bytes)
2: (b7) r0 = 0
3: (95) exit
int handle_block_rq_complete(struct request * rq, int error, unsigned int nr_bytes):
; static int handle_block_rq_complete(struct request *rq, int error, unsigned int nr_bytes)
4: (7b) *(u64 *)(r10 -24) = r1
; u64 slot, *tsp, ts = bpf_ktime_get_ns();
5: (85) call bpf_ktime_get_ns#256016
6: (bf) r6 = r0
7: (b7) r1 = 0
; struct hist_key hkey = {};
8: (7b) *(u64 *)(r10 -32) = r1
; if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
9: (18) r1 = map[id:21][0]+0
11: (71) r1 = *(u8 *)(r1 +0)
; if (filter_cg && !bpf_current_task_under_cgroup(&cgroup_map, 0))
12: (bf) r2 = r10
;
13: (07) r2 += -24
; tsp = bpf_map_lookup_elem(&start, &rq);
14: (18) r1 = map[id:18]
16: (85) call __htab_map_lookup_elem#296912
17: (15) if r0 == 0x0 goto pc+1
18: (07) r0 += 56
; if (!tsp)
posted on 2025-10-24 17:00 文心雕蟲 閱讀(7) 評(píng)論(0) 收藏 舉報(bào)
浙公網(wǎng)安備 33010602011771號(hào)