Een korte introductie tot BPF en eBPF

Hallo, Habr! Wij willen u graag laten weten dat wij bezig zijn met het voorbereiden van een boek voor uitgave."Linux-waarneembaarheid met BPF".

Een korte introductie tot BPF en eBPF
Omdat de virtuele BPF-machine blijft evolueren en in de praktijk actief wordt gebruikt, hebben we voor u een artikel vertaald waarin de belangrijkste mogelijkheden en de huidige staat ervan worden beschreven.

De afgelopen jaren zijn programmeerhulpmiddelen en -technieken steeds populairder geworden om de beperkingen van de Linux-kernel te compenseren in gevallen waarin hoogwaardige pakketverwerking vereist is. Een van de meest populaire technieken van dit soort wordt genoemd kernel-bypass (kernel bypass) en maakt het mogelijk, waarbij de kernelnetwerklaag wordt omzeild, alle pakketverwerking vanuit de gebruikersruimte uit te voeren. Het omzeilen van de kernel houdt ook in dat je de netwerkkaart bestuurt gebruikersruimte. Met andere woorden: bij het werken met een netwerkkaart vertrouwen we op de driver gebruikersruimte.

Door de volledige controle over de netwerkkaart over te dragen aan een gebruikersruimteprogramma, verminderen we de kernel-overhead (contextwisseling, netwerklaagverwerking, interrupts, enz.), wat behoorlijk belangrijk is bij het draaien met snelheden van 10 Gb/s of hoger. Kernel bypass plus een combinatie van andere functies (batchverwerking) en zorgvuldige prestatieafstemming (NUMA-boekhouding, CPU-isolatie, etc.) komen overeen met de fundamenten van hoogwaardige netwerkverwerking in de gebruikersruimte. Misschien is dit een voorbeeldig voorbeeld van deze nieuwe benadering van pakketverwerking DPDK van Intel (Data Plane-ontwikkelingskit), hoewel er nog andere bekende tools en technieken zijn, waaronder Cisco's VPP (Vector Packet Processing), Netmap en natuurlijk snauwen.

Het organiseren van netwerkinteracties in de gebruikersruimte heeft een aantal nadelen:

  • De OS-kernel is een abstractielaag voor hardwarebronnen. Omdat gebruikersruimteprogramma's hun bronnen rechtstreeks moeten beheren, moeten ze ook hun eigen hardware beheren. Vaak betekent dit dat u uw eigen stuurprogramma's moet programmeren.
  • Omdat we de kernelruimte volledig opgeven, geven we ook alle netwerkfunctionaliteit op die door de kernel wordt geboden. Gebruikersruimteprogramma's moeten functies opnieuw implementeren die mogelijk al door de kernel of het besturingssysteem worden geleverd.
  • Programma's werken in de sandbox-modus, wat hun interactie ernstig beperkt en verhindert dat ze met andere delen van het besturingssysteem integreren.

Bij het netwerken in de gebruikersruimte worden prestatiewinsten bereikt door de pakketverwerking van de kernel naar de gebruikersruimte te verplaatsen. XDP doet precies het tegenovergestelde: het verplaatst netwerkprogramma's van de gebruikersruimte (filters, solvers, routing, enz.) naar het kernelgebied. Met XDP kunnen we een netwerkfunctie uitvoeren zodra een pakket een netwerkinterface raakt en voordat het omhoog gaat naar het kernelnetwerksubsysteem. Als gevolg hiervan neemt de pakketverwerkingssnelheid aanzienlijk toe. Hoe kan de kernel de gebruiker echter in staat stellen zijn programma's in de kernelruimte uit te voeren? Laten we, voordat we deze vraag beantwoorden, eens kijken naar wat BPF is.

BPF en eBPF

Ondanks de verwarrende naam is BPF (Berkeley Packet Filtering) in feite een model van een virtuele machine. Deze virtuele machine is oorspronkelijk ontworpen om pakketfiltering af te handelen, vandaar de naam.

Een van de bekendste tools die BPF gebruiken is tcpdump. Bij het vastleggen van pakketten met behulp van tcpdump de gebruiker kan een expressie opgeven om pakketten te filteren. Alleen pakketten die aan deze uitdrukking voldoen, worden vastgelegd. De uitdrukking “tcp dst port 80” verwijst naar alle TCP-pakketten die op poort 80 aankomen. De compiler kan deze uitdrukking inkorten door deze naar BPF-bytecode te converteren.

$ 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

