BPF和eBPF簡介

你好,哈布爾! 我們想通知您,我們正在準備出版一本書。”使用 BPF 實作 Linux 可觀察性".

BPF和eBPF簡介
由於 BPF 虛擬機器不斷發展並在實踐中積極使用,我們為您翻譯了一篇文章,描述其主要功能和當前狀態。

近年來,程式設計工具和技術越來越流行,以彌補 Linux 核心在需要高效能資料包處理的情況下的限制。 這種最受歡迎的技術之一稱為 核心繞過 (核心旁路)並允許繞過核心網路層,從用戶空間執行所有資料包處理。 繞過核心還涉及從以下位置控制網卡 使用者空間。 換句話說,當使用網卡時,我們依賴驅動程式 使用者空間.

透過將對網卡的完全控制權轉移給用戶空間程序,我們減少了核心開銷(上下文切換、網路層處理、中斷等),這在以 10Gb/s 或更高速度運行時非常重要。 核心旁路加上其他功能的組合(批量處理)和仔細的性能調整(NUMA 會計, CPU隔離等)對應於使用者空間中高效能網路處理的基礎。 也許這種新的資料包處理方法的一個例子是 DPDK 來自英特爾(數據平面開發套件),儘管還有其他眾所周知的工具和技術,包括 Cisco 的 VPP(向量包處理)、Netmap,當然還有 斯納布.

在使用者空間組織網路互動有很多缺點:

  • 作業系統核心是硬體資源的抽象層。 由於用戶空間程式必須直接管理其資源,因此它們還必須管理自己的硬體。 這通常意味著必須編寫自己的驅動程式。
  • 因為我們完全放棄了核心空間,所以我們也放棄了核心提供的所有網路功能。 使用者空間程式必須重新實作核心或作業系統可能已經提供的功能。
  • 程式在沙箱模式下運行,這嚴重限制了它們的交互並阻止它們與作業系統的其他部分整合。

本質上,當網路發生在用戶空間時,效能增益是透過將資料包處理從核心轉移到用戶空間來實現的。 XDP 的作用恰恰相反:它將網路程式從用戶空間(過濾器、解析器、路由等)移至核心空間。 XDP 讓我們在封包到達網路介面後、開始向上移動到核心網路子系統之前執行網路功能。 結果,資料包處理速度顯著提高。 然而,核心如何允許用戶在核心空間中執行他們的程式? 在回答這個問題之前,我們先來看看什麼是BPF。

BPF 和 eBPF

儘管名稱令人困惑,但 BPF(伯克利資料包過濾)實際上是一個虛擬機器模型。 該虛擬機器最初設計用於處理資料包過濾,因此得名。

使用 BPF 的最著名的工具之一是 tcpdump。 當使用捕獲資料包時 tcpdump 使用者可以指定一個表達式來過濾資料包。 只有匹配該表達式的資料包才會被捕獲。 例如,表達式“tcp dst port 80」 指到達連接埠 80 的所有 TCP 封包。編譯器可以透過將其轉換為 BPF 字節碼來縮短此表達式。

$ sudo tcpdump -d "tcp dst port 80"
(000) ldh [12] (001) jeq #0x86dd jt 2 jf 6
(002) ldb [20] (003) jeq #0x6 jt 4 jf 15
(004) ldh [56] (005) jeq #0x50 jt 14 jf 15
(006) jeq #0x800 jt 7 jf 15
(007) ldb [23] (008) jeq #0x6 jt 9 jf 15
(009) ldh [20] (010) jset #0x1fff jt 15 jf 11
(011) ldxb 4*([14]&0xf)
(012) ldh [x + 16] (013) jeq #0x50 jt 14 jf 15
(014) ret #262144
(015) ret #0

這就是上面的程序的基本作用:

  • 指令 (000):將偏移量 12 處的資料包作為 16 位元字載入到累加器中。 偏移量 12 對應於資料包的以太類型。
  • 指令(001):將累加器中的值與0x86dd(即IPv6的ethertype值)進行比較。 如果結果為真,則程式計數器會轉到指令(002),如果結果不為真,則程式計數器會轉到指令(006)。
  • 指令(006):將該值與 0x800(IPv4 的乙太網路類型值)進行比較。 如果答案為真,則程式轉到(007),否則,則轉到(015)。

