En kort introduktion til BPF og eBPF

Hej Habr! Vi informerer dig om, at vi forbereder at udgive en bog "Linux observerbarhed med BPF".

En kort introduktion til BPF og eBPF
Da den virtuelle BPF-maskine fortsætter med at udvikle sig og bruges aktivt i praksis, har vi oversat en artikel til dig, der beskriver dens hovedfunktioner og nuværende tilstand.

I de senere år har programmeringsværktøjer og -teknikker vundet popularitet for at kompensere for Linux-kernens begrænsninger i tilfælde, hvor højtydende pakkebehandling er påkrævet. En af de mest populære metoder af denne art kaldes kerne bypass (kernebypass) og tillader, at springe netværkslaget af kernen over, at udføre al pakkebehandling fra brugerens plads. Omgåelse af kernen involverer også styring af netværkskortet fra brugerplads. Med andre ord, når vi arbejder med et netværkskort, er vi afhængige af driveren brugerplads.

Ved at overføre fuld kontrol over netværkskortet til et brugerrumsprogram reducerer vi kernens overhead (kontekstskift, netværkslagsbehandling, afbrydelser osv.), hvilket er ret vigtigt, når man kører med hastigheder på 10 Gb/s eller højere. Omgå kernen plus en kombination af andre funktioner (batchbehandling) og omhyggelig justering af ydeevne (NUMA regnskab, CPU isoleringosv.) passer til det grundlæggende i højtydende bruger-rum netværk. Måske et eksemplarisk eksempel på denne nye tilgang til pakkebehandling er DPDK fra Intel (Data Plane Development Kit), selvom der er andre velkendte værktøjer og teknikker, herunder VPP fra Cisco (Vector Packet Processing), Netmap og, selvfølgelig, snuppe.

Organiseringen af ​​netværksinteraktioner i brugerrummet har en række ulemper:

  • En OS-kerne er et abstraktionslag for hardwareressourcer. Fordi user-space-programmer skal administrere deres ressourcer direkte, skal de også administrere deres egen hardware. Dette betyder ofte at programmere dine egne drivere.
  • Da vi fuldstændig opgiver kerneplads, opgiver vi også al netværksfunktionaliteten fra kernen. User-space-programmer skal genimplementere funktioner, som muligvis allerede er leveret af kernen eller operativsystemet.
  • Programmer fungerer i en sandkassetilstand, hvilket i høj grad begrænser deres interaktion og forhindrer dem i at integrere med andre dele af operativsystemet.

I det væsentlige, når der netværks i brugerrum, opnås ydeevnegevinster ved at flytte pakkebehandling fra kernen til brugerrummet. XDP gør præcis det modsatte: det flytter netværksprogrammer fra brugerrum (filtre, konvertere, routing osv.) til kerneområdet. XDP giver os mulighed for at udføre netværksfunktionen, så snart pakken rammer netværksgrænsefladen, og før den begynder at rejse op til kernens netværksundersystem. Som et resultat øges pakkebehandlingshastigheden betydeligt. Men hvordan tillader kernen brugeren at køre deres programmer i kernerummet? Før vi besvarer dette spørgsmål, lad os se på, hvad BPF er.

BPF og eBPF

På trods af det ikke helt klare navn er BPF (Packet Filtering, Berkeley) i virkeligheden en virtuel maskinmodel. Denne virtuelle maskine blev oprindeligt designet til at håndtere pakkefiltrering, deraf navnet.