Dit is wat het bovenstaande programma in principe doet:

  • Instructie (000): Laadt het pakket op offset 12, als een 16-bits woord, in de accumulator. Offset 12 komt overeen met het ethertype van het pakket.
  • Instructie (001): vergelijkt de waarde in de accumulator met 0x86dd, dat wil zeggen met de ethertypewaarde voor IPv6. Als het resultaat waar is, gaat de programmateller naar instructie (002), en zo niet, dan naar (006).
  • Instructie (006): vergelijkt de waarde met 0x800 (ethertype-waarde voor IPv4). Als het antwoord waar is, gaat het programma naar (007), zo niet, dan naar (015).

En zo verder totdat het pakketfilterprogramma een resultaat retourneert. Dit is meestal een Booleaanse waarde. Het retourneren van een waarde die niet nul is (instructie (014)) betekent dat het pakket is geaccepteerd, en het retourneren van een nulwaarde (instructie (015)) betekent dat het pakket niet is geaccepteerd.

De virtuele BPF-machine en zijn bytecode werden eind 1992 voorgesteld door Steve McCann en Van Jacobson toen hun artikel werd gepubliceerd BSD-pakketfilter: nieuwe architectuur voor pakketopname op gebruikersniveauwerd deze technologie voor het eerst gepresenteerd op de Usenix-conferentie in de winter van 1993.

Omdat BPF een virtuele machine is, definieert het de omgeving waarin programma's worden uitgevoerd. Naast de bytecode definieert het ook het batchgeheugenmodel (laadinstructies worden impliciet toegepast op de batch), registers (A en X; accumulator- en indexregisters), scratch-geheugenopslag en een impliciete programmateller. Interessant genoeg is de BPF-bytecode gemodelleerd naar de Motorola 6502 ISA. Zoals Steve McCann zich herinnerde in zijn plenair verslag op Sharkfest '11 was hij bekend met build 6502 vanaf zijn middelbare schooltijd met programmeren op de Apple II, en deze kennis beïnvloedde zijn werk bij het ontwerpen van de BPF-bytecode.

BPF-ondersteuning is geïmplementeerd in de Linux-kernel in versies v2.5 en hoger, voornamelijk toegevoegd door de inspanningen van Jay Schullist. De BPF-code bleef ongewijzigd tot 2011, toen Eric Dumaset de BPF-tolk opnieuw ontwierp om in JIT-modus te werken (bron: JIT voor pakketfilters). Hierna zou de kernel, in plaats van de BPF-bytecode te interpreteren, BPF-programma's rechtstreeks naar de doelarchitectuur kunnen converteren: x86, ARM, MIPS, enz.

Later, in 2014, stelde Alexey Starovoitov een nieuw JIT-mechanisme voor BPF voor. In feite werd dit nieuwe JIT een nieuwe op BPF gebaseerde architectuur en heette het eBPF. Ik denk dat beide VM's enige tijd naast elkaar hebben bestaan, maar momenteel wordt pakketfiltering geïmplementeerd op basis van eBPF. In veel voorbeelden van moderne documentatie wordt BPF zelfs gezien als eBPF, terwijl klassieke BPF tegenwoordig bekend staat als cBPF.

