En kort introduksjon til BPF og eBPF

Hei, Habr! Vi vil gjerne informere deg om at vi forbereder en bok for utgivelse."Linux observerbarhet med BPF".

En kort introduksjon til BPF og eBPF
Siden den virtuelle BPF-maskinen fortsetter å utvikle seg og brukes aktivt i praksis, har vi oversatt en artikkel for deg som beskriver dens hovedfunksjoner og nåværende tilstand.

De siste årene har programmeringsverktøy og -teknikker blitt stadig mer populære for å kompensere for begrensningene til Linux-kjernen i tilfeller der det kreves pakkebehandling med høy ytelse. En av de mest populære teknikkene av denne typen kalles kjernebypass (kernel bypass) og lar, omgå kjernenettverkslaget, utføre all pakkebehandling fra brukerplass. Å omgå kjernen innebærer også å kontrollere nettverkskortet fra brukerplass. Med andre ord, når vi jobber med et nettverkskort, stoler vi på driveren brukerplass.

Ved å overføre full kontroll over nettverkskortet til et brukerromsprogram reduserer vi kjerneoverhead (kontekstbytting, nettverkslagsbehandling, avbrudd osv.), noe som er ganske viktig når du kjører med hastigheter på 10Gb/s eller høyere. Kernel bypass pluss en kombinasjon av andre funksjoner (batchbehandling) og nøye ytelsesjustering (NUMA regnskap, CPU-isolasjon, etc.) tilsvarer det grunnleggende om høyytelses nettverksbehandling i brukerrommet. Kanskje et eksemplarisk eksempel på denne nye tilnærmingen til pakkebehandling er DPDK fra Intel (Dataplanutviklingssett), selv om det er andre velkjente verktøy og teknikker, inkludert Ciscos VPP (Vector Packet Processing), Netmap og, selvfølgelig, Snabb.

Organisering av nettverksinteraksjoner i brukerområdet har en rekke ulemper:

  • OS-kjernen er et abstraksjonslag for maskinvareressurser. Fordi brukerromsprogrammer må administrere ressursene sine direkte, må de også administrere sin egen maskinvare. Dette betyr ofte at du må programmere dine egne drivere.
  • Fordi vi gir opp kjerneplass fullstendig, gir vi også opp all nettverksfunksjonaliteten som kjernen tilbyr. Brukerromsprogrammer må implementere funksjoner som allerede kan leveres av kjernen eller operativsystemet på nytt.
  • Programmer fungerer i sandkassemodus, noe som alvorlig begrenser deres interaksjon og hindrer dem i å integreres med andre deler av operativsystemet.

I hovedsak, ved nettverksbygging i brukerrom, oppnås ytelsesgevinster ved å flytte pakkebehandling fra kjernen til brukerområdet. XDP gjør akkurat det motsatte: det flytter nettverksprogrammer fra brukerrom (filtre, resolvere, ruting, etc.) til kjerneplass. XDP lar oss utføre en nettverksfunksjon så snart en pakke treffer et nettverksgrensesnitt og før den begynner å bevege seg opp i kjernenettverkets delsystem. Som et resultat øker pakkebehandlingshastigheten betydelig. Men hvordan lar kjernen brukeren kjøre programmene sine i kjerneplass? Før vi svarer på dette spørsmålet, la oss se på hva BPF er.

BPF og eBPF

Til tross for det forvirrende navnet, er BPF (Berkeley Packet Filtering) faktisk en virtuell maskinmodell. Denne virtuelle maskinen ble opprinnelig designet for å håndtere pakkefiltrering, derav navnet.

Et av de mest kjente verktøyene som bruker BPF er tcpdump. Når du fanger pakker ved hjelp av tcpdump brukeren kan spesifisere et uttrykk for å filtrere pakker. Bare pakker som samsvarer med dette uttrykket vil bli fanget. For eksempel uttrykket "tcp dst port 80” refererer til alle TCP-pakker som ankommer port 80. Kompilatoren kan forkorte dette uttrykket ved å konvertere det til BPF-bytekode.

$ 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

