Kratek uvod v BPF in eBPF

Pozdravljeni, Habr! Obveščamo vas, da pripravljamo knjigo za izid."Opazljivost Linuxa z BPF".

Kratek uvod v BPF in eBPF
Ker se virtualni stroj BPF še naprej razvija in se aktivno uporablja v praksi, smo za vas prevedli članek, ki opisuje njegove glavne zmogljivosti in trenutno stanje.

V zadnjih letih postajajo vse bolj priljubljena programska orodja in tehnike, ki kompenzirajo omejitve jedra Linuxa v primerih, ko je potrebna visoko zmogljiva obdelava paketov. Ena najbolj priljubljenih tovrstnih tehnik se imenuje obvod jedra (kernel bypass) in omogoča, mimo omrežne plasti jedra, izvajanje celotne obdelave paketov iz uporabniškega prostora. Obhod jedra vključuje tudi nadzor omrežne kartice iz uporabniški prostor. Z drugimi besedami, pri delu z omrežno kartico se zanašamo na gonilnik uporabniški prostor.

S prenosom popolnega nadzora nad omrežno kartico na program v uporabniškem prostoru zmanjšamo obremenitev jedra (kontekstno preklapljanje, obdelava omrežne plasti, prekinitve itd.), kar je zelo pomembno pri delovanju s hitrostmi 10 Gb/s ali več. Obhod jedra in kombinacija drugih funkcij (paketna obdelava) in skrbno nastavitev delovanja (Računovodstvo NUMA, CPU izolacija, itd.) ustrezajo osnovam visoko zmogljivega omrežnega procesiranja v uporabniškem prostoru. Morda je zgleden primer tega novega pristopa k obdelavi paketov DPDK od Intel (Razvojni komplet Data Plane), čeprav obstajajo druga dobro znana orodja in tehnike, vključno s Ciscovim VPP (vektorska obdelava paketov), ​​Netmap in seveda Snabb.

Organiziranje omrežnih interakcij v uporabniškem prostoru ima številne pomanjkljivosti:

  • Jedro OS je abstraktna plast za vire strojne opreme. Ker morajo programi uporabniškega prostora neposredno upravljati svoje vire, morajo upravljati tudi lastno strojno opremo. To pogosto pomeni, da morate sami programirati gonilnike.
  • Ker se v celoti odpovedujemo prostoru jedra, se odpovedujemo tudi vsem omrežnim funkcijam, ki jih zagotavlja jedro. Programi uporabniškega prostora morajo znova implementirati funkcije, ki jih morda že ponuja jedro ali operacijski sistem.
  • Programi delujejo v načinu peskovnika, kar resno omejuje njihovo interakcijo in onemogoča integracijo z drugimi deli operacijskega sistema.

V bistvu se pri omrežju v uporabniškem prostoru izboljšanje zmogljivosti doseže s premikanjem obdelave paketov iz jedra v uporabniški prostor. XDP deluje ravno nasprotno: premakne omrežne programe iz uporabniškega prostora (filtri, razreševalci, usmerjanje itd.) v prostor jedra. XDP nam omogoča izvajanje omrežne funkcije takoj, ko paket zadene omrežni vmesnik in preden se začne premikati navzgor v podsistem omrežja jedra. Posledično se hitrost obdelave paketov znatno poveča. Vendar, kako jedro dovoljuje uporabniku, da izvaja svoje programe v prostoru jedra? Preden odgovorimo na to vprašanje, poglejmo, kaj je BPF.

BPF in eBPF

Kljub zmedenemu imenu je BPF (Berkeley Packet Filtering) v resnici model virtualnega stroja. Ta virtualni stroj je bil prvotno zasnovan za filtriranje paketov, od tod tudi ime.

Eno najbolj znanih orodij, ki uporabljajo BPF, je tcpdump. Pri zajemanju paketov z uporabo tcpdump uporabnik lahko določi izraz za filtriranje paketov. Zajeti bodo samo paketi, ki se ujemajo s tem izrazom. Na primer, izraz "tcp dst port 80” se nanaša na vse pakete TCP, ki prispejo na vrata 80. Prevajalnik lahko ta izraz skrajša tako, da ga pretvori v bajtno kodo BPF.

