Eine kurze Einführung in BPF und eBPF

Hey Habr! Wir informieren Sie, dass wir die Veröffentlichung eines Buches vorbereiten.Linux-Beobachtbarkeit mit BPF".

Eine kurze Einführung in BPF und eBPF
Da sich die virtuelle BPF-Maschine ständig weiterentwickelt und aktiv in der Praxis eingesetzt wird, haben wir für Sie einen Artikel übersetzt, der ihre Hauptfunktionen und den aktuellen Stand beschreibt.

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 Kernbypass (Kernel-Bypass) und ermöglicht unter Überspringen der Netzwerkschicht des Kernels die gesamte Paketverarbeitung aus dem Benutzerbereich. Zur Umgehung des Kernels gehört auch die Verwaltung der Netzwerkkarte Benutzerraum. Mit anderen Worten: Bei der Arbeit mit einer Netzwerkkarte sind wir auf den Treiber angewiesen Benutzerraum.

Durch die Übertragung der vollständigen Kontrolle über die Netzwerkkarte an ein User-Space-Programm reduzieren wir den Overhead des Kernels (Kontextwechsel, Verarbeitung auf Netzwerkebene, Interrupts usw.), was bei Geschwindigkeiten von 10 Gbit/s oder sehr wichtig ist höher. Umgehen des Kernels plus eine Kombination anderer Funktionen (Stapelverarbeitung) und sorgfältige Leistungsoptimierung (NUMA-Buchhaltung, CPU-Isolationusw.) entsprechen den Grundlagen einer leistungsstarken User-Space-Vernetzung. Ein vielleicht beispielhaftes Beispiel für diesen neuen Ansatz zur Paketverarbeitung ist DPDK von Intel (Data Plane-Entwicklungskit), obwohl es auch andere bekannte Tools und Techniken gibt, darunter VPP von Cisco (Vector Packet Processing), Netmap und natürlich Schnell.

Die Organisation von Netzwerkinteraktionen im Benutzerraum hat eine Reihe von Nachteilen:

  • Ein Betriebssystemkernel ist eine Abstraktionsschicht für Hardwareressourcen. Da User-Space-Programme ihre Ressourcen direkt verwalten müssen, müssen sie auch ihre eigene Hardware verwalten. Dies bedeutet oft, dass Sie Ihre eigenen Treiber programmieren müssen.
  • Da wir den Kernel-Speicherplatz vollständig aufgeben, geben wir auch alle vom Kernel bereitgestellten Netzwerkfunktionen auf. User-Space-Programme müssen Funktionen neu implementieren, die möglicherweise bereits vom Kernel oder Betriebssystem bereitgestellt werden.
  • Programme arbeiten im Sandbox-Modus, was ihre Interaktion erheblich einschränkt und ihre Integration in andere Teile des Betriebssystems verhindert.

Im Wesentlichen werden bei der Vernetzung im User Space Leistungssteigerungen dadurch erzielt, dass die Paketverarbeitung vom Kernel in den User Space verlagert wird. XDP macht genau das Gegenteil: Es verschiebt Netzwerkprogramme vom Userspace (Filter, Konverter, Routing etc.) in den Kernel-Bereich. Mit XDP können wir die Netzwerkfunktion ausführen, sobald das Paket die Netzwerkschnittstelle erreicht und bevor es zum Netzwerksubsystem des Kernels gelangt. Dadurch wird die Paketverarbeitungsgeschwindigkeit deutlich erhöht. Doch wie ermöglicht der Kernel dem Benutzer, seine Programme im Kernel-Space auszuführen? Bevor wir diese Frage beantworten, schauen wir uns an, was BPF ist.

BPF und eBPF

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

Eines der bekannteren Tools, die BPF verwenden, ist tcpdump. Beim Erfassen von Paketen mit tcpdump Der Benutzer kann einen Ausdruck für die Paketfilterung angeben. Nur Pakete, die diesem Ausdruck entsprechen, werden erfasst. Zum Beispiel der Ausdruck „tcp dst port 80„bezieht sich auf alle TCP-Pakete, die an Port 80 ankommen. Der Compiler kann diesen Ausdruck verkürzen, indem er ihn in BPF-Bytecode konvertiert.

$ 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