依此類推,直到包過濾程序返回結果。 這通常是一個布林值。 傳回非零值(指令(014))表示封包被接受,傳回零值(指令(015))表示封包未被接受。

BPF 虛擬機器及其字節碼是由 Steve McCann 和 Van Jacobson 於 1992 年底發表論文時提出的 BSD 封包過濾器:用戶級封包擷取的新架構,這項技術首次在1993年冬天的Usenix會議上提出。

因為BPF是一個虛擬機,所以它定義了程式運作的環境。 除了字節碼之外,它還定義了批次記憶體模型(載入指令隱式應用於批次)、暫存器(A 和 X;累加器和索引暫存器)、臨時記憶體儲存和隱式程式計數器。 有趣的是,BPF 字節碼是按照 Motorola 6502 ISA 建模的。 正如史蒂夫·麥肯在他的書中回憶的那樣 全體會議報告 在 Sharkfest '11 上,他在高中時代在 Apple II 上編程時就熟悉了 build 6502,這些知識影響了他設計 BPF 字節碼的工作。

BPF 支援在 v2.5 及更高版本的 Linux 核心中實現,主要是由 Jay Schullist 努力添加的。 BPF 程式碼一直保持不變,直到 2011 年,Eric Dumaset 重新設計了 BPF 解譯器以 JIT 模式運作(資料來源: 用於資料包過濾器的 JIT)。 此後,核心可以直接將BPF程式轉換為目標架構:x86、ARM、MIPS等,而不是解釋BPF字節碼。

後來,在2014年,Alexey Starovoitov為BPF提出了一種新的JIT機制。 事實上,這個新的 JIT 成為了一個新的基於 BPF 的架構,被稱為 eBPF。 我認為這兩台虛擬機器共存了一段時間,但目前資料包過濾是基於 eBPF 實現的。 事實上,在現代文件的許多範例中,BPF 被理解為 eBPF,而經典 BPF 現在被稱為 cBPF。

eBPF 透過多種方式擴展了經典的 BPF 虛擬機器:

  • 基於現代 64 位元架構。 eBPF 使用 64 位元暫存器,並將可用暫存器的數量從 2 個(累加器和 X)增加到 10 個。eBPF 還提供額外的操作碼(BPF_MOV、BPF_JNE、BPF_CALL...)。
  • 與網路層子系統分離。 BPF 與批次資料模型相關。 由於它用於資料包過濾,因此其代碼位於提供網路通訊的子系統中。 然而,eBPF 虛擬機器不再與資料模型綁定,可以用於任何目的。 所以,現在eBPF程式可以連接到tracepoint或kprobe。 這為 eBPF 檢測、效能分析以及其他核心子系統上下文中的許多其他用例開闢了道路。 現在,eBPF 程式碼位於自己的路徑中:kernel/bpf。
  • 稱為地圖的全球資料儲存。 映射是鍵值存儲,可實現用戶空間和核心空間之間的資料交換。 eBPF 提供了多種類型的對應。
  • 次要功能。 特別是,重寫包、計算校驗和或克隆包。 這些函數在核心內部運行,不是用戶空間程式。 您也可以從 eBPF 程式進行系統呼叫。
  • 結束通話。 eBPF 中的程式大小限制為 4096 位元組。 尾部呼叫功能允許 eBPF 程序將控制權轉移到新的 eBPF 程序,從而繞過此限制(透過這種方式最多可以連結 32 個程序)。

eBPF:範例

Linux 核心原始碼中有幾個 eBPF 的範例。 它們可以在samples/bpf/ 中找到。 要編譯這些範例,只需輸入:

$ sudo make samples/bpf/

我不會自己為 eBPF 編寫新範例,但會使用樣本/bpf/ 中提供的範例之一。 我將查看程式碼的某些部分並解釋其工作原理。 作為示例,我選擇了該程序 tracex4.

一般來說,samples/bpf/ 中的每個範例都包含兩個檔案。 在這種情況下:

  • tracex4_kern.c,包含要在核心中作為 eBPF 字節碼執行的原始碼。
  • tracex4_user.c,包含來自使用者空間的程式。

在這種情況下,我們需要編譯 tracex4_kern.c 到 eBPF 字節碼。 目前在 gcc eBPF 沒有後端。 幸運的是, clang 可以輸出eBPF字節碼。 Makefile 用途 clang 用於編譯 tracex4_kern.c 到目標檔案。