Et af de mere kendte værktøjer, der bruger BPF er tcpdump. Når du fanger pakker med tcpdump brugeren kan angive et udtryk for pakkefiltrering. Kun pakker, der matcher dette udtryk, vil blive fanget. For eksempel udtrykket "tcp dst port 80” refererer til alle TCP-pakker, der ankommer til port 80. Compileren kan forkorte dette udtryk ved at 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 grundlæggende, hvad ovenstående program gør:

  • Instruktion (000): Indlæser pakken ved offset 12, som et 16-bit ord, i akkumulatoren. Offset 12 svarer til pakkens ethertype.
  • Instruktion (001): sammenligner værdien i akkumulatoren med 0x86dd, det vil sige med ethertype-værdien for IPv6. Hvis resultatet er sandt, går programtælleren til instruktion (002), og hvis ikke, så til (006).
  • Instruktion (006): sammenligner værdien med 0x800 (ethertypeværdi for IPv4). Hvis svaret er sandt, går programmet til (007), hvis ikke, så til (015).

Og så videre, indtil pakkefiltreringsprogrammet returnerer et resultat. Normalt er det boolesk. At returnere en ikke-nul værdi (instruktion (014)) betyder, at pakken matchede, og returnering af nul (instruktion (015)) betyder, at pakken ikke matchede.

Den virtuelle BPF-maskine og dens bytekode blev foreslået af Steve McCann og Van Jacobson i slutningen af ​​1992, da deres papir udkom. BSD-pakkefilter: Ny arkitektur til pakkefangst på brugerniveau, for første gang blev denne teknologi præsenteret på Usenix-konferencen i vinteren 1993.

Fordi BPF er en virtuel maskine, definerer den det miljø, som programmer kører i. Ud over bytekode definerer den også en pakkehukommelsesmodel (indlæsningsinstruktioner anvendes implicit på en pakke), registre (A og X; akkumulator- og indeksregistre), lagring af scratch-hukommelse og en implicit programtæller. Interessant nok blev BPF-bytekoden modelleret efter Motorola 6502 ISA. Som Steve McCann huskede i sin plenumsberetning ved Sharkfest '11 var han bekendt med build 6502 fra gymnasiet, da han programmerede på Apple II, og denne viden påvirkede hans arbejde med at designe BPF-bytekoden.

BPF-understøttelse er implementeret i Linux-kernen i version v2.5 og senere, primært tilføjet af Jay Schullist. BPF-koden forblev uændret indtil 2011, hvor Eric Dumaset redesignede BPF-fortolkeren til at fungere i JIT-tilstand (Kilde: JIT til pakkefiltre). Derefter, i stedet for at fortolke BPF-bytekoden, kunne kernen direkte konvertere BPF-programmer til målarkitekturen: x86, ARM, MIPS osv.

Senere, i 2014, foreslog Alexei Starovoitov en ny JIT-mekanisme for BPF. Faktisk blev denne nye JIT en ny arkitektur baseret på BPF og blev kaldt eBPF. Jeg tror, ​​at begge VM'er har eksisteret side om side i nogen tid, men pakkefiltrering er i øjeblikket implementeret oven på eBPF. Faktisk er BPF i mange moderne dokumentationseksempler omtalt som eBPF, og klassisk BPF er i dag kendt som cBPF.