Das ist im Grunde das, was das obige Programm macht:

  • Befehl (000): Lädt das Paket bei Offset 12 als 16-Bit-Wort in den Akkumulator. Offset 12 entspricht dem Ethertyp des Pakets.
  • Anweisung (001): vergleicht den Wert im Akkumulator mit 0x86dd, also mit dem Ethertype-Wert für IPv6. Wenn das Ergebnis wahr ist, geht der Programmzähler zu Befehl (002), andernfalls zu (006).
  • Anweisung (006): vergleicht den Wert mit 0x800 (Ethertype-Wert für IPv4). Wenn die Antwort wahr ist, geht das Programm zu (007), wenn nicht, dann zu (015).

Und so weiter, bis das Paketfilterprogramm ein Ergebnis zurückgibt. Normalerweise ist es boolesch. Die Rückgabe eines Werts ungleich Null (Anweisung (014)) bedeutet, dass das Paket übereinstimmte, und die Rückgabe von Null (Anweisung (015)) bedeutet, dass das Paket nicht übereinstimmte.

Die virtuelle BPF-Maschine und ihr Bytecode wurden Ende 1992 von Steve McCann und Van Jacobson vorgeschlagen, als ihre Arbeit erschien. BSD-Paketfilter: Neue Architektur für die Paketerfassung auf BenutzerebeneErstmals wurde diese Technologie auf der Usenix-Konferenz im Winter 1993 vorgestellt.

Da es sich bei BPF um eine virtuelle Maschine handelt, definiert es die Umgebung, in der Programme ausgeführt werden. Zusätzlich zum Bytecode definiert es auch ein Paketspeichermodell (Ladeanweisungen werden implizit auf ein Paket angewendet), Register (A und X; Akkumulator- und Indexregister), Arbeitsspeicherspeicher und einen impliziten Programmzähler. Interessanterweise wurde der BPF-Bytecode dem Motorola 6502 ISA nachempfunden. Wie Steve McCann sich in seinem erinnerte Plenarbericht Beim Sharkfest '11 war er mit Build 6502 aus der High School vertraut, als er auf dem Apple II programmierte, und dieses Wissen beeinflusste seine Arbeit beim Entwerfen des BPF-Bytecodes.

Die BPF-Unterstützung ist im Linux-Kernel ab Version 2.5 implementiert und wurde hauptsächlich von Jay Schullist hinzugefügt. Der BPF-Code blieb bis 2011 unverändert, als Eric Dumaset den BPF-Interpreter so umgestaltete, dass er im JIT-Modus funktioniert (Quelle: JIT für Paketfilter). Anstatt den BPF-Bytecode zu interpretieren, könnte der Kernel danach BPF-Programme direkt in die Zielarchitektur konvertieren: x86, ARM, MIPS usw.

