'n Kort inleiding tot BPF en eBPF

Haai Habr! Ons stel jou in kennis dat ons voorberei om 'n boek vry te stel "Linux waarneembaarheid met BPF".

'n Kort inleiding tot BPF en eBPF
Aangesien die BPF virtuele masjien voortgaan om te ontwikkel en aktief in die praktyk gebruik word, het ons 'n artikel vir jou vertaal wat sy hoofkenmerke en huidige toestand beskryf.

In onlangse jare het programmeringsinstrumente en -tegnieke gewild geword om te vergoed vir die beperkings van die Linux-kern in gevalle waar hoëprestasie-pakketverwerking vereis word. Een van die gewildste metodes van hierdie soort word genoem kern omleiding (kern-omleiding) en laat toe, deur die netwerklaag van die kern oor te slaan, om alle pakkieverwerking vanuit gebruikersruimte uit te voer. Om die kern te omseil behels ook die bestuur van die netwerkkaart vanaf gebruikersruimte. Met ander woorde, wanneer ons met 'n netwerkkaart werk, maak ons ​​staat op die bestuurder gebruikersruimte.

Deur volle beheer van die netwerkkaart na 'n gebruikersspasieprogram oor te dra, verminder ons die oorhoofse koste wat deur die kern veroorsaak word (konteksskakelaars, netwerklaagverwerking, onderbrekings, ens.), wat nogal belangrik is wanneer dit teen snelhede van 10Gb/s of hoër. Om die kern te omseil plus 'n kombinasie van ander kenmerke (Batch verwerking) en noukeurige verrigtingafstelling (NUMA rekeningkunde, SVE isolasie, ens.) pas by die basiese beginsels van hoëwerkverrigting gebruiker-ruimte-netwerk. Miskien is 'n voorbeeldige voorbeeld van hierdie nuwe benadering tot pakkieverwerking DPDK van Intel (Data Plane Development Kit), hoewel daar ander bekende gereedskap en tegnieke is, insluitend VPP van Cisco (Vector Packet Processing), Netmap en, natuurlik, Snaaks.

Die organisasie van netwerkinteraksies in gebruikersruimte het 'n aantal nadele:

  • 'n OS-kern is 'n abstraksielaag vir hardewarehulpbronne. Omdat gebruikersruimte-programme hul hulpbronne direk moet bestuur, moet hulle ook hul eie hardeware bestuur. Dit beteken dikwels dat jy jou eie drywers programmeer.
  • Aangesien ons kernspasie heeltemal prysgee, gee ons ook afstand van al die netwerkfunksies wat deur die kern verskaf word. Gebruikersruimteprogramme moet kenmerke wat reeds deur die kern of bedryfstelsel verskaf word, herimplementeer.
  • Programme werk in 'n sandbox-modus, wat hul interaksie ernstig beperk en verhoed dat hulle met ander dele van die bedryfstelsel integreer.

In wese, wanneer daar in gebruikersruimte genetwerk word, word prestasiewinste behaal deur pakkieverwerking van die kern na gebruikersruimte te skuif. XDP doen presies die teenoorgestelde: dit skuif netwerkprogramme van gebruikersruimte (filters, omskakelaars, roetering, ens.) na die kernarea. XDP stel ons in staat om die netwerkfunksie uit te voer sodra die pakkie die netwerkkoppelvlak tref en voordat dit na die netwerksubstelsel van die kern begin beweeg. As gevolg hiervan word die pakketverwerkingspoed aansienlik verhoog. Hoe laat die kern die gebruiker egter toe om hul programme in kernspasie te laat loop? Voordat ons hierdie vraag beantwoord, kom ons kyk na wat BPF is.

BPF en eBPF

Ten spyte van die nie heeltemal duidelike naam nie, is BPF (Packet Filtering, Berkeley) in werklikheid 'n virtuele masjienmodel. Hierdie virtuele masjien is oorspronklik ontwerp om pakkiefiltrering te hanteer, vandaar die naam.

Een van die meer bekende instrumente wat BPF gebruik, is tcpdump. Wanneer pakkies vasgelê word met tcpdump die gebruiker kan 'n uitdrukking vir pakkiefiltrering spesifiseer. Slegs pakkies wat by hierdie uitdrukking pas, sal vasgelê word. Byvoorbeeld, die uitdrukking "tcp dst port 80” verwys na alle TCP-pakkies wat op poort 80 aankom. Die samesteller kan hierdie uitdrukking verkort deur dit na BPF-greepkode om te skakel.