eBPF udvider den klassiske BPF virtuelle maskine på flere måder:

  • Stoler på moderne 64-bit arkitekturer. eBPF bruger 64-bit registre og øger antallet af tilgængelige registre fra 2 (akkumulator og X) til 10. eBPF giver også yderligere opkoder (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Adskilt fra netværkslagets undersystem. BPF var bundet til batchdatamodellen. Da det blev brugt til at filtrere pakker, var dets kode i det undersystem, der leverede netværksinteraktioner. Den virtuelle eBPF-maskine er dog ikke længere bundet til en datamodel og kan bruges til ethvert formål. Så nu kan eBPF-programmet forbindes til tracepoint eller til kprobe. Dette åbner døren til eBPF-instrumentering, præstationsanalyse og mange andre use cases i forbindelse med andre kerneundersystemer. Nu er eBPF-koden placeret i sin egen sti: kernel/bpf.
  • Globale datalagre kaldet Maps. Kort er nøgleværdi-lagre, der giver dataudveksling mellem brugerrum og kernerum. eBPF tilbyder flere typer kort.
  • Sekundære funktioner. Især for at overskrive en pakke, beregne en kontrolsum eller klone en pakke. Disse funktioner kører inde i kernen og hører ikke til brugerrumsprogrammer. Derudover kan systemkald foretages fra eBPF-programmer.
  • Afslut opkald. Programstørrelse i eBPF er begrænset til 4096 bytes. End call-funktionen gør det muligt for et eBPF-program at overføre kontrol til et nyt eBPF-program og dermed omgå denne begrænsning (op til 32 programmer kan kædes sammen på denne måde).

eBPF eksempel

Der er flere eksempler på eBPF i Linux-kernekilderne. De er tilgængelige på samples/bpf/. For at kompilere disse eksempler skal du blot skrive:

$ sudo make samples/bpf/

Jeg vil ikke selv skrive et nyt eksempel til eBPF, men vil bruge en af ​​de eksempler, der er tilgængelige i samples/bpf/. Jeg vil se på nogle dele af koden og forklare, hvordan den virker. Som eksempel valgte jeg programmet tracex4.

Generelt består hvert af eksemplerne i samples/bpf/ af to filer. I dette tilfælde:

  • tracex4_kern.c, indeholder kildekode, der skal udføres i kernen som eBPF-bytekode.
  • tracex4_user.c, indeholder et program fra brugerrummet.

I dette tilfælde skal vi kompilere tracex4_kern.c til eBPF bytecode. I øjeblikket i gcc der er ingen serverdel til eBPF. Heldigvis, clang kan producere eBPF bytecode. Makefile bruger clang at kompilere tracex4_kern.c til objektfilen.

Jeg nævnte ovenfor, at en af ​​de mest interessante funktioner ved eBPF er kort. tracex4_kern definerer ét kort:

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 af ​​de mange korttyper, som eBPF tilbyder. I dette tilfælde er det bare en hash. Du har måske også lagt mærke til annoncen SEC("maps"). SEC er en makro, der bruges til at oprette en ny sektion af en binær fil. Faktisk i eksemplet tracex4_kern yderligere to sektioner er defineret:

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 funktioner giver dig mulighed for at fjerne en post fra kortet (kprobe/kmem_cache_free) og tilføj en ny post til kortet (kretprobe/kmem_cache_alloc_node). Alle funktionsnavne skrevet med store bogstaver svarer til makroer defineret i bpf_helpers.h.

Hvis jeg dumper sektionerne af objektfilen, skulle jeg se, at disse nye sektioner allerede er defineret:

$ 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

Der er også tracex4_user.c, hovedprogram. Grundlæggende lytter dette program efter begivenheder kmem_cache_alloc_node. Når en sådan hændelse opstår, udføres den tilsvarende eBPF-kode. Koden gemmer objektets IP-attribut på et kort, hvorefter objektet loopes gennem 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 hænger brugerrumsprogrammet og eBPF-programmet sammen? Ved initialisering tracex4_user.c indlæser objektfil tracex4_kern.o ved hjælp af funktionen 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;
}

Ved at gøre load_bpf_file prober defineret i eBPF-filen tilføjes /sys/kernel/debug/tracing/kprobe_events. Nu lytter vi efter disse begivenheder, og vores program kan gøre noget, når de sker.

$ 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 struktureret på samme måde. De indeholder altid to filer:

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

eBPF-programmet definerer de kort og funktioner, der er knyttet til et afsnit. Når kernen udsender en begivenhed af en bestemt type (f.eks. tracepoint), udføres de bundne funktioner. Kort giver kommunikation mellem et kerneprogram og et brugerrumsprogram.

Konklusion

I denne artikel blev BPF og eBPF diskuteret i generelle vendinger. Jeg ved, at der er en masse information og ressourcer om eBPF i dag, så jeg vil anbefale et par flere materialer til videre undersøgelse.

Jeg anbefaler at læse:

Kilde: www.habr.com

Tilføj en kommentar