Dette er hva programmet ovenfor i utgangspunktet gjør:

  • Instruksjon (000): Laster pakken ved offset 12, som et 16-bits ord, inn i akkumulatoren. Offset 12 tilsvarer etertypen til pakken.
  • Instruksjon (001): sammenligner verdien i akkumulatoren med 0x86dd, det vil si med ethertype-verdien for IPv6. Hvis resultatet er sant, går programtelleren til instruksjon (002), og hvis ikke, så til (006).
  • Instruksjon (006): sammenligner verdien med 0x800 (etertypeverdi for IPv4). Hvis svaret er sant, går programmet til (007), hvis ikke, så til (015).

Og så videre til pakkefiltreringsprogrammet returnerer et resultat. Dette er vanligvis en boolsk. Å returnere en verdi som ikke er null (instruksjon (014)) betyr at pakken ble akseptert, og returnering av null (instruksjon (015)) betyr at pakken ikke ble akseptert.

Den virtuelle BPF-maskinen og dens bytekode ble foreslått av Steve McCann og Van Jacobson på slutten av 1992 da papiret deres ble publisert BSD-pakkefilter: Ny arkitektur for pakkefangst på brukernivå, ble denne teknologien først presentert på Usenix-konferansen vinteren 1993.

Fordi BPF er en virtuell maskin, definerer den miljøet som programmene kjører i. I tillegg til bytekoden, definerer den også batchminnemodellen (lastinstruksjoner blir implisitt brukt på batchen), registre (A og X; akkumulator- og indeksregistre), lagring av skrapelime og en implisitt programteller. Interessant nok ble BPF-bytekoden modellert etter Motorola 6502 ISA. Som Steve McCann husket i sin plenumsrapport på Sharkfest '11 var han kjent med build 6502 fra hans videregående programmering på Apple II, og denne kunnskapen påvirket arbeidet hans med å designe BPF-bytekoden.

BPF-støtte er implementert i Linux-kjernen i versjoner v2.5 og høyere, hovedsakelig lagt til av innsatsen til Jay Schullist. BPF-koden forble uendret til 2011, da Eric Dumaset redesignet BPF-tolken for å kjøre i JIT-modus (Kilde: JIT for pakkefiltre). Etter dette kunne kjernen, i stedet for å tolke BPF-bytekode, direkte konvertere BPF-programmer til målarkitekturen: x86, ARM, MIPS, etc.

Senere, i 2014, foreslo Alexey Starovoitov en ny JIT-mekanisme for BPF. Faktisk ble denne nye JIT en ny BPF-basert arkitektur og ble kalt eBPF. Jeg tror begge VM-ene eksisterte sammen i noen tid, men for øyeblikket er pakkefiltrering implementert basert på eBPF. Faktisk, i mange eksempler på moderne dokumentasjon, er BPF forstått å være eBPF, og klassisk BPF er i dag kjent som cBPF.

