BPF 提供多種對映型別,各有其適用場景。Queue Map 採用 FIFO 方式儲存資料,適合處理需要依序存取的資料流。Stack Map 則以 LIFO 方式儲存資料,適用於追蹤函式呼叫堆積疊等場景。Reuseport Socket Map 則專用於管理可重複使用的 socket 參照,搭配 BPF_PROG_TYPE_SK_REUSEPORT 程式型別使用,可有效控制網路封包的過濾和分發。除了這些對映型別,BPF 還提供了其他對映型別,例如 Array Map、Hash Map 等,開發者可根據需求選擇合適的型別。理解不同 BPF 對映型別的特性,有助於編寫更有效率的 BPF 程式。
9. Reuseport Socket Maps
Reuseport Socket Maps 是一種特殊的 BPF Map,用於儲存可以被重用的 socket 參照。這種 Map 主要與 BPF_PROG_TYPE_SK_REUSEPORT
程式型別一起使用,提供了一種控制網路封包過濾和分發的機制。
每種 BPF Map 型別都有其特定的應用場景和優點,開發者可以根據具體需求選擇合適的 Map 型別來實作高效的資料儲存和查詢功能。
什麼是 Queue Maps?
Queue Maps 是一種使用先進先出(FIFO)儲存方式的對映表,用於保持對映表中的元素。它們被定義為 BPF_MAP_TYPE_QUEUE
型別。當您從對映表中取出一個元素時,結果將是最早被新增到對映表中的元素。
Queue Maps 的工作原理
當您使用 bpf_map_lookup_elem
時,對映表總是查詢最舊的元素。當您使用 bpf_map_update_elem
時,對映表總是將元素新增到佇列的末尾,因此您需要先讀取佇列中的其他元素才能取出這個元素。您也可以使用 bpf_map_lookup_and_delete
幫助器來取出最舊的元素並以原子方式從對映表中刪除它。
Queue Maps 的限制
Queue Maps 不支援 bpf_map_delete_elem
和 bpf_map_get_next_key
幫助器。如果您嘗試使用它們,它們將失敗並將 errno
變數設定為 EINVAL
。
Queue Maps 的範例
以下是使用 Queue Maps 的範例:
struct bpf_map_def SEC("maps") queue_map = {
.type = BPF_MAP_TYPE_QUEUE,
.key_size = 0,
.value_size = sizeof(int),
.max_entries = 100,
.map_flags = 0,
};
int i;
for (i = 0; i < 5; i++)
bpf_map_update_elem(&queue_map, NULL, &i, BPF_ANY);
int value;
for (i = 0; i < 5; i++) {
bpf_map_lookup_and_delete(&queue_map, NULL, &value);
printf("Value read from the map: '%d'\n", value);
}
這個範例將輸出:
Value read from the map: '0'
Value read from the map: '1'
Value read from the map: '2'
Value read from the map: '3'
Value read from the map: '4'
什麼是 Stack Maps?
Stack Maps 是一種使用後進先出(LIFO)儲存方式的對映表,用於保持對映表中的元素。它們被定義為 BPF_MAP_TYPE_STACK
型別。當您從對映表中取出一個元素時,結果將是最近被新增到對映表中的元素。
Stack Maps 的工作原理
當您使用 bpf_map_lookup_elem
時,對映表總是查詢最新的元素。當您使用 bpf_map_update_elem
時,對映表總是將元素新增到堆積疊的頂部,因此它是第一個被取出的元素。您也可以使用 bpf_map_lookup_and_delete
幫助器來取出最新的元素並以原子方式從對映表中刪除它。
Stack Maps 的限制
Stack Maps 不支援 bpf_map_delete_elem
和 bpf_map_get_next_key
幫助器。如果您嘗試使用它們,它們將失敗並將 errno
變數設定為 EINVAL
。
Stack Maps 的範例
以下是使用 Stack Maps 的範例:
struct bpf_map_def SEC("maps") stack_map = {
.type = BPF_MAP_TYPE_STACK,
.key_size = 0,
.value_size = sizeof(int),
.max_entries = 100,
.map_flags = 0,
};
int i;
for (i = 0; i < 5; i++)
bpf_map_update_elem(&stack_map, NULL, &i, BPF_ANY);
int value;
for (i = 0; i < 5; i++) {
bpf_map_lookup_and_delete(&stack_map, NULL, &value);
printf("Value read from the map: '%d'\n", value);
}
這個範例將輸出:
Value read from the map: '4'
Value read from the map: '3'
Value read from the map: '2'
Value read from the map: '1'
Value read from the map: '0'
BPF 虛擬檔案系統
BPF 虛擬檔案系統是一種用於儲存 BPF 物件的檔案系統。它允許您將 BPF 物件儲存為檔案,並在程式之間分享。
BPF 虛擬檔案系統的工作原理
BPF 虛擬檔案系統使用 /sys/fs/bpf
作為其預設目錄。您可以將 BPF 物件儲存為檔案,並使用 BPF_PIN_FD
命令將其儲存到檔案系統中。您也可以使用 BPF_OBJ_GET
命令從檔案系統中讀取 BPF 物件。
BPF 虛擬檔案系統的範例
以下是使用 BPF 虛擬檔案系統的範例:
static const char * file_path = "/sys/fs/bpf/my_array";
這個範例示範如何將 BPF 物件儲存為檔案,並使用 BPF_PIN_FD
命令將其儲存到檔案系統中。
BPF 虛擬檔案系統和對映
在上一章中,我們探討了 BPF Maps 的基礎概念和它們在 kernel 和 user-space 之間建立通訊通道的重要性。在本章中,我們將更深入地探討 BPF 虛擬檔案系統和如何使用它來儲存和載入 BPF 物件,包括對映。
建立和儲存對映
首先,我們需要建立一個對映並將其儲存到檔案系統中。以下是示例程式碼:
int main(int argc, char **argv) {
int key, value, fd, added, pinned;
fd = bpf_create_map(BPF_MAP_TYPE_ARRAY, sizeof(int), sizeof(int), 100, 0);
if (fd < 0) {
printf("Failed to create map: %d (%s)\n", fd, strerror(errno));
return -1;
}
key = 1, value = 1234;
added = bpf_map_update_elem(fd, &key, &value, BPF_ANY);
if (added < 0) {
printf("Failed to update map: %d (%s)\n", added, strerror(errno));
return -1;
}
pinned = bpf_obj_pin(fd, file_path);
if (pinned < 0) {
printf("Failed to pin map to the file system: %d (%s)\n",
pinned, strerror(errno));
return -1;
}
return 0;
}
這段程式碼建立了一個陣列對映,更新對映中的元素,並將其儲存到檔案系統中。
載入和存取對映
接下來,我們可以載入已經儲存的對映並存取其元素。以下是示例程式碼:
static const char * file_path = "/sys/fs/bpf/my_array";
int main(int argc, char **argv) {
int fd, key, value, result;
fd = bpf_obj_get(file_path);
if (fd < 0) {
printf("Failed to fetch the map: %d (%s)\n", fd, strerror(errno));
return -1;
}
key = 1;
result = bpf_map_lookup_elem(fd, &key, &value);
if (result < 0) {
printf("Failed to read value from the map: %d (%s)\n",
result, strerror(errno));
return -1;
}
printf("Value read from the map: '%d'\n", value);
return 0;
}
這段程式碼載入已經儲存的對映,存取其元素,並印出元素的值。
圖表翻譯:
flowchart TD A[建立對映] --> B[更新對映] B --> C[儲存對映到檔案系統] C --> D[載入對映] D --> E[存取對映元素]
這個流程圖展示了建立、更新、儲存、載入和存取對映的過程。
Tracing with BPF:深入瞭解系統運作
Tracing 是軟體工程中的一種方法,用於收集資料以進行效能最佳化和除錯。其目的是在執行時提供有用的資訊,以便於未來的分析。使用 BPF(Berkeley Packet Filter)進行 Tracing 的主要優點是,可以存取幾乎所有的 Linux 內核和應用程式資訊。BPF 新增的系統效能和延遲開銷最小,並且不需要開發人員修改應用程式以收集資料。
Linux 核心提供了多種工具,可以與 BPF 結合使用。在本章中,我們將討論這些不同的工具,並展示如何使用它們來存取核心資訊。
Tracing 的最終目的是提供對系統的深入瞭解,透過收集所有可用的資料並以有用的方式呈現給使用者。在本章中,我們將討論不同的資料表示形式,以及如何在不同場景中使用它們。
使用 BPF Compiler Collection(BCC)
從本章開始,我們將使用 BCC 進行 BPF 程式開發。BCC 是一組工具,旨在使構建 BPF 程式更加可預測,即使您精通 Clang 和 LLVM,也不需要花費太多時間構建相同的工具並確保 BPF 驗證器不會拒絕您的程式。BCC 提供了可重用的元件,例如 Perf 事件對映,以及與 LLVM 後端的整合,以提供更好的除錯選項。此外,BCC 還包括了多種程式語言的繫結,我們將在示例中使用 Python。這些繫結允許您使用高階語言編寫 BPF 程式的使用者空間部分,從而得出更有用的程式。
探索核心資訊
要開始在 Linux 核心中進行 Tracing,您需要找出核心提供的擴充套件點,即所謂的探測點(probes)。探測點允許您將 BPF 程式附加到核心中的特定位置,以收集資料。
探測點(Probes)
探測點是設計用於傳輸環境資訊的探索性程式。它們收集系統資料,並使其可供您探索和分析。傳統上,在 Linux 中使用探測點涉及編寫編譯為核心模組的程式,這可能會在生產系統中引起災難性的問題。隨著時間的推移,它們已經演變成更安全的執行,但仍然很麻煩地編寫和測試。像 SystemTap 這樣的工具建立了新的協定,用於編寫探測點,並為從 Linux 內核和所有使用者空間程式中取得更豐富的資訊鋪平了道路。BPF 藉助探測點來收集除錯和分析的資訊。
型別的探測點
在本章中,我們將涵蓋四種不同型別的探測點:
- 核心探測點(Kernel Probes):提供對核心內部元件的動態存取。
- 追蹤點(Tracepoints):提供對核心內部元件的靜態存取。
- 使用者空間探測點(User-space Probes):提供對使用者空間程式的動態存取。
- 使用者靜態定義追蹤點(User Statically Defined Tracepoints):允許對使用者空間程式進行靜態存取。
核心探測點(Kernel Probes)
核心探測點允許您在幾乎任何核心指令中設定動態標誌或斷點,從而最小化開銷。當核心到達這些標誌之一時,它將執行附加到探測點的程式碼,然後還原其正常工作。核心探測點可以提供有關系統中發生的事情的任何資訊,例如檔案開啟和二進位制檔案執行。需要注意的是,核心探測點沒有穩定的應用二進位制介面(ABI),這意味著它們可能會在不同核心版本之間更改。
Kprobes
Kprobes 允許您在任何核心指令之前插入 BPF 程式。您需要知道要打斷的函式簽名,因為這不是一個穩定的 ABI,所以您需要小心地設定這些探測點,以便在不同核心版本之間執行相同的程式。
示例:使用 Kprobes 列印執行二進位制檔名稱
from bcc import BPF
bpf_source = """
int do_sys_execve(struct pt_regs *ctx, void *filename, void *argv, void *envp) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("executing program: %s", comm);
return 0;
}
"""
bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kprobe(event=execve_function, fn_name="do_sys_execve")
這個示例展示瞭如何使用 Kprobes 來列印任何在系統中執行的二進位制檔名稱。
使用BPF進行系統追蹤
BPF(Berkeley Packet Filter)是一種強大的系統追蹤工具,允許開發人員在核心級別上追蹤和分析系統的行為。在本文中,我們將探討如何使用BPF進行系統追蹤,包括kprobes、kretprobes和tracepoints。
Kprobes
Kprobes是一種動態追蹤機制,允許開發人員在核心級別上追蹤特定的函式或指令。以下是使用kprobes進行系統追蹤的範例:
from bcc import BPF
bpf_source = """
int kprobe_execve(struct pt_regs *ctx) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("program: %s", comm);
return 0;
}
"""
bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kprobe(event=execve_function, fn_name="kprobe_execve")
bpf.trace_print()
在這個範例中,我們定義了一個BPF程式,該程式使用kprobes來追蹤execve
系統呼叫。當execve
系統呼叫被觸發時,BPF程式將列印預出目前的命令名稱。
Kretprobes
Kretprobes是一種動態追蹤機制,允許開發人員在核心級別上追蹤特定的函式或指令的傳回值。以下是使用kretprobes進行系統追蹤的範例:
from bcc import BPF
bpf_source = """
int kretprobe_execve(struct pt_regs *ctx) {
int return_value;
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
return_value = PT_REGS_RC(ctx);
bpf_trace_printk("program: %s, return: %d", comm, return_value);
return 0;
}
"""
bpf = BPF(text=bpf_source)
execve_function = bpf.get_syscall_fnname("execve")
bpf.attach_kretprobe(event=execve_function, fn_name="kretprobe_execve")
bpf.trace_print()
在這個範例中,我們定義了一個BPF程式,該程式使用kretprobes來追蹤execve
系統呼叫的傳回值。當execve
系統呼叫傳回時,BPF程式將列印預出目前的命令名稱和傳回值。
Tracepoints
Tracepoints是一種靜態追蹤機制,允許開發人員在核心級別上追蹤特定的事件或函式。以下是使用tracepoints進行系統追蹤的範例:
from bcc import BPF
bpf_source = """
int trace_bpf_prog_load(void *ctx) {
char comm[16];
bpf_get_current_comm(&comm, sizeof(comm));
bpf_trace_printk("%s is loading a BPF program", comm);
return 0;
}
"""
bpf = BPF(text=bpf_source)
bpf.attach_tracepoint(tp="bpf:bpf_prog_load", fn_name="trace_bpf_prog_load")
在這個範例中,我們定義了一個BPF程式,該程式使用tracepoints來追蹤BPF程式的載入事件。當BPF程式被載入時,BPF程式將列印預出目前的命令名稱。
使用BPF進行追蹤
BPF(Berkeley Packet Filter)是一種強大的Linux核心追蹤工具,允許開發人員在核心級別上追蹤和分析系統的行為。在本章中,我們將探討如何使用BPF進行追蹤,包括核心級別的追蹤和使用者空間的追蹤。
核心級別的追蹤
核心級別的追蹤是指在核心中插入追蹤點,以捕捉核心函式的呼叫和傳回。這種方法可以提供對核心行為的詳細瞭解,包括系統呼叫、核心函式的執行等。
使用kprobes進行核心級別的追蹤
kprobes是一種核心級別的追蹤機制,允許開發人員在核心中插入追蹤點。以下是使用kprobes進行核心級別的追蹤的例子:
#include <linux/bpf.h>
int trace_kernel_function(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
bpf_trace_printk("New kernel process running with PID: %d", pid);
}
int main() {
struct bpf_prog_load_attr prog_load_attr = {
.prog_type = BPF_PROG_TYPE_KPROBE,
.file = "kernel_function",
};
bpf_prog_load_xattr(&prog_load_attr, &prog_load);
bpf_object__find_prog_fd_by_name(bpf_obj, "trace_kernel_function");
bpf_prog_attach(kp_fd, prog_fd);
return 0;
}
在這個例子中,我們定義了一個BPF程式trace_kernel_function
,它會在核心函式kernel_function
被呼叫時執行。然後,我們使用bpf_prog_load_xattr
函式載入BPF程式,並使用bpf_prog_attach
函式將其附加到核心函式上。
使用tracepoints進行核心級別的追蹤
tracepoints是一種核心級別的追蹤機制,允許開發人員在核心中插入追蹤點。以下是使用tracepoints進行核心級別的追蹤的例子:
#include <linux/bpf.h>
int trace_kernel_function(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
bpf_trace_printk("New kernel process running with PID: %d", pid);
}
int main() {
struct bpf_prog_load_attr prog_load_attr = {
.prog_type = BPF_PROG_TYPE_TRACEPOINT,
.file = "kernel_function",
};
bpf_prog_load_xattr(&prog_load_attr, &prog_load);
bpf_object__find_prog_fd_by_name(bpf_obj, "trace_kernel_function");
bpf_prog_attach(tp_fd, prog_fd);
return 0;
}
在這個例子中,我們定義了一個BPF程式trace_kernel_function
,它會在核心函式kernel_function
被呼叫時執行。然後,我們使用bpf_prog_load_xattr
函式載入BPF程式,並使用bpf_prog_attach
函式將其附加到核心函式上。
使用者空間的追蹤
使用者空間的追蹤是指在使用者空間中插入追蹤點,以捕捉使用者空間程式的行為。這種方法可以提供對使用者空間程式的詳細瞭解,包括函式呼叫、變數存取等。
使用uprobes進行使用者空間的追蹤
uprobes是一種使用者空間的追蹤機制,允許開發人員在使用者空間中插入追蹤點。以下是使用uprobes進行使用者空間的追蹤的例子:
#include <linux/bpf.h>
int trace_user_function(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
bpf_trace_printk("New user process running with PID: %d", pid);
}
int main() {
struct bpf_prog_load_attr prog_load_attr = {
.prog_type = BPF_PROG_TYPE_UPROBE,
.file = "user_function",
};
bpf_prog_load_xattr(&prog_load_attr, &prog_load);
bpf_object__find_prog_fd_by_name(bpf_obj, "trace_user_function");
bpf_prog_attach(uprobe_fd, prog_fd);
return 0;
}
在這個例子中,我們定義了一個BPF程式trace_user_function
,它會在使用者空間程式user_function
被呼叫時執行。然後,我們使用bpf_prog_load_xattr
函式載入BPF程式,並使用bpf_prog_attach
函式將其附加到使用者空間程式上。
使用 BPF 進行追蹤
BPF(Berkeley Packet Filter)是一種強大的 Linux 核心追蹤工具,允許開發人員在使用者空間和核心空間中注入自定義程式碼,以便進行追蹤和監控。在本章中,我們將探討如何使用 BPF 進行追蹤,特別是使用 uprobes
和 uretprobes
來追蹤使用者空間程式的執行。
Uprobes
Uprobes
是用於追蹤使用者空間程式的工具,允許開發人員在特定指令上注入 BPF 程式碼。這些程式碼可以在指令執行前或執行後被觸發,從而提供對程式執行的詳細追蹤。
Uretprobes
Uretprobes
是 uprobes
的補充,允許開發人員在指令傳回時注入 BPF 程式碼。這些程式碼可以存取傳回值,並提供對程式執行的更詳細的追蹤。
結合 Uprobes 和 Uretprobes
透過結合 uprobes
和 uretprobes
,開發人員可以建立更複雜的 BPF 程式,以便更全面地瞭解應用程式的執行。這些程式可以在指令執行前和執行後注入程式碼,從而提供對應用程式行為的詳細追蹤。
範例:測量函式執行時間
以下範例展示瞭如何使用 uprobes
和 uretprobes
來測量 Go 程式中 main
函式的執行時間:
bpf_source = """
int trace_go_main(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
bpf_trace_printk("New hello-bpf process running with PID: %d", pid);
}
"""
bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "hello-bpf", sym = "main.main", fn_name = "trace_go_main")
bpf.trace_print()
這個範例建立了一個 BPF 程式,當 hello-bpf
程式的 main
函式執行時,該程式會被觸發。然後,程式會使用 bpf_trace_printk
函式將 PID 和函式名稱記錄到追蹤管道中。
接下來,範例建立了一個 BPF 散列表,以便在 uprobe
和 uretprobe
函式之間分享資料。然後,範例定義了一個 uretprobe
函式,當 main
函式傳回時被觸發:
bpf_source += """
static int print_duration(struct pt_regs *ctx) {
u64 pid = bpf_get_current_pid_tgid();
u64 start_time_ns = cache.lookup(&pid);
if (start_time_ns == 0) {
return 0;
}
u64 duration_ns = bpf_ktime_get_ns() - start_time_ns;
bpf_trace_printk("Function call duration: %d", duration_ns);
return 0;
}
"""
這個 uretprobe
函式會計算函式的執行時間,並使用 bpf_trace_printk
函式將結果記錄到追蹤管道中。
最後,範例將這兩個 BPF 函式附加到對應的探針上:
bpf = BPF(text = bpf_source)
bpf.attach_uprobe(name = "hello-bpf", sym = "main.main", fn_name = "trace_start_time")
bpf.attach_uretprobe(name = "hello-bpf", sym = "main.main", fn_name = "print_duration")
這樣,就可以使用 BPF 來測量 Go 程式中 main
函式的執行時間,並將結果記錄到追蹤管道中。
使用 BPF 進行使用者空間追蹤
在上一節中,我們探討瞭如何使用 BPF 來追蹤核心空間的操作。在這一節中,我們將關注如何使用 BPF 來追蹤使用者空間的操作。
從使用者空間程式追蹤到核心層級的效能分析,本文深入探討了BPF技術的應用與發展。BPF技術的日趨成熟,讓開發者得以透過kprobes、uretprobes、tracepoints等工具,深入理解系統的執行機制,並精確診斷效能瓶頸。觀察開源社群的活躍貢獻,BPF工具鏈生態正在快速發展,降低了使用門檻,也讓更多開發者得以應用BPF技術於系統和應用程式的效能調校。玄貓認為,BPF技術在可觀測性領域的應用潛力巨大,未來幾年將持續推動系統效能分析和除錯技術的革新,值得密切關注其發展趨勢並及早投入學習與應用。對於追求極致效能的開發團隊而言,掌握BPF技術將成為提升系統效能和穩定性的關鍵利器。