$ 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

To je tisto, kar zgornji program v bistvu naredi:

  • Navodilo (000): naloži paket pri odmiku 12 kot 16-bitno besedo v akumulator. Odmik 12 ustreza tipu ether paketa.
  • Navodilo (001): primerja vrednost v akumulatorju z 0x86dd, to je z vrednostjo ethertype za IPv6. Če je rezultat resničen, gre programski števec na ukaz (002), če ni, pa na (006).
  • Navodilo (006): primerja vrednost z 0x800 (vrednost ethertype za IPv4). Če je odgovor pravilen, gre program na (007), če ni, pa na (015).

In tako naprej, dokler program za filtriranje paketov ne vrne rezultata. To je običajno logična vrednost. Vrnitev vrednosti, ki ni nič (navodilo (014)), pomeni, da je bil paket sprejet, vrnitev nič (navodilo (015)) pomeni, da paket ni bil sprejet.

Virtualni stroj BPF in njegovo bajtno kodo sta predlagala Steve McCann in Van Jacobson konec leta 1992, ko je bil objavljen njihov članek. Filter paketov BSD: nova arhitektura za zajem paketov na ravni uporabnika, je bila ta tehnologija prvič predstavljena na konferenci Usenix pozimi 1993.

Ker je BPF virtualni stroj, definira okolje, v katerem se izvajajo programi. Poleg bajtne kode definira tudi paketni pomnilniški model (navodila za nalaganje se implicitno uporabljajo za paket), registre (A in X; zbiralnik in indeksni registri), shrambo pomnilnika za praske in implicitni programski števec. Zanimivo je, da je bila bajtna koda BPF oblikovana po modelu Motorola 6502 ISA. Kot se je spomnil Steve McCann v svojem plenarno poročilo na Sharkfestu '11 je bil seznanjen z gradnjo 6502 iz srednješolskih dni programiranja na Apple II in to znanje je vplivalo na njegovo delo pri oblikovanju bajtne kode BPF.

Podpora za BPF je implementirana v jedru Linuxa v različicah v2.5 in novejših, dodana predvsem s prizadevanji Jaya Schullista. Koda BPF je ostala nespremenjena do leta 2011, ko je Eric Dumaset preoblikoval tolmač BPF za delovanje v načinu JIT (Vir: JIT za paketne filtre). Po tem bi lahko jedro namesto interpretacije bajtne kode BPF neposredno pretvorilo programe BPF v ciljno arhitekturo: x86, ARM, MIPS itd.

Kasneje, leta 2014, je Alexey Starovoitov predlagal nov mehanizem JIT za BPF. Pravzaprav je ta novi JIT postal nova arhitektura, ki temelji na BPF, in se je imenoval eBPF. Mislim, da sta oba VM obstajala nekaj časa, trenutno pa je filtriranje paketov implementirano na podlagi eBPF. Pravzaprav je v številnih primerih sodobne dokumentacije BPF razumljen kot eBPF, klasični BPF pa je danes znan kot cBPF.

eBPF razširja klasični virtualni stroj BPF na več načinov:

  • Temelji na sodobnih 64-bitnih arhitekturah. eBPF uporablja 64-bitne registre in poveča število razpoložljivih registrov z 2 (akumulator in X) na 10. eBPF nudi tudi dodatne opcode (BPF_MOV, BPF_JNE, BPF_CALL ...).
  • Ločeno od podsistema omrežne plasti. BPF je bil vezan na paketni podatkovni model. Ker je bil uporabljen za filtriranje paketov, se je njegova koda nahajala v podsistemu, ki zagotavlja omrežne komunikacije. Vendar virtualni stroj eBPF ni več vezan na podatkovni model in ga je mogoče uporabiti za kateri koli namen. Torej, zdaj lahko program eBPF povežete s tracepoint ali kprobe. To odpira pot do instrumentacije eBPF, analize zmogljivosti in številnih drugih primerov uporabe v kontekstu drugih podsistemov jedra. Zdaj se koda eBPF nahaja na svoji poti: kernel/bpf.
  • Globalne shrambe podatkov, imenovane Zemljevidi. Zemljevidi so shrambe ključev in vrednosti, ki omogočajo izmenjavo podatkov med uporabniškim prostorom in prostorom jedra. eBPF ponuja več vrst zemljevidov.
  • Sekundarne funkcije. Zlasti za ponovno pisanje paketa, izračun kontrolne vsote ali kloniranje paketa. Te funkcije se izvajajo znotraj jedra in niso programi uporabniškega prostora. Sistemske klice lahko opravljate tudi iz programov eBPF.
  • Končajte klice. Velikost programa v eBPF je omejena na 4096 bajtov. Funkcija repnega klica omogoča programu eBPF, da prenese nadzor na nov program eBPF in tako zaobide to omejitev (na ta način je mogoče povezati do 32 programov).

