In koarte yntroduksje ta BPF en eBPF

Hallo, Habr! Wy wolle jo graach ynformearje dat wy in boek meitsje foar frijlitting."Linux-observabiliteit mei BPF".

In koarte yntroduksje ta BPF en eBPF
Sûnt de firtuele masine BPF bliuwt evoluearje en aktyf wurdt brûkt yn 'e praktyk, hawwe wy in artikel foar jo oerset dat syn wichtichste mooglikheden en hjoeddeistige steat beskriuwt.

Yn 'e ôfrûne jierren binne programmearringsynstruminten en -techniken hieltyd populêrder wurden om de beheiningen fan' e Linux-kernel te kompensearjen yn gefallen wêr't hege prestaasjes pakketferwurking nedich is. Ien fan 'e populêrste techniken fan dit soarte wurdt neamd kernel bypass (kernel bypass) en lit, it omgean fan de kernel netwurk laach, alle pakketferwurking út brûkersromte útfiere. Bypassing fan de kernel ek omfiemet it kontrolearjen fan de netwurk kaart út brûkersromte. Mei oare wurden, by it wurkjen mei in netwurkkaart, fertrouwe wy op de bestjoerder brûkersromte.

Troch it oerdragen fan folsleine kontrôle fan 'e netwurkkaart nei in brûker-romteprogramma, ferminderje wy kernel-overhead (kontekstwikseling, netwurklaachferwurking, ûnderbrekkingen, ensfh.), Wat heul wichtich is as jo rinne mei snelheden fan 10Gb / s of heger. Kernel bypass plus in kombinaasje fan oare funksjes (batch ferwurking) en soarchfâldich ôfstimmen fan prestaasjes (NUMA boekhâlding, CPU isolaasje, ensfh.) oerienkomme mei de fûneminten fan hege prestaasjes netwurkferwurking yn brûkersromte. Miskien is in foarbyldich foarbyld fan dizze nije oanpak foar pakketferwurking DPDK fan Intel (Data Plane Development Kit), hoewol d'r oare bekende ark en techniken binne, ynklusyf Cisco's VPP (Vector Packet Processing), Netmap en, fansels, Snabb.

It organisearjen fan netwurkynteraksjes yn brûkersromte hat in oantal neidielen:

  • De OS-kern is in abstraksjelaach foar hardware-boarnen. Om't brûkersromteprogramma's har boarnen direkt moatte beheare, moatte se ek har eigen hardware beheare. Dit betsjut faak dat jo jo eigen bestjoerders moatte programmearje.
  • Om't wy kernelromte folslein opjaan, jouwe wy ek alle netwurkfunksjonaliteit op dy't troch de kernel wurdt levere. Brûkerromteprogramma's moatte funksjes opnij ymplementearje dy't al kinne wurde levere troch de kernel of it bestjoeringssysteem.
  • Programma's wurkje yn sandbox-modus, dy't har ynteraksje serieus beheint en foarkomt dat se yntegrearje mei oare dielen fan it bestjoeringssysteem.

Yn essinsje, by netwurkjen yn brûkersromte, wurde prestaasjeswinsten berikt troch pakketferwurking fan 'e kernel nei brûkersromte te ferpleatsen. XDP docht krekt it tsjinoerstelde: it ferpleatst netwurkprogramma's fan brûkersromte (filters, resolvers, routing, ensfh.) yn kernelromte. XDP lit ús in netwurkfunksje útfiere sa gau as in pakket in netwurkynterface treft en foardat it begjint te bewegen nei it kernelnetwurksubsysteem. As resultaat nimt de pakketferwurkingssnelheid signifikant ta. Hoe lit de kernel lykwols de brûker har programma's yn kernelromte útfiere? Foardat jo dizze fraach beäntwurdzje, litte wy sjen nei wat BPF is.

BPF en eBPF

Nettsjinsteande de betiizjende namme is BPF (Berkeley Packet Filtering) yn feite in firtuele masinemodel. Dizze firtuele masine is oarspronklik ûntworpen om pakketfiltering te behanneljen, fandêr de namme.

Ien fan 'e meast ferneamde ark mei help fan BPF is tcpdump. By it fêstlizzen fan pakketten mei tcpdump de brûker kin in útdrukking opjaan om pakketten te filterjen. Allinnich pakketten dy't oerienkomme mei dizze útdrukking sille wurde fêstlein. Bygelyks, de útdrukking "tcp dst port 80” ferwiist nei alle TCP-pakketten dy't oankomme op poarte 80. De kompilator kin dizze útdrukking ynkoarte troch it konvertearjen nei BPF-bytekoade.