eBPF utvider den klassiske BPF virtuelle maskinen på flere måter:

  • Basert på moderne 64-bits arkitekturer. eBPF bruker 64-bits registre og øker antallet tilgjengelige registre fra 2 (akkumulator og X) til 10. eBPF gir også ekstra opkoder (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Koblet fra nettverkslagets delsystem. BPF var knyttet til batchdatamodellen. Siden den ble brukt til pakkefiltrering, var koden plassert i undersystemet som gir nettverkskommunikasjon. Den virtuelle eBPF-maskinen er imidlertid ikke lenger knyttet til datamodellen og kan brukes til ethvert formål. Så nå kan eBPF-programmet kobles til tracepoint eller kprobe. Dette åpner veien for eBPF-instrumentering, ytelsesanalyse og mange andre brukstilfeller i sammenheng med andre kjerneundersystemer. Nå er eBPF-koden plassert i sin egen bane: kernel/bpf.
  • Globale datalagre kalt Maps. Kart er nøkkelverdilagre som muliggjør datautveksling mellom brukerrom og kjerneplass. eBPF tilbyr flere typer kart.
  • Sekundære funksjoner. Spesielt for å omskrive en pakke, beregne en kontrollsum eller klone en pakke. Disse funksjonene kjører inne i kjernen og er ikke brukerromsprogrammer. Du kan også foreta systemanrop fra eBPF-programmer.
  • Avslutt samtaler. Programstørrelsen i eBPF er begrenset til 4096 byte. Haleanropsfunksjonen lar et eBPF-program overføre kontrollen til et nytt eBPF-program og dermed omgå denne begrensningen (opptil 32 programmer kan kobles sammen på denne måten).

eBPF: eksempel

Det er flere eksempler for eBPF i Linux-kjernekildene. De er tilgjengelige på samples/bpf/. For å kompilere disse eksemplene, skriv inn:

$ sudo make samples/bpf/

Jeg skal ikke skrive et nytt eksempel for eBPF selv, men vil bruke en av prøvene tilgjengelig i samples/bpf/. Jeg skal se på noen deler av koden og forklare hvordan den fungerer. Som et eksempel valgte jeg programmet tracex4.

Generelt består hvert av eksemplene i samples/bpf/ av to filer. I dette tilfellet:

  • tracex4_kern.c, inneholder kildekoden som skal kjøres i kjernen som eBPF-bytekode.
  • tracex4_user.c, inneholder et program fra brukerområdet.

I dette tilfellet må vi kompilere tracex4_kern.c til eBPF-bytekode. For tiden i gcc det er ingen backend for eBPF. Heldigvis, clang kan sende ut eBPF-bytekode. Makefile bruker clang for kompilering tracex4_kern.c til objektfilen.

Jeg nevnte ovenfor at en av de mest interessante funksjonene til eBPF er kart. tracex4_kern definerer ett kart:

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 er en av mange typer kort som tilbys av eBPF. I dette tilfellet er det bare en hasj. Du har kanskje også lagt merke til en annonse SEC("maps"). SEC er en makro som brukes til å lage en ny del av en binær fil. Faktisk i eksemplet tracex4_kern ytterligere to seksjoner er definert:

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

Disse to funksjonene lar deg slette en oppføring fra kartet (kprobe/kmem_cache_free) og legg til en ny oppføring på kartet (kretprobe/kmem_cache_alloc_node). Alle funksjonsnavn skrevet med store bokstaver tilsvarer makroer definert i bpf_helpers.h.

Hvis jeg dumper delene av objektfilen, bør jeg se at disse nye delene allerede er definert:

$ 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

Det er også tracex4_user.c, hovedprogram. I utgangspunktet lytter dette programmet til hendelser kmem_cache_alloc_node. Når en slik hendelse inntreffer, utføres den tilsvarende eBPF-koden. Koden lagrer IP-attributtet til objektet i et kart, og objektet blir deretter sløyfet gjennom hovedprogrammet. Eksempel:

$ 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

Hvordan er et brukerromsprogram og et eBPF-program relatert? Ved initialisering tracex4_user.c laster en objektfil tracex4_kern.o ved å bruke funksjonen 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;
}

Mens du gjør load_bpf_file sonder definert i eBPF-filen legges til /sys/kernel/debug/tracing/kprobe_events. Nå lytter vi etter disse arrangementene og programmet vårt kan gjøre noe når de skjer.

$ 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 andre programmer i sample/bpf/ er strukturert på samme måte. De inneholder alltid to filer:

  • XXX_kern.c: eBPF-program.
  • XXX_user.c: hovedprogram.

eBPF-programmet identifiserer kart og funksjoner knyttet til en seksjon. Når kjernen utsteder en hendelse av en bestemt type (f.eks. tracepoint), blir de bundne funksjonene utført. Kortene gir kommunikasjon mellom kjerneprogrammet og brukerromsprogrammet.

Konklusjon

Denne artikkelen diskuterte BPF og eBPF i generelle termer. Jeg vet at det er mye informasjon og ressurser om eBPF i dag, så jeg vil anbefale noen flere ressurser for videre studier

Jeg anbefaler å lese:

Kilde: www.habr.com

Legg til en kommentar