$ 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 basies wat die bogenoemde program doen:

  • Instruksie (000): Laai 'n pakkie teen offset 12, as 'n 16-bis woord, in die akkumulator. Offset 12 stem ooreen met die etertipe van die pakkie.
  • Instruksie (001): vergelyk die waarde in die akkumulator met 0x86dd, dit wil sê met die etertipe waarde vir IPv6. As die resultaat waar is, gaan die programteller na instruksie (002), en indien nie, dan na (006).
  • Instruksie (006): vergelyk die waarde met 0x800 (etertipe waarde vir IPv4). As die antwoord waar is, gaan die program na (007), indien nie, dan na (015).

En so aan, totdat die pakkiefiltreringsprogram 'n resultaat gee. Gewoonlik is dit boolean. Om 'n nie-nul waarde (instruksie (014)) terug te gee, beteken dat die pakkie ooreenstem, en om nul terug te gee (instruksie (015)) beteken dat die pakkie nie ooreenstem nie.

Die virtuele BPF-masjien en sy greepkode is laat in 1992 deur Steve McCann en Van Jacobson voorgestel toe hul koerant uitgekom het. BSD Pakketfilter: Nuwe argitektuur vir pakketopname op gebruikersvlak, vir die eerste keer is hierdie tegnologie by die Usenix-konferensie in die winter van 1993 aangebied.