我上面提到 eBPF 最有趣的功能之一是映射。 tracex4_kern 定義了一個映射:

struct pair {
    u64 val;
    u64 ip;
};  

struct bpf_map_def SEC("maps") my_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(long),
    .value_size = sizeof(struct pair),
    .max_entries = 1000000,
};

BPF_MAP_TYPE_HASH 是 eBPF 提供的多種卡片類型之一。 在這種情況下,它只是一個哈希值。 您可能還注意到一則廣告 SEC("maps")。 SEC 是一個宏,用於建立二進位檔案的新部分。 事實上,在例子中 tracex4_kern 另外定義了兩個部分:

SEC("kprobe/kmem_cache_free")
int bpf_prog1(struct pt_regs *ctx)
{   
    long ptr = PT_REGS_PARM2(ctx);

    bpf_map_delete_elem(&my_map, &ptr); 
    return 0;
}
    
SEC("kretprobe/kmem_cache_alloc_node") 
int bpf_prog2(struct pt_regs *ctx)
{
    long ptr = PT_REGS_RC(ctx);
    long ip = 0;

    // получаем ip-адрес вызывающей стороны kmem_cache_alloc_node() 
    BPF_KRETPROBE_READ_RET_IP(ip, ctx);

    struct pair v = {
        .val = bpf_ktime_get_ns(),
        .ip = ip,
    };
    
    bpf_map_update_elem(&my_map, &ptr, &v, BPF_ANY);
    return 0;
}   

這兩個函數允許您從地圖中刪除條目(kprobe/kmem_cache_free)並在地圖上新增一個條目(kretprobe/kmem_cache_alloc_node)。 所有以大寫字母書寫的函數名稱都對應於中定義的宏 bpf_helpers.h.

如果我轉儲目標檔案的部分,我應該會看到這些新部分已經定義:

$ objdump -h tracex4_kern.o

tracex4_kern.o: file format elf64-little

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2
CONTENTS, ALLOC, LOAD, DATA
4 license 00000004 0000000000000000 0000000000000000 00000164 2**0
CONTENTS, ALLOC, LOAD, DATA
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
CONTENTS, ALLOC, LOAD, DATA
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

還有 tracex4_user.c,主程式。 基本上,這個程式監聽事件 kmem_cache_alloc_node。 當此類事件發生時,就會執行對應的 eBPF 程式碼。 程式碼將物件的IP屬性儲存到一個map中,然後在主程式中循環出該物件。 例子:

$ sudo ./tracex4
obj 0xffff8d6430f60a00 is 2sec old was allocated at ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 is 23sec old was allocated at ip ffffffff98090e8f
obj 0xffff8d5f80161780 is 6sec old was allocated at ip ffffffff98090e8f

使用者空間程式和 eBPF 程式如何相關? 初始化時 tracex4_user.c 載入目標文件 tracex4_kern.o 使用函數 load_bpf_file.

int main(int ac, char **argv)
{
    struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
    char filename[256];
    int i;

    snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);

    if (setrlimit(RLIMIT_MEMLOCK, &r)) {
        perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
        return 1;
    }

    if (load_bpf_file(filename)) {
        printf("%s", bpf_log_buf);
        return 1;
    }

    for (i = 0; ; i++) {
        print_old_objects(map_fd[1]);
        sleep(1);
    }

    return 0;
}

通過做 load_bpf_file eBPF 檔案中定義的探針被加入到 /sys/kernel/debug/tracing/kprobe_events。 現在我們監聽這些事件,當它們發生時我們的程式可以做一些事情。

$ sudo cat /sys/kernel/debug/tracing/kprobe_events
p:kprobes/kmem_cache_free kmem_cache_free
r:kprobes/kmem_cache_alloc_node kmem_cache_alloc_node

Sample/bpf/ 中的所有其他程式的結構都類似。 它們始終包含兩個檔案:

  • XXX_kern.c:eBPF 程式。
  • XXX_user.c: 主程式。

eBPF 程式識別與節相關的映射和函數。 當內核發出某種類型的事件時(例如, tracepoint),執行綁定的函數。 這些卡片提供核心程式和用戶空間程式之間的通訊。

結論

本文一般性地討論了 BPF 和 eBPF。 我知道今天有很多關於 eBPF 的資訊和資源,所以我會推薦一些更多的資源以供進一步學習

我建議閱讀:

來源: www.habr.com

添加評論