eBPF breidt de klassieke BPF virtuele machine op verschillende manieren uit:

  • Gebaseerd op moderne 64-bit architecturen. eBPF gebruikt 64-bit registers en verhoogt het aantal beschikbare registers van 2 (accumulator en X) naar 10. eBPF biedt ook aanvullende opcodes (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Losgemaakt van het netwerklaagsubsysteem. BPF was gekoppeld aan het batchgegevensmodel. Omdat het werd gebruikt voor pakketfiltering, bevond de code zich in het subsysteem dat netwerkcommunicatie verzorgt. De virtuele eBPF-machine is echter niet langer gebonden aan het datamodel en kan voor elk doel worden gebruikt. Dus nu kan het eBPF-programma worden verbonden met Tracepoint of kprobe. Dit opent de weg naar eBPF-instrumentatie, prestatieanalyse en vele andere gebruiksscenario's in de context van andere kernelsubsystemen. Nu bevindt de eBPF-code zich in zijn eigen pad: kernel/bpf.
  • Wereldwijde gegevensopslag genaamd Maps. Kaarten zijn sleutelwaardeopslagplaatsen die gegevensuitwisseling tussen gebruikersruimte en kernelruimte mogelijk maken. eBPF biedt verschillende soorten kaarten.
  • Secundaire functies. In het bijzonder om een ​​pakket te herschrijven, een controlesom te berekenen of een pakket te klonen. Deze functies draaien binnen de kernel en zijn geen gebruikersruimteprogramma's. U kunt ook systeemoproepen doen vanuit eBPF-programma's.
  • Gesprekken beëindigen. De programmagrootte in eBPF is beperkt tot 4096 bytes. Dankzij de tail call-functie kan een eBPF-programma de besturing overdragen aan een nieuw eBPF-programma en zo deze beperking omzeilen (op deze manier kunnen maximaal 32 programma's worden gekoppeld).

eBPF: voorbeeld

Er zijn verschillende voorbeelden voor eBPF in de Linux-kernelbronnen. Ze zijn verkrijgbaar op samples/bpf/. Om deze voorbeelden samen te stellen, voert u eenvoudigweg het volgende in:

$ sudo make samples/bpf/

Ik ga zelf geen nieuw voorbeeld voor eBPF schrijven, maar gebruik een van de samples die beschikbaar zijn in samples/bpf/. Ik zal enkele delen van de code bekijken en uitleggen hoe het werkt. Als voorbeeld heb ik het programma gekozen tracex4.

Over het algemeen bestaat elk voorbeeld in samples/bpf/ uit twee bestanden. In dit geval:

  • tracex4_kern.c, bevat de broncode die in de kernel moet worden uitgevoerd als eBPF-bytecode.
  • tracex4_user.c, bevat een programma uit de gebruikersruimte.

In dit geval moeten we compileren tracex4_kern.c naar eBPF-bytecode. Op dit moment in gcc er is geen backend voor eBPF. Gelukkig, clang kan eBPF-bytecode uitvoeren. Makefile toepassingen clang voor compilatie tracex4_kern.c naar het objectbestand.

Ik heb hierboven vermeld dat kaarten een van de meest interessante kenmerken van eBPF zijn. tracex4_kern definieert één kaart:

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 is een van de vele soorten kaarten die eBPF aanbiedt. In dit geval is het gewoon een hash. Mogelijk heeft u ook een advertentie opgemerkt SEC("maps"). SEC is een macro die wordt gebruikt om een ​​nieuwe sectie van een binair bestand te maken. Eigenlijk in het voorbeeld tracex4_kern er zijn nog twee secties gedefinieerd:

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

Met deze twee functies kunt u een item van de kaart verwijderen (kprobe/kmem_cache_free) en voeg een nieuw item toe aan de kaart (kretprobe/kmem_cache_alloc_node). Alle functienamen die in hoofdletters zijn geschreven, komen overeen met macro's die zijn gedefinieerd in bpf_helpers.h.

Als ik de secties van het objectbestand dump, zou ik moeten zien dat deze nieuwe secties al zijn gedefinieerd:

$ 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

Er is ook tracex4_user.c, hoofdprogramma. Kortom, dit programma luistert naar gebeurtenissen kmem_cache_alloc_node. Wanneer een dergelijke gebeurtenis zich voordoet, wordt de overeenkomstige eBPF-code uitgevoerd. De code slaat het IP-attribuut van het object op in een kaart, en het object wordt vervolgens door het hoofdprogramma geleid. Voorbeeld:

$ 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

Hoe zijn een gebruikersruimteprogramma en een eBPF-programma met elkaar verbonden? Bij initialisatie tracex4_user.c laadt een objectbestand tracex4_kern.o de functie gebruiken: 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;
}

Door te doen load_bpf_file sondes die in het eBPF-bestand zijn gedefinieerd, worden toegevoegd /sys/kernel/debug/tracing/kprobe_events. Nu luisteren we naar deze gebeurtenissen en ons programma kan iets doen als ze plaatsvinden.

$ 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 andere programma's in sample/bpf/ zijn op dezelfde manier gestructureerd. Ze bevatten altijd twee bestanden:

  • XXX_kern.c: eBPF-programma.
  • XXX_user.c: hoofdprogramma.

Het eBPF-programma identificeert kaarten en functies die bij een sectie horen. Wanneer de kernel een gebeurtenis van een bepaald type afgeeft (bijvoorbeeld tracepoint), worden de gebonden functies uitgevoerd. De kaarten zorgen voor communicatie tussen het kernelprogramma en het gebruikersruimteprogramma.

Conclusie

In dit artikel worden BPF en eBPF in algemene termen besproken. Ik weet dat er tegenwoordig veel informatie en bronnen over eBPF bestaat, dus ik zal nog een paar bronnen aanbevelen voor verder onderzoek

Ik raad aan om te lezen:

Bron: www.habr.com

Voeg een reactie