Später, im Jahr 2014, schlug Alexei Starovoitov einen neuen JIT-Mechanismus für BPF vor. Tatsächlich wurde dieses neue JIT zu einer neuen Architektur, die auf BPF basiert und eBPF genannt wurde. Ich denke, dass beide VMs eine Zeit lang nebeneinander existierten, aber die Paketfilterung ist derzeit zusätzlich zu eBPF implementiert. Tatsächlich wird BPF in vielen modernen Dokumentationsbeispielen als eBPF bezeichnet, und klassisches BPF ist heute als cBPF bekannt.

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

  • Setzt auf moderne 64-Bit-Architekturen. eBPF verwendet 64-Bit-Register und erhöht die Anzahl der verfügbaren Register von 2 (Akkumulator und X) auf 10. eBPF bietet auch zusätzliche Opcodes (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Vom Netzwerkschicht-Subsystem getrennt. BPF war an das Batch-Datenmodell gebunden. Da es zum Filtern von Paketen verwendet wurde, befand sich sein Code im Subsystem, das Netzwerkinteraktionen bereitstellte. Die virtuelle eBPF-Maschine ist jedoch nicht mehr an ein Datenmodell gebunden und kann für jeden Zweck verwendet werden. Nun kann das eBPF-Programm mit Tracepoint oder kprobe verbunden werden. Dies öffnet die Tür zur eBPF-Instrumentierung, Leistungsanalyse und vielen anderen Anwendungsfällen im Kontext anderer Kernel-Subsysteme. Jetzt befindet sich der eBPF-Code in einem eigenen Pfad: kernel/bpf.
  • Globale Datenspeicher namens Maps. Karten sind Schlüsselwertspeicher, die den Datenaustausch zwischen Benutzerraum und Kernelraum ermöglichen. eBPF bietet verschiedene Arten von Karten an.
  • Sekundärfunktionen. Insbesondere um ein Paket zu überschreiben, eine Prüfsumme zu berechnen oder ein Paket zu klonen. Diese Funktionen werden im Kernel ausgeführt und gehören nicht zu User-Space-Programmen. Darüber hinaus können Systemaufrufe aus eBPF-Programmen erfolgen.
  • Anrufe beenden. Die Programmgröße in eBPF ist auf 4096 Byte begrenzt. Mit der End-Call-Funktion kann ein eBPF-Programm die Kontrolle an ein neues eBPF-Programm übertragen und so diese Einschränkung umgehen (bis zu 32 Programme können auf diese Weise verkettet werden).

eBPF-Beispiel

Es gibt mehrere Beispiele für eBPF in den Linux-Kernel-Quellen. Sie sind unter Samples/bpf/ verfügbar. Um diese Beispiele zusammenzustellen, geben Sie einfach Folgendes ein:

$ sudo make samples/bpf/

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

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

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

In diesem Fall müssen wir kompilieren tracex4_kern.c zum eBPF-Bytecode. Im Moment in gcc Es gibt keinen Serverteil für eBPF. Zum Glück, clang kann eBPF-Bytecode erzeugen. Makefile verwendet clang kompilieren tracex4_kern.c in die Objektdatei.

Ich habe oben erwähnt, dass Karten eine der interessantesten Funktionen von eBPF sind. Tracex4_kern definiert eine Karte:

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 ist einer der vielen Kartentypen, die eBPF anbietet. In diesem Fall handelt es sich lediglich um einen Hash. Möglicherweise ist Ihnen die Anzeige auch schon aufgefallen SEC("maps"). SEC ist ein Makro, mit dem ein neuer Abschnitt einer Binärdatei erstellt wird. Eigentlich im Beispiel tracex4_kern Es werden zwei weitere Abschnitte definiert:

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 aus der Karte entfernen (kprobe/kmem_cache_free) und fügen Sie der Karte einen neuen Eintrag hinzu (kretprobe/kmem_cache_alloc_node). Alle in Großbuchstaben geschriebenen Funktionsnamen entsprechen Makros, die in definiert sind bpf_helpers.h.

Wenn ich die Abschnitte der Objektdatei speichere, sollte ich sehen, dass diese neuen Abschnitte bereits definiert sind:

$ 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

Gibt es noch tracex4_user.c, Hauptprogramm. Grundsätzlich lauscht dieses Programm auf Ereignisse kmem_cache_alloc_node. Wenn ein solches Ereignis eintritt, wird der entsprechende eBPF-Code ausgeführt. Der Code speichert das IP-Attribut des Objekts in einer Karte, und dann durchläuft das Objekt eine Schleife durch das Hauptprogramm. Beispiel:

$ 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

Wie hängen das User-Space-Programm und das eBPF-Programm zusammen? Bei der Initialisierung tracex4_user.c lädt Objektdatei tracex4_kern.o mit der Funktion 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;
}

Beim Aufführen load_bpf_file In der eBPF-Datei definierte Sonden werden hinzugefügt /sys/kernel/debug/tracing/kprobe_events. Jetzt achten wir auf diese Ereignisse und unser Programm kann etwas unternehmen, wenn sie eintreten.

$ 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

Alle anderen Programme in sample/bpf/ sind ähnlich aufgebaut. Sie enthalten immer zwei Dateien:

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

Das eBPF-Programm definiert die Karten und Funktionen, die einem Abschnitt zugeordnet sind. Wenn der Kernel ein Ereignis eines bestimmten Typs ausgibt (z. B. tracepoint), werden die gebundenen Funktionen ausgeführt. Karten ermöglichen die Kommunikation zwischen einem Kernel-Programm und einem User-Space-Programm.

Abschluss

In diesem Artikel wurden BPF und eBPF allgemein besprochen. Ich weiß, dass es heute viele Informationen und Ressourcen zu eBPF gibt, daher werde ich einige weitere Materialien zum weiteren Studium empfehlen.

Ich empfehle zu lesen:

Source: habr.com

Kommentar hinzufügen