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_elembpf_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_elembpf_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 藉助探測點來收集除錯和分析的資訊。

型別的探測點

在本章中,我們將涵蓋四種不同型別的探測點:

  1. 核心探測點(Kernel Probes):提供對核心內部元件的動態存取。
  2. 追蹤點(Tracepoints):提供對核心內部元件的靜態存取。
  3. 使用者空間探測點(User-space Probes):提供對使用者空間程式的動態存取。
  4. 使用者靜態定義追蹤點(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 進行追蹤,特別是使用 uprobesuretprobes 來追蹤使用者空間程式的執行。

Uprobes

Uprobes 是用於追蹤使用者空間程式的工具,允許開發人員在特定指令上注入 BPF 程式碼。這些程式碼可以在指令執行前或執行後被觸發,從而提供對程式執行的詳細追蹤。

Uretprobes

Uretprobesuprobes 的補充,允許開發人員在指令傳回時注入 BPF 程式碼。這些程式碼可以存取傳回值,並提供對程式執行的更詳細的追蹤。

結合 Uprobes 和 Uretprobes

透過結合 uprobesuretprobes,開發人員可以建立更複雜的 BPF 程式,以便更全面地瞭解應用程式的執行。這些程式可以在指令執行前和執行後注入程式碼,從而提供對應用程式行為的詳細追蹤。

範例:測量函式執行時間

以下範例展示瞭如何使用 uprobesuretprobes 來測量 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 散列表,以便在 uprobeuretprobe 函式之間分享資料。然後,範例定義了一個 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技術將成為提升系統效能和穩定性的關鍵利器。