Kurze Einführung in BPF und eBPF

Hallo Habr! Wir informieren Sie, dass wir uns auf die Veröffentlichung des Buches " Linux Observability with BPF " vorbereiten .


Da sich die virtuelle BPF-Maschine weiterentwickelt und in der Praxis aktiv angewendet wird, haben wir einen Artikel für Sie übersetzt, in dem die Hauptfunktionen und der aktuelle Status beschrieben werden.

In den letzten Jahren haben Programmiertools und -techniken an Popularität gewonnen, um die Einschränkungen des Linux-Kernels in Fällen auszugleichen, in denen eine leistungsstarke Paketverarbeitung erforderlich ist. Eine der beliebtesten Methoden dieser Art heißt das Umgehen des Kernels (Kernel-Bypass) und ermöglicht das Übergeben von Netzwerkschichtkernen die gesamte Paketverarbeitung vom Benutzerraum aus. Das Umgehen des Kernels umfasst auch das Verwalten der Netzwerkkarte vom Benutzerbereich aus . Mit anderen Worten, wenn wir mit einer Netzwerkkarte arbeiten, verlassen wir uns auf den User Space- Treiber .

Durch die Übertragung der vollständigen Kontrolle über die Netzwerkkarte aus dem Benutzerbereich auf das Programm reduzieren wir die mit dem Kernel verbundenen Kosten (Switching-Kontext, Verarbeitung der Netzwerkebene, Interrupts usw.), was bei Arbeiten mit einer Geschwindigkeit von 10 Gbit / s oder höher sehr wichtig ist. Der Kernel-Bypass sowie eine Kombination aus anderen Funktionen ( Stapelverarbeitung ) und einer genauen Leistungsoptimierung ( NUMA-Abrechnung , CPU-Isolation usw.) entsprechen den Grundlagen der Hochleistungs-Netzwerkverarbeitung im Benutzerbereich. Ein Musterbeispiel für diesen neuen Ansatz zur Paketverarbeitung ist möglicherweise das DPDK ( Data Plane Development Kit) von Intel), obwohl es andere bekannte Tools und Techniken gibt, darunter VPP von Cisco (Vector Packet Processing), Netmap und natürlich Snabb .

Die Organisation von Netzwerkinteraktionen im Benutzerbereich weist eine Reihe von Nachteilen auf:

  • Der Kernel des Betriebssystems ist die Abstraktionsebene für Hardwareressourcen. Da User-Space-Programme ihre Ressourcen direkt verwalten müssen, müssen sie auch ihre eigene Hardware verwalten. Dies bedeutet häufig, dass Sie Ihre eigenen Treiber programmieren müssen.
  • , , . , , , .
  • , .

Im Wesentlichen wird beim Organisieren von Netzwerkinteraktionen im Benutzerbereich eine Steigerung der Produktivität erreicht, indem die Paketverarbeitung vom Kernel in den Benutzerbereich übertragen wird. XDP macht genau das Gegenteil: Verschiebt Netzwerkprogramme aus dem Benutzerbereich (Filter, Konverter, Routing usw.) in den Kernelbereich. Mit XDP können wir eine Netzwerkfunktion ausführen, sobald das Paket an der Netzwerkschnittstelle ankommt und bevor es in das Netzwerksubsystem des Kernels verschoben wird. Infolgedessen steigt die Paketverarbeitungsgeschwindigkeit erheblich an. Wie erlaubt der Kernel dem Benutzer jedoch, seine Programme im Kernelraum auszuführen? Bevor wir diese Frage beantworten, schauen wir uns an, was BPF ist.

BPF und eBPF

Trotz des nicht so klaren Namens BPF (Packet Filtering, Berkeley) handelt es sich tatsächlich um ein Modell einer virtuellen Maschine. Diese virtuelle Maschine wurde ursprünglich für die Paketfilterung entwickelt, daher der Name.