eBPF: primer

Obstaja več primerov za eBPF v izvornih kodah jedra Linuxa. Na voljo so na vzorcih/bpf/. Če želite sestaviti te primere, preprosto vnesite:

$ sudo make samples/bpf/

Sam ne bom napisal novega primera za eBPF, ampak bom uporabil enega od vzorcev, ki so na voljo v samples/bpf/. Ogledal si bom nekaj delov kode in razložil, kako deluje. Kot primer sem izbral program tracex4.

Na splošno je vsak od primerov v samples/bpf/ sestavljen iz dveh datotek. V tem primeru:

  • tracex4_kern.c, vsebuje izvorno kodo, ki se izvede v jedru kot bajtno kodo eBPF.
  • tracex4_user.c, vsebuje program iz uporabniškega prostora.

V tem primeru moramo prevesti tracex4_kern.c v bajtno kodo eBPF. Trenutno v gcc za eBPF ni ozadja. na srečo, clang lahko izpiše bajtno kodo eBPF. Makefile uporablja clang za kompilacijo tracex4_kern.c v objektno datoteko.

Zgoraj sem omenil, da so ena najzanimivejših funkcij eBPF zemljevidi. tracex4_kern definira en zemljevid:

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 je ena od mnogih vrst kartic, ki jih ponuja eBPF. V tem primeru gre samo za zgoščevanje. Morda ste opazili tudi oglas SEC("maps"). SEC je makro, ki se uporablja za ustvarjanje novega odseka binarne datoteke. Pravzaprav v primeru tracex4_kern definirana sta še dva razdelka:

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

Ti dve funkciji omogočata brisanje vnosa z zemljevida (kprobe/kmem_cache_free) in dodajte nov vnos na zemljevid (kretprobe/kmem_cache_alloc_node). Vsa imena funkcij, napisana z velikimi črkami, ustrezajo makrom, definiranim v bpf_helpers.h.

Če izpišem razdelke objektne datoteke, bi moral videti, da so ti novi razdelki že definirani:

$ 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

Je tudi tracex4_user.c, glavni program. V bistvu ta program posluša dogodke kmem_cache_alloc_node. Ko pride do takega dogodka, se izvede ustrezna koda eBPF. Koda shrani atribut IP objekta v zemljevid, objekt pa se nato vrne skozi glavni program. primer:

$ 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

Kako sta povezana program uporabniškega prostora in program eBPF? Pri inicializaciji tracex4_user.c naloži objektno datoteko tracex4_kern.o uporabo funkcije 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;
}

Medtem ko delaš load_bpf_file dodane sonde, definirane v datoteki eBPF /sys/kernel/debug/tracing/kprobe_events. Zdaj poslušamo te dogodke in naš program lahko nekaj stori, ko se zgodijo.

$ 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

Vsi ostali programi v sample/bpf/ so strukturirani podobno. Vedno vsebujejo dve datoteki:

  • XXX_kern.c: program eBPF.
  • XXX_user.c: glavni program.

Program eBPF identificira zemljevide in funkcije, povezane z odsekom. Ko jedro izda dogodek določene vrste (npr. tracepoint), se izvršijo vezane funkcije. Kartice zagotavljajo komunikacijo med programom jedra in programom uporabniškega prostora.

Zaključek

Ta članek je obravnaval BPF in eBPF na splošno. Vem, da je danes veliko informacij in virov o eBPF, zato bom priporočil še nekaj virov za nadaljnje študije

Priporočam branje:

Vir: www.habr.com

Dodaj komentar