$ 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 it boppesteande programma yn prinsipe docht:

  • Ynstruksje (000): Laadt it pakket by offset 12, as in 16-bit wurd, yn 'e accumulator. Offset 12 komt oerien mei it ethertype fan it pakket.
  • Ynstruksje (001): fergeliket de wearde yn 'e accumulator mei 0x86dd, dat is, mei de ethertypewearde foar IPv6. As it resultaat wier is, dan giet de programmateller nei ynstruksje (002), en sa net, dan nei (006).
  • Ynstruksje (006): fergeliket de wearde mei 0x800 (ethertypewearde foar IPv4). As it antwurd wier is, dan giet it programma nei (007), sa net, dan nei (015).

En sa fierder oant it pakketfilterprogramma in resultaat jout. Dit is normaal in Boolean. It werombringen fan in net-nul wearde (ynstruksje (014)) betsjut dat it pakket waard akseptearre, en werom in nul wearde (ynstruksje (015)) betsjut dat it pakket waard net akseptearre.

De BPF firtuele masine en syn bytekoade waarden foarsteld troch Steve McCann en Van Jacobson ein 1992 doe't harren papier waard publisearre BSD-pakketfilter: Nije arsjitektuer foar pakket capture op brûkersnivo, dizze technology waard foar it earst presintearre op 'e Usenix-konferinsje yn' e winter fan 1993.