Eines der bekanntesten Tools mit BPF ist tcpdump. Beim Erfassen von Paketen mit kann der tcpdumpBenutzer einen Ausdruck zum Filtern von Paketen angeben. Es werden nur Pakete erfasst, die diesem Ausdruck entsprechen. Beispielsweise gilt der Ausdruck " tcp dst port 80" für alle TCP-Pakete, die an Port 80 ankommen. Der Compiler kann diesen Ausdruck verkürzen, indem er ihn in BPF-Bytecode konvertiert. Das obige Programm macht im Grunde Folgendes:

$ 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




  • Anweisung (000): Lädt ein Paket mit Offset 12 als 16-Bit-Wort in die Batterie herunter. Ein Offset von 12 entspricht einem Ethertyp-Paket.
  • (001): 0x86dd, , ethertype- IPv6. true, (002), – (006).
  • (006): 0x800 (ethertype- IPv4). true, (007), – (015).

Und so weiter, bis das Paketfilterprogramm ein Ergebnis zurückgibt. Dies ist normalerweise ein Bulean. Die Rückgabe eines Wertes ungleich Null (Befehl (014)) bedeutet, dass sich das Paket nähert, und die Rückgabe von Null (Befehl (015)) bedeutet, dass das Paket nicht angekommen ist.

Die virtuelle BPF-Maschine und ihr Bytecode wurden Ende 1992 von Steve McCann und Van Jacobson vorgeschlagen, als ihr Artikel BSD-Paketfilter: Eine neue Architektur für die Paketerfassung auf Benutzerebene erstmals auf einer Usenix-Konferenz im Winter 1993 vorgestellt wurde.

Da BPF eine virtuelle Maschine ist, definiert es die Umgebung, in der Programme ausgeführt werden. Zusätzlich zum Bytecode werden ein Batch-Speichermodell (Startanweisungen werden implizit auf das Paket angewendet), Register (A und X; Batterie- und Indexregister), Arbeitsspeicher und ein impliziter Programmzähler definiert. Interessanterweise wurde der BPF-Bytecode dem Motorola 6502 ISA nachempfunden. Wie Steve McCann in seinem Plenarvortrag auf dem Sharkfest '11 erinnerte, war er seit der High School, als er in Apple II programmierte, mit dem 6502-Build vertraut, und dieses Wissen beeinflusste seine Arbeit beim Entwerfen von BPF-Bytecode.

Die Unterstützung für BPF ist im Linux-Kernel ab Version 2.5 implementiert, was hauptsächlich durch die Bemühungen von Jay Schullist ergänzt wurde. Der BPF-Code blieb bis 2011 unverändert, als Eric Dumazett den BPF-Interpreter so überarbeitete, dass er im JIT-Modus arbeitet (Quelle: JIT für Paketfilter ). Danach könnte der Kernel anstelle der Interpretation des BPF-Bytecodes die BPF-Programme direkt in die Zielarchitektur konvertieren: x86, ARM, MIPS usw.

Später, im Jahr 2014, schlug Alexey Starovoitov einen neuen JIT-Mechanismus für BPF vor. Tatsächlich ist diese neue JIT zur neuen BPF-basierten Architektur geworden und heißt eBPF. Ich denke, dass beide virtuellen Maschinen für einige Zeit nebeneinander existierten, aber derzeit ist die Paketfilterung basierend auf eBPF implementiert. In vielen Beispielen moderner Dokumentation wird unter BPF eBPF verstanden, und klassisches BPF wird heute als cBPF bezeichnet.

eBPF erweitert die klassische virtuelle BPF-Maschine auf verschiedene Weise:

  • Basierend auf modernen 64-Bit-Architekturen. eBPF verwendet 64-Bit-Register und erhöht die Anzahl der verfügbaren Register von 2 (Batterie und X) auf 10. eBPF bietet auch zusätzliche Betriebscodes (BPF_MOV, BPF_JNE, BPF_CALL ...).
  • . BPF . , , . , eBPF . , eBPF tracepoint kprobe. eBPF, . eBPF : kernel/bpf.
  • . – «-», . eBPF .
  • . , , . . , eBPF .
  • . eBPF 4096 . eBPF eBPF- ( 32 ).

eBPF: Ein Beispiel