Omdat BPF 'n virtuele masjien is, definieer dit die omgewing waarin programme loop. Benewens greepkode, definieer dit ook 'n pakkiegeheuemodel (laaiinstruksies word implisiet op 'n pakkie toegepas), registers (A en X; akkumulator- en indeksregisters), krapgeheueberging en 'n implisiete programteller. Interessant genoeg is die BPF-greepkode gemodelleer na die Motorola 6502 ISA. Soos Steve McCann in syne onthou plenêre verslag by Sharkfest '11, was hy vertroud met bou 6502 vanaf hoërskool toe hy op die Apple II geprogrammeer het, en hierdie kennis het sy werk met die ontwerp van die BPF-greepkode beïnvloed.

BPF-ondersteuning word in die Linux-kern geïmplementeer in weergawe v2.5 en later, hoofsaaklik bygevoeg deur Jay Schullist. Die BPF-kode het onveranderd gebly tot 2011, toe Eric Dumaset die BPF-tolk herontwerp het om in JIT-modus te werk (Bron: JIT vir Pakkie Filters). Daarna, in plaas daarvan om die BPF-greepkode te interpreteer, kan die kern BPF-programme direk na die teikenargitektuur omskakel: x86, ARM, MIPS, ens.

Later, in 2014, het Alexei Starovoitov 'n nuwe JIT-meganisme vir BPF voorgestel. Trouens, hierdie nuwe JIT het 'n nuwe argitektuur geword wat op BPF gebaseer is en is eBPF genoem. Ek dink beide VM's het 'n geruime tyd saam bestaan, maar pakkiefiltrering word tans bo-op eBPF geïmplementeer. Trouens, in baie moderne dokumentasievoorbeelde word na BPF verwys as eBPF, en klassieke BPF staan ​​vandag bekend as cBPF.

eBPF brei die klassieke BPF virtuele masjien op verskeie maniere uit:

  • Maak staat op moderne 64-bis-argitekture. eBPF gebruik 64-bis registers en verhoog die aantal beskikbare registers van 2 (akkumulator en X) tot 10. eBPF verskaf ook addisionele opkodes (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Losgemaak van die netwerklaagsubstelsel. BPF was gekoppel aan die bondeldatamodel. Aangesien dit gebruik is om pakkies te filter, was die kode in die substelsel wat netwerkinteraksies verskaf het. Die eBPF virtuele masjien is egter nie meer gebonde aan 'n datamodel nie en kan vir enige doel gebruik word. So, nou kan die eBPF-program aan spoorpunt of aan kprobe gekoppel word. Dit maak die deur oop vir eBPF-instrumentasie, prestasie-analise en baie ander gebruiksgevalle in die konteks van ander kernsubstelsels. Nou is die eBPF-kode in sy eie pad geleë: kern/bpf.
  • Globale datawinkels genaamd Maps. Kaarte is sleutelwaarde-winkels wat data-uitruiling tussen gebruikersruimte en kernspasie verskaf. eBPF bied verskeie soorte kaarte.
  • Sekondêre funksies. In die besonder, om 'n pakket te oorskryf, 'n kontrolesom te bereken of 'n pakket te kloon. Hierdie funksies loop binne die kern en behoort nie aan gebruikersruimteprogramme nie. Daarbenewens kan stelseloproepe vanaf eBPF-programme gemaak word.
  • Beëindig oproepe. Programgrootte in eBPF is beperk tot 4096 grepe. Die eindoproepfunksie laat 'n eBPF-program toe om beheer na 'n nuwe eBPF-program oor te dra en sodoende hierdie beperking te omseil (tot 32 programme kan op hierdie manier vasgeketting word).

eBPF voorbeeld

Daar is verskeie voorbeelde vir eBPF in die Linux-kernbronne. Hulle is beskikbaar by samples/bpf/. Om hierdie voorbeelde saam te stel, tik net:

$ sudo make samples/bpf/

Ek sal nie self 'n nuwe voorbeeld vir eBPF skryf nie, maar sal een van die voorbeelde wat beskikbaar is in samples/bpf/ gebruik. Ek sal na sommige dele van die kode kyk en verduidelik hoe dit werk. As voorbeeld het ek die program gekies tracex4.

Oor die algemeen bestaan ​​elk van die voorbeelde in monsters/bpf/ uit twee lêers. In hierdie geval:

  • tracex4_kern.c, bevat bronkode wat in die kern as eBPF-greepkode uitgevoer moet word.
  • tracex4_user.c, bevat 'n program vanaf gebruikersruimte.

In hierdie geval moet ons saamstel tracex4_kern.c na eBPF-greepkode. Op die oomblik in gcc daar is geen bedienerdeel vir eBPF nie. Gelukkig, clang kan eBPF-greepkode produseer. Makefile gebruike clang saam te stel tracex4_kern.c na die objeklêer.

Ek het hierbo genoem dat een van die interessantste kenmerke van eBPF kaarte is. tracex4_kern definieer een 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 die vele kaarttipes wat deur eBPF aangebied word. In hierdie geval is dit net 'n hash. Jy het dalk ook die advertensie opgemerk SEC("maps"). SEC is 'n makro wat gebruik word om 'n nuwe afdeling van 'n binêre lêer te skep. Eintlik, in die voorbeeld tracex4_kern nog twee afdelings word gedefinieer:

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

Hierdie twee funksies laat jou toe om 'n inskrywing van die kaart te verwyder (kprobe/kmem_cache_free) en voeg 'n nuwe inskrywing by die kaart (kretprobe/kmem_cache_alloc_node). Alle funksiename wat in hoofletters geskryf is, stem ooreen met makro's wat in bpf_helpers.h.

As ek die afdelings van die objeklêer stort, moet ek sien dat hierdie nuwe afdelings reeds gedefinieer is:

$ 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

Daar is ook tracex4_user.c, hoofprogram. Basies luister hierdie program na gebeure kmem_cache_alloc_node. Wanneer so 'n gebeurtenis plaasvind, word die ooreenstemmende eBPF-kode uitgevoer. Die kode stoor die voorwerp se IP-kenmerk op 'n kaart, en dan word die voorwerp deur die hoofprogram gelus. 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 is die gebruikersruimteprogram en die eBPF-program verwant? By inisialisering tracex4_user.c laai voorwerplêer tracex4_kern.o die funksie te gebruik 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;
}

Terwyl jy doen load_bpf_file probes wat in die eBPF-lêer gedefinieer is, word bygevoeg /sys/kernel/debug/tracing/kprobe_events. Nou luister ons na hierdie gebeure en ons program kan iets doen wanneer dit gebeur.

$ 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 ander programme in sample/bpf/ is soortgelyk gestruktureer. Hulle bevat altyd twee lêers:

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

Die eBPF-program definieer die kaarte en funksies wat met 'n afdeling geassosieer word. Wanneer die kern 'n gebeurtenis van 'n sekere tipe uitstuur (byvoorbeeld, tracepoint), word die gebonde funksies uitgevoer. Kaarte verskaf kommunikasie tussen 'n kernprogram en 'n gebruikerruimteprogram.

Gevolgtrekking

In hierdie artikel is BPF en eBPF in algemene terme bespreek. Ek weet dat daar vandag baie inligting en hulpbronne oor eBPF is, so ek sal nog 'n paar materiaal aanbeveel vir verdere studie.

Ek beveel aan om te lees:

Bron: will.com

Voeg 'n opmerking