1. 程式人生 > 其它 >BPF、eBPF與XDP簡介與使用

BPF、eBPF與XDP簡介與使用

大雜燴,基本翻譯自

A brief introduction to XDP and eBPF

The eXpress Data Path

xdp-ebpf 簡介

Kernel Bypass

在過去幾年中,我們看到了程式設計工具包和技術的升級,以克服Linux kernel的限制,來進行高效能資料包處理。最流行的技術之一是kernel bypass(核心旁路),這意味著跳過核心的網路層,在使用者態(user-sapce)做全部的包處理。kernel bypass涉及從user-space管理NIC(network interface controller,也就是常說的網絡卡),也就是說需要使用者態的驅動程式(user space driver)來處理NIC

使用者態程式完全控制NIC,有什麼好處呢?減少了核心開銷;等

壞處呢?使用者程式需要直接管理硬體;kernel被完全跳過,所以核心提供的所有網路功能也被跳過,使用者程式可能需要實現一些原來核心提供的功能;

本質上kernel bypass實現高效能包處理是通過將資料包從kernel移動到user-space

XDP(後面會講)實際上正好相反,XDP允許我們在資料包到達NIC時,在它移動到kernel’s networking subsystem之前,執行我們定義的處理函式,從而顯著提高資料包處理速度。但是使用者態定義的程式如何在核心中執行呢?

這就用到了BPF,BPF就是一種在核心中執行使用者指定的程式的設計

BPF

Berkeley packet filter,用於過濾網路報文(packet)

是tcpdump(linux)和wireshark(windows)乃至整個網路監控(network monitoring)的基石

BPF實際上並不只是包處理,而更像一個VM(virtual machine)

BPF虛擬機器及其位元組碼由Steve McCanne和Van Jacobson於1992年底在其論文《The BSD Packet Filter: A New Architecture for User-level Packet Capture》中介紹,並首次在1993年冬季Usenix會議上提出。

由於BPF是一個VM,它定義了一個程式執行的環境。除了位元組碼,它還定義了基於資料包的記憶體模型(packet-based memory model)、暫存器(A and X; Accumulator ans Index register)、暫存記憶體(scratch memory)、隱式程式計數器(implicit pc)。有趣的是,BPF的位元組碼是模仿摩托羅拉6502ISA的。Steve McCanne在他的Sharkfest ‘11 keynote主題演講中回憶道,他在初中時就熟悉6502 assembly在Apple II上的程式設計,這在他設計BPF位元組碼時對他產生了影響

Linux核心從v2.5開始就支援BPF,主要由Jay Schullist新增。直到2011年,BPF程式碼才發生重大變化,Eric Dumazet將BPF直譯器轉換為JIT(來源:A JIT for packet filters)。現在核心不再解釋BPF位元組碼,而是能夠將BPF程式直接轉換為目標體系結構:x86、ARM、MIPS等。

隨後,在2014年,Alexei Starovoitov引入了新的BPF JIT。這種新的JIT實際上是一種基於BPF的新體系結構,稱為eBPF。我認為這兩個虛擬機器共存了一段時間,但現在包過濾是在eBPF之上實現的。事實上,許多文件現在將eBPF稱為BPF,而經典的BPF稱為cBPF。

eBPF

eBPF在以下幾個方面擴充套件了傳統的BPF虛擬機器:

  • 利用現代64位體系結構。eBPF使用64位暫存器,並將可用暫存器的數量從2(累加器和X暫存器)增加到10。eBPF還擴充套件了操作碼的數量(BPF_MOV、BPF_JNE、BPF_CALL…
  • 與網路子系統分離。BPF被繫結到基於資料包的資料模型。由於它被用於資料包過濾,其程式碼位於網路子系統中。但是,eBPF VM不再侷限於資料模型,它可以用於任何目的。現在可以將eBPF程式連線到跟蹤點或kprobe。這為eBPF在其他核心子系統中的插裝、效能分析和更多用途打開了大門。eBPF程式碼現在位於自己的路徑:kernel/bpf
  • 增加Maps用來儲存全域性資料。Maps是鍵值對的儲存方式,允許在user-sapce和kernel-space做資料互動。eBPF提供了多種型別的Map
  • 增加輔助函式(helper function)。例如資料包重寫、校驗和計算或資料包克隆。與使用者空間程式設計不同,這些函式在核心中執行。此外,還可以從eBPF程式執行系統呼叫
  • 增加尾呼叫(tail call)。eBPF程式限制為4096位元組。尾部呼叫功能允許eBPF程式通過控制一個新的eBPF程式,從而克服此限制(最多可以連結32個程式)

eBPF怎麼使用呢?

看一個例子,也是Linux kernel自帶的樣例。它們可在 samples/bpf/上獲得。要編譯這些示例,可參考我前面的一篇文章。

我們選擇 tracex4程式分析,eBPF程式設計通常包括兩個程式:eBPF程式和user-sapce程式

首先我們需要將tracex4_kern.c程式設計成eBPF bytecode,gcc缺乏BPF後端,幸運的是,Clang支援,自帶的Makefile利用Clang將trace4_kern.c編譯成一個目標檔案(object file)

閱讀以下tracex4_kern.c原始碼:

Maps are key/value stores that allow to exchange data between user-space and kernel-space programs. tracex4_kern defines one map:

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提供的多個Map中的一個,你還能看到 SEC("map"),SEC是一個巨集用來在二進位制檔案(目標檔案,.o檔案)中生成一個新的section

tracex4_kern.c還定義了另外兩個section:

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;

    // get ip address of kmem_cache_alloc_node() caller
    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;
}   

這兩個函式允許我們在map中增加一個entry(kprobe/kmem_cache_free) 和新增一條entry(kretprobe/kmem_cache_alloc_node)。

所有大寫字母的函式實際上都是巨集,定義在 bpf_helpers.h

如果我們反彙編目標檔案,我們可以看見新的section被定義:

$ 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

main program是 tracex4_user.c,大體上,這個程式的作用就是監聽kmem_cache_alloc_node上的事件,當事件發生時,對應的eBPF code會被執行,且把ip資訊儲存到Map,main program從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

這user-sapce program 和 eBPF program是怎麼連線在一起的?在初始化的時候,tracex4_user.c<c/ode> 使用load_bpf_file 載入 tracex4_kern.o

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檔案中定義的探測(kprobe)將新增到/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

XDP

XDP的設計源於Cloudflare在Netdev 1.1上提出的DDoS攻擊緩解解決方案

因為Cloudflare希望保持使用iptables(以及核心網路堆疊的其餘部分)的便利性,所以他們無法使用完全控制硬體的解決方案(即前面的kernel bypass),例如DPDK

Cloudflare的解決方案使用Netmap工具包實現其部分核心旁路(partial kernel bypass)(來源:Single Rx queue kernel bypass with Netmap)。這個想法可以通過在Linux核心網路堆疊中新增一個檢查點(checkpoint),最好是在NIC中接收到資料包之後。該checkpoint應將資料包傳遞給eBPF程式,該程式將決定如何處理該資料包:丟棄該資料包(drop)或讓其繼續通過正常路徑(pass). 就像這幅圖一樣:

Example: An IPv6 packet filter

介紹XDP的典型例子是DDos過濾器,它的作用是:果資料包來自可疑來源,就丟棄它們。在我的例子中,我將使用更簡單的功能:一個過濾除IPv6之外的所有流量的功能。

為了簡單處理,我們不需要管理可疑地址列表。我們只簡單地檢查資料包的ethertype值,並讓它繼續通過網路堆疊(network stack),或者根據是否是IPv6資料包丟棄它。

SEC("prog")
int xdp_ipv6_filter_program(struct xdp_md *ctx)
{
    void *data_end = (void *)(long)ctx->data_end;
    void *data     = (void *)(long)ctx->data;
    struct ethhdr *eth = data;
    u16 eth_type = 0;

    if (!(parse_eth(eth, data_end, eth_type))) {
        bpf_debug("Debug: Cannot parse L2\n");
        return XDP_PASS;
    }

    bpf_debug("Debug: eth_type:0x%x\n", ntohs(eth_type));
    if (eth_type == ntohs(0x86dd)) {
        return XDP_PASS;
    } else {
        return XDP_DROP;
    }
}

函式xdp_ipv6_filter_程式是我們的主程式。我們在二進位制檔案中定義了一個稱為prog的新部分。這是我們的程式和XDP之間的掛鉤。每當XDP收到一個數據包,我們的程式碼就會被執行。

CTX表示一個上下文,一個包含訪問資料包所需的所有資料的結構。我們的程式呼叫parse_eth來獲取ethertype。然後檢查其值是否為0x86dd(IPv6乙太網型別),如果是,資料包將通過。否則,資料包將被丟棄。此外,出於除錯目的,所有ethertype值都會打印出來。

bpf_debug實際上是一個巨集,定義如下:

#define bpf_debug(fmt, ...)                          \
    ({                                               \
        char ____fmt[] = fmt;                        \
        bpf_trace_printk(____fmt, sizeof(____fmt),   \
            ##__VA_ARGS__);                          \
    })

內部其實也是呼叫了bpf_trace_printk,這個函式會列印在/sys/kernel/debug/tracing/trace_pipe中的資訊

函式parse_eth獲取資料包的開頭和結尾,並解析其內容:

static __always_inline
bool parse_eth(struct ethhdr *eth, void *data_end, u16 *eth_type)
{
    u64 offset;

    offset = sizeof(*eth);
    if ((void *)eth + offset > data_end)
        return false;
    *eth_type = eth->h_proto;
    return true;
}

在核心中執行外部程式碼涉及某些風險。例如,無限迴圈可能會凍結核心,或者程式可能會訪問不受限制的記憶體區域。為避免這些潛在危險,在載入eBPF程式碼時執行驗證器。驗證器遍歷所有可能的程式碼路徑,檢查我們的程式沒有訪問超出範圍的記憶體,也沒有越界跳轉;驗證器還確保程式在有限時間內終止。
我們的eBPF程式符合這些要求。現在我們只需要編譯它(完整的原始碼可以在:xdp_ipv6_filter上找到)。

$ make

這會生成xdp_ipv6_filter.o,一個eBPF object file

現在我們需要把這個object file載入到network interface,這有兩種方式可以做到這一點:

  • 寫一個user-space program載入目標檔案到network interface
  • 使用iproute來載入目標檔案到interface

在這個例子中,我們將使用後一種方法

目前,支援XDP的網路介面數量有限(ixgbe、i40e、mlx5、veth、tap、tun、virtio_net和其他),儘管數量在不斷增加。其中一些網路介面在驅動程式級別支援XDP(言下之意有些還不能在驅動級別)。這意味著,XDP鉤子是在網路層的最低點實現的,就在NIC在Rx ring中接收到資料包的時候。在其他情況下,XDP鉤子在網路堆疊中的較高點實現。前者提供了更好的效能結果,儘管後者使XDP可用於任何網路介面。

幸運的是,XDP支援veth interfaces,我將建立一個veth對,並將eBPF程式連線到它的一端。記住veth總是成對的,它就像一根虛擬電纜連線兩個埠,任何在一端傳送的東西都會到達另一端,反之亦然。

$ sudo ip link add dev veth0 type veth peer name veth1
$ sudo ip link set up dev veth0
$ sudo ip link set up dev veth1

現在我們將eBPF program attach到veth1上:

$ sudo ip link set dev veth1 xdp object xdp_ipv6_filter.o

您可能已經注意到,我將eBPF程式的部分稱為“prog”。這是iproute2希望查詢的節的名稱,使用其他名稱命名該節將導致錯誤。
如果程式成功載入,我將在veth1介面中看到一個xdp標誌:

$ sudo ip link sh veth1
8: veth1@veth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 xdp qdisc noqueue state UP mode DEFAULT group default qlen 1000
    link/ether 32:05:fc:9a:d8:75 brd ff:ff:ff:ff:ff:ff
    prog/xdp id 32 tag bdb81fb6a5cf3154 jited

為了驗證我的程式是否按預期工作,我將把IPv4和IPv6資料包的混合推送到veth0(IPv4-and-IPv6-data.pcap)。我的示例總共有20個數據包(10個IPv4和10個IPv6)。但在這樣做之前,我將在veth1上啟動一個tcpdump程式,它只准備捕獲10個IPv6資料包。

$ sudo tcpdump "ip6" -i veth1 -w captured.pcap -c 10
tcpdump: listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes

送packets到veth0:

$ sudo tcpreplay -i veth0 ipv4-and-ipv6-data.pcap

過濾後的資料包到達另一端。由於收到了所有預期的資料包,tcpdump程式終止。

10 packets captured
10 packets received by filter
0 packets dropped by kernel

我們也可以打印出/sys/kernel/debug/tracing/trace_pipe,來檢查ethertype value.

$ sudo cat /sys/kernel/debug/tracing/trace_pipe
tcpreplay-4496  [003] ..s1 15472.046835: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046847: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046855: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046862: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046869: 0: Debug: eth_type:0x86dd
tcpreplay-4496  [003] ..s1 15472.046878: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046885: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046892: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046903: 0: Debug: eth_type:0x800
tcpreplay-4496  [003] ..s1 15472.046911: 0: Debug: eth_type:0x800
...

非常建議大家親自動手做這個實驗的!!

最後mark一些沒詳細看完的資料:

狄衛華_E B P F 技術簡介

LINUX.CONG.AU_BPF: Tracing and More

Taiwan Linux Kernel Hackers_主題分享:Introduction to eBPF and XDP

個性簽名:時間會解決一切