Om't BPF in firtuele masine is, definiearret it de omjouwing wêryn programma's rinne. Neist de bytekoade definiearret it ek it batchûnthâldmodel (load-ynstruksjes wurde ymplisyt tapast op 'e batch), registers (A en X; accumulator- en yndeksregisters), opslach fan krasûnthâld en in ymplisite programmateller. Ynteressant is de BPF-bytekoade modelearre nei de Motorola 6502 ISA. As Steve McCann herinnerde yn syn plenêre ferslach by Sharkfest '11, hy wie bekend mei build 6502 út syn hege skoalle dagen programmearring op de Apple II, en dizze kennis beynfloede syn wurk designing de BPF bytecode.

BPF-stipe wurdt ymplementearre yn 'e Linux-kernel yn ferzjes v2.5 en heger, benammen tafoege troch de ynspanningen fan Jay Schullist. De BPF-koade bleau ûnferoare oant 2011, doe't Eric Dumaset de BPF-tolk opnij ûntwurp om yn JIT-modus te rinnen (Boarne: JIT foar pakketfilters). Hjirnei koe de kernel, ynstee fan BPF-bytekoade te ynterpretearjen, BPF-programma's direkt konvertearje nei de doelarsjitektuer: x86, ARM, MIPS, ensfh.

Letter, yn 2014, Alexey Starovoitov foarstelde in nij JIT meganisme foar BPF. Yn feite waard dizze nije JIT in nije BPF-basearre arsjitektuer en waard eBPF neamd. Ik tink dat beide VM's in skoft tegearre besteane, mar op it stuit wurdt pakketfiltering ymplementearre basearre op eBPF. Yn feite, yn in protte foarbylden fan moderne dokumintaasje, wurdt BPF begrepen as eBPF, en klassike BPF is hjoed bekend as cBPF.

eBPF wreidet de klassike BPF firtuele masine op ferskate manieren út:

  • Basearre op moderne 64-bit arsjitektuer. eBPF brûkt 64-bit registers en fergruttet it oantal beskikbere registers út 2 (accumulator en X) oan 10. eBPF jout ek ekstra opcodes (BPF_MOV, BPF_JNE, BPF_CALL ...).
  • Losmakke fan it netwurk laach subsysteem. BPF wie bûn oan it batchgegevensmodel. Sûnt it waard brûkt foar pakketfiltering, waard de koade pleatst yn it subsysteem dat netwurkkommunikaasje leveret. De eBPF firtuele masine is lykwols net mear bûn oan it gegevensmodel en kin brûkt wurde foar elk doel. Dat, no kin it eBPF-programma ferbûn wurde mei tracepoint of kprobe. Dit iepenet de wei nei eBPF-ynstrumintaasje, prestaasjesanalyse, en in protte oare gebrûksgefallen yn 'e kontekst fan oare kernel-subsystemen. No sit de eBPF-koade yn syn eigen paad: kernel/bpf.
  • Globale gegevenswinkels neamd Maps. Kaarten binne kaai-wearde winkels dy't ynskeakelje gegevens útwikseling tusken brûkersromte en kernel romte. eBPF biedt ferskate soarten kaarten.
  • Sekundêre funksjes. Benammen om in pakket te herskriuwen, in kontrôlesum te berekkenjen, of in pakket te klonen. Dizze funksjes rinne binnen de kernel en binne gjin programma's foar brûkersromte. Jo kinne ek systeemoproppen meitsje fan eBPF-programma's.
  • Oproppen einigje. De programmagrutte yn eBPF is beheind ta 4096 bytes. De sturtopropfunksje lit in eBPF-programma kontrôle oerdrage nei in nij eBPF-programma en dus dizze beheining omgean (oant 32 programma's kinne op dizze manier keppele wurde).

eBPF: foarbyld

D'r binne ferskate foarbylden foar eBPF yn 'e Linux kernel boarnen. Se binne te krijen by samples/bpf/. Om dizze foarbylden te kompilearjen, fier gewoan yn:

$ sudo make samples/bpf/

Ik sil net skriuwe in nij foarbyld foar eBPF mysels, mar sil brûke ien fan de samples beskikber yn samples / bpf /. Ik sil sjen op guon dielen fan 'e koade en útlizze hoe't it wurket. As foarbyld haw ik it programma keazen tracex4.

Yn 't algemien bestiet elk fan' e foarbylden yn samples/bpf/ út twa triemmen. Yn dit gefal:

  • tracex4_kern.c, befettet de boarnekoade dy't útfierd wurde yn 'e kearn as eBPF bytecode.
  • tracex4_user.c, befettet in programma fan brûkersromte.

Yn dit gefal moatte wy kompilearje tracex4_kern.c nei eBPF bytecode. Op it stuit yn gcc der is gjin backend foar eBPF. Lokkich, clang kin útfiere eBPF bytecode. Makefile brûkt clang foar kompilaasje tracex4_kern.c nei it objekttriem.

Ik neamde hjirboppe dat ien fan 'e meast nijsgjirrige funksjes fan eBPF kaarten binne. tracex4_kern definiearret ien 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 ien fan de protte soarten kaarten oanbean troch eBPF. Yn dit gefal is it gewoan in hash. Jo kinne ek hawwe opfallen in advertinsje SEC("maps"). SEC is in makro brûkt om in nije seksje fan in binêre triem te meitsjen. Eins, yn it foarbyld tracex4_kern noch twa seksjes wurde definiearre:

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

Mei dizze twa funksjes kinne jo in yngong fan de kaart wiskje (kprobe/kmem_cache_free) en foegje in nije yngong ta oan de kaart (kretprobe/kmem_cache_alloc_node). Alle funksjenammen skreaun yn haadletters komme oerien mei makro's definieare yn bpf_helpers.h.

As ik de seksjes fan it objektbestân dump, soe ik sjen moatte dat dizze nije seksjes al definiearre binne:

$ 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

Dêr is ek tracex4_user.c, haadprogramma. Yn prinsipe harket dit programma nei eveneminten kmem_cache_alloc_node. As sa'n evenemint bart, wurdt de oerienkommende eBPF-koade útfierd. De koade bewarret it IP-attribút fan it objekt yn in kaart, en it objekt wurdt dan troch it haadprogramma lutsen. Foarbyld:

$ 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 binne in brûkersromteprogramma en in eBPF-programma relatearre? Op inisjalisaasje tracex4_user.c laadt in foarwerp triem tracex4_kern.o mei help fan de funksje 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;
}

Troch te dwaan load_bpf_file probes definieare yn it eBPF-bestân wurde tafoege oan /sys/kernel/debug/tracing/kprobe_events. No harkje wy nei dizze eveneminten en ús programma kin wat dwaan as se barre.

$ 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 oare programma's yn sample/bpf/ binne op deselde manier strukturearre. Se befetsje altyd twa triemmen:

  • XXX_kern.c: eBPF programma.
  • XXX_user.c: haadprogramma.

It eBPF-programma identifisearret kaarten en funksjes dy't ferbûn binne mei in seksje. As de kernel in evenemint fan in bepaald type útjout (bygelyks, tracepoint), wurde de bûne funksjes útfierd. De kaarten jouwe kommunikaasje tusken it kernelprogramma en it programma foar brûkersromte.

konklúzje

Dit artikel besprutsen BPF en eBPF yn algemiene termen. Ik wit dat d'r hjoed in protte ynformaasje en boarnen is oer eBPF, dus ik sil in pear mear boarnen oanbefelje foar fierdere stúdzje

Ik advisearje it lêzen:

Boarne: www.habr.com

Add a comment