In den Linux-Kernelquellen gibt es mehrere Beispiele für eBPF. Sie sind unter samples / bpf / verfügbar. Um diese Beispiele zu kompilieren, geben Sie einfach

$ sudo make samples/bpf/

Folgendes ein: Ich werde selbst kein neues Beispiel für eBPF schreiben, sondern eines der in samples / bpf / verfügbaren Beispiele verwenden. Ich werde mir einige Abschnitte des Codes ansehen und erklären, wie es funktioniert. Als Beispiel habe ich ein Programm gewählt tracex4.

Im Allgemeinen besteht jedes der Beispiele in samples / bpf / aus zwei Dateien. In diesem Fall:

  • tracex4_kern.c, enthält den Quellcode, der im Kernel als eBPF-Bytecode ausgeführt werden muss.
  • tracex4_user.c, enthält ein Programm aus dem User Space.

In diesem Fall müssen wir den tracex4_kern.ceBPF in Bytecode kompilieren . Derzeit gibt es gcckeinen Serverteil für eBPF. Glücklicherweise kann es clangeBPF-Bytecode ausgeben. Makefilewird clangzum Kompilieren tracex4_kern.ceiner Objektdatei verwendet.

Ich habe oben erwähnt, dass eine der interessantesten Funktionen von eBPFs Karten sind. tracex4_kern definiert eine 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- Eine der vielen Arten von Karten, die von eBPF angeboten werden. In diesem Fall ist es nur ein Hash. Möglicherweise haben Sie auch eine Anzeige bemerkt SEC("maps"). SEC ist ein Makro, mit dem ein neuer Abschnitt einer Binärdatei erstellt wird. Tatsächlich tracex4_kerndefiniert das Beispiel zwei weitere Abschnitte:

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;
}   

Mit diesen beiden Funktionen können Sie einen Eintrag von der Karte löschen ( kprobe/kmem_cache_free) und der Karte einen neuen Datensatz ( kretprobe/kmem_cache_alloc_node) hinzufügen . Alle in Großbuchstaben geschriebenen Funktionsnamen entsprechen den in definierten Makros bpf_helpers.h.

Wenn ich einen Speicherauszug von Abschnitten der Objektdatei ausgebe, sollte ich sehen, dass diese neuen Abschnitte bereits definiert sind: Immer noch das Hauptprogramm. Grundsätzlich hört dieses Programm Ereignisse ab . Wenn ein solches Ereignis eintritt, wird der entsprechende eBPF-Code ausgeführt. Der Code speichert das IP-Attribut des Objekts in der Karte, und dieses Objekt wird dann zyklisch im Hauptprogramm angezeigt. Beispiel: Wie hängen das User Space-Programm und das eBPF-Programm zusammen? Bei der Initialisierung wird eine Objektdatei mithilfe einer Funktion geladen .

$ 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.ckmem_cache_alloc_node

$ 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


tracex4_user.ctracex4_kern.oload_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;
}

Bei der Ausführung werden die load_bpf_filein der eBPF-Datei definierten Sonden hinzugefügt /sys/kernel/debug/tracing/kprobe_events. Jetzt hören wir uns diese Ereignisse an und unser Programm kann etwas tun, wenn sie eintreten. Alle anderen Programme in sample / bpf / sind ähnlich aufgebaut. Sie haben immer zwei Dateien:

$ 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




  • XXX_kern.c: eBPF-Programm.
  • XXX_user.c: Hauptprogramm.

Das eBPF-Programm definiert die Karten und Funktionen, die dem Abschnitt zugeordnet sind. Wenn der Kernel ein Ereignis eines bestimmten Typs auslöst (z. B. tracepoint), werden die gebundenen Funktionen ausgeführt. Maps ermöglichen den Datenaustausch zwischen dem Kernelprogramm und dem User Space-Programm.

Schlussfolgerung In

diesem Artikel wurden BPF und eBPF beschrieben. Ich weiß, dass es heute viele Informationen und Ressourcen zu eBPF gibt, daher empfehle ich einige weitere Materialien für weitere Studien. Ich

empfehle zu lesen:


All Articles