O scurtă introducere în BPF și eBPF

Bună, Habr! Dorim să vă informăm că pregătim o carte pentru lansare.”Observabilitate Linux cu BPF".

O scurtă introducere în BPF și eBPF
Deoarece mașina virtuală BPF continuă să evolueze și este utilizată activ în practică, am tradus pentru dvs. un articol care descrie principalele sale capabilități și starea actuală.

În ultimii ani, instrumentele și tehnicile de programare au devenit din ce în ce mai populare pentru a compensa limitările nucleului Linux în cazurile în care este necesară procesarea de pachete de înaltă performanță. Una dintre cele mai populare tehnici de acest gen se numește bypass al nucleului (bypass kernel) și permite, ocolind stratul de rețea kernel, să efectueze toate procesarea pachetelor din spațiul utilizatorului. Ocolirea nucleului implică și controlul plăcii de rețea de la spatiu utilizator. Cu alte cuvinte, atunci când lucrăm cu o placă de rețea, ne bazăm pe șofer spatiu utilizator.

Transferând controlul complet al plăcii de rețea către un program de spațiu utilizator, reducem supraîncărcarea nucleului (schimbarea contextului, procesarea stratului de rețea, întreruperi etc.), ceea ce este destul de important atunci când rulăm la viteze de 10 Gb/s sau mai mari. Bypass kernel plus o combinație de alte caracteristici (procesare în lot) și reglarea atentă a performanței (Contabilitatea NUMA, Izolarea procesorului, etc.) corespund fundamentelor procesării de înaltă performanță a rețelei în spațiul utilizatorului. Poate că un exemplu exemplar al acestei noi abordări a procesării pachetelor este DPDK de la Intel (Kit de dezvoltare a planului de date), deși există și alte instrumente și tehnici binecunoscute, inclusiv VPP (Vector Packet Processing) de la Cisco, Netmap și, desigur, snab.

Organizarea interacțiunilor de rețea în spațiul utilizatorului are o serie de dezavantaje:

  • Nucleul sistemului de operare este un strat de abstractizare pentru resursele hardware. Deoarece programele spațiale ale utilizatorului trebuie să își gestioneze resursele în mod direct, ele trebuie să își gestioneze și propriul hardware. Acest lucru înseamnă adesea să vă programați propriile drivere.
  • Deoarece renunțăm în întregime la spațiul nucleului, renunțăm, de asemenea, la toată funcționalitatea de rețea oferită de nucleu. Programele din spațiul utilizatorului trebuie să reimplementeze caracteristici care pot fi deja furnizate de kernel sau de sistemul de operare.
  • Programele funcționează în modul sandbox, ceea ce limitează serios interacțiunea lor și le împiedică să se integreze cu alte părți ale sistemului de operare.

În esență, atunci când rețeaua are loc în spațiul utilizatorului, câștigurile de performanță sunt obținute prin mutarea procesării pachetelor din nucleu în spațiul utilizatorului. XDP face exact invers: mută programele de rețea din spațiul utilizatorului (filtre, soluții, rutare etc.) în spațiul kernel. XDP ne permite să îndeplinim o funcție de rețea de îndată ce un pachet atinge o interfață de rețea și înainte de a începe să se miște în subsistemul rețelei kernel. Ca urmare, viteza de procesare a pachetelor crește semnificativ. Cu toate acestea, cum permite nucleul utilizatorului să-și execute programele în spațiul nucleului? Înainte de a răspunde la această întrebare, să ne uităm la ce este BPF.

BPF și eBPF

În ciuda numelui confuz, BPF (Berkeley Packet Filtering) este, de fapt, un model de mașină virtuală. Această mașină virtuală a fost proiectată inițial pentru a gestiona filtrarea pachetelor, de unde și numele.

Unul dintre cele mai cunoscute instrumente care utilizează BPF este tcpdump. La capturarea pachetelor folosind tcpdump utilizatorul poate specifica o expresie pentru a filtra pachetele. Vor fi capturate numai pachetele care corespund acestei expresii. De exemplu, expresia „tcp dst port 80” se referă la toate pachetele TCP care sosesc pe portul 80. Compilatorul poate scurta această expresie transformând-o în bytecode 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

Acesta este ceea ce face programul de mai sus:

  • Instrucțiune (000): Încarcă pachetul la offset 12, ca un cuvânt de 16 biți, în acumulator. Offset-ul 12 corespunde tipului eter al pachetului.
  • Instrucțiune (001): compară valoarea din acumulator cu 0x86dd, adică cu valoarea ethertype pentru IPv6. Dacă rezultatul este adevărat, atunci contorul programului merge la instrucțiunea (002), iar dacă nu, atunci la (006).
  • Instrucțiune (006): compară valoarea cu 0x800 (valoarea ethertype pentru IPv4). Dacă răspunsul este adevărat, atunci programul merge la (007), dacă nu, atunci la (015).

Și așa mai departe până când programul de filtrare a pachetelor returnează un rezultat. Acesta este de obicei un boolean. Returnarea unei valori diferite de zero (instrucțiunea (014)) înseamnă că pachetul a fost acceptat, iar returnarea unei valori zero (instrucțiunea (015)) înseamnă că pachetul nu a fost acceptat.

Mașina virtuală BPF și codul său de octeți au fost propuse de Steve McCann și Van Jacobson la sfârșitul anului 1992, când lucrarea lor a fost publicată Filtru de pachete BSD: nouă arhitectură pentru capturarea pachetelor la nivel de utilizator, această tehnologie a fost prezentată pentru prima dată la conferința Usenix din iarna anului 1993.

Deoarece BPF este o mașină virtuală, definește mediul în care rulează programele. Pe lângă bytecode, definește și modelul de memorie batch (instrucțiunile de încărcare sunt aplicate implicit lotului), registrele (A și X; registre de acumulator și index), stocarea memoriei scratch și un contor de program implicit. Interesant este că bytecode-ul BPF a fost modelat după Motorola 6502 ISA. După cum și-a amintit Steve McCann în a lui raportul plenului la Sharkfest '11, era familiarizat cu build 6502 din programarea lui de liceu pe Apple II, iar aceste cunoștințe i-au influențat munca de proiectare a codului de octet BPF.

Suportul BPF este implementat în nucleul Linux în versiunile v2.5 și superioare, adăugat în principal prin eforturile lui Jay Schullist. Codul BPF a rămas neschimbat până în 2011, când Eric Dumaset a reproiectat interpretul BPF pentru a rula în modul JIT (Sursa: JIT pentru filtre de pachete). După aceasta, nucleul, în loc să interpreteze bytecode BPF, ar putea converti direct programele BPF în arhitectura țintă: x86, ARM, MIPS etc.

Mai târziu, în 2014, Alexey Starovoitov a propus un nou mecanism JIT pentru BPF. De fapt, acest nou JIT a devenit o nouă arhitectură bazată pe BPF și s-a numit eBPF. Cred că ambele VM au coexistat de ceva timp, dar în prezent filtrarea pachetelor este implementată pe baza eBPF. De fapt, în multe exemple de documentație modernă, BPF este înțeles ca fiind eBPF, iar BPF clasic este astăzi cunoscut sub numele de cBPF.

eBPF extinde mașina virtuală clasică BPF în mai multe moduri:

  • Bazat pe arhitecturi moderne pe 64 de biți. eBPF folosește registre pe 64 de biți și crește numărul de registre disponibile de la 2 (acumulator și X) la 10. eBPF oferă și coduri operaționale suplimentare (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Detașat din subsistemul stratului de rețea. BPF a fost legat de modelul de date pe lot. Deoarece a fost folosit pentru filtrarea pachetelor, codul său a fost localizat în subsistemul care oferă comunicații în rețea. Cu toate acestea, mașina virtuală eBPF nu mai este legată de modelul de date și poate fi utilizată în orice scop. Deci, acum programul eBPF poate fi conectat la tracepoint sau kprobe. Aceasta deschide calea către instrumentarea eBPF, analiza performanței și multe alte cazuri de utilizare în contextul altor subsisteme kernel. Acum codul eBPF este localizat în propria cale: kernel/bpf.
  • Magazine de date globale numite Maps. Hărțile sunt depozite cheie-valoare care permit schimbul de date între spațiul utilizator și spațiul kernel. eBPF oferă mai multe tipuri de hărți.
  • Funcții secundare. În special, pentru a rescrie un pachet, a calcula o sumă de control sau a clona un pachet. Aceste funcții rulează în interiorul nucleului și nu sunt programe în spațiul utilizatorului. De asemenea, puteți efectua apeluri de sistem din programele eBPF.
  • Terminați apelurile. Dimensiunea programului în eBPF este limitată la 4096 de octeți. Caracteristica tail call permite unui program eBPF să transfere controlul către un nou program eBPF și astfel să ocolească această limitare (până la 32 de programe pot fi conectate în acest fel).

eBPF: exemplu

Există mai multe exemple pentru eBPF în sursele kernel-ului Linux. Sunt disponibile la samples/bpf/. Pentru a compila aceste exemple, introduceți pur și simplu:

$ sudo make samples/bpf/

Nu voi scrie un exemplu nou pentru eBPF, dar voi folosi unul dintre mostrele disponibile în samples/bpf/. Mă voi uita la unele părți ale codului și voi explica cum funcționează. Ca exemplu, am ales programul tracex4.

În general, fiecare dintre exemplele din samples/bpf/ constă din două fișiere. În acest caz:

  • tracex4_kern.c, conține codul sursă care urmează să fie executat în nucleu ca bytecode eBPF.
  • tracex4_user.c, conține un program din spațiul utilizatorului.

În acest caz, trebuie să compilăm tracex4_kern.c la eBPF bytecode. Momentan în gcc nu există backend pentru eBPF. Din fericire, clang poate scoate eBPF bytecode. Makefile utilizări clang pentru compilare tracex4_kern.c la fișierul obiect.

Am menționat mai sus că una dintre cele mai interesante caracteristici ale eBPF sunt hărțile. tracex4_kern definește o hartă:

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 este unul dintre numeroasele tipuri de carduri oferite de eBPF. În acest caz, este doar un hash. Este posibil să fi observat și un anunț SEC("maps"). SEC este o macrocomandă folosită pentru a crea o nouă secțiune a unui fișier binar. De fapt, în exemplu tracex4_kern mai sunt definite două secțiuni:

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

Aceste două funcții vă permit să ștergeți o intrare de pe hartă (kprobe/kmem_cache_free) și adăugați o nouă intrare pe hartă (kretprobe/kmem_cache_alloc_node). Toate numele de funcții scrise cu majuscule corespund macrocomenzilor definite în bpf_helpers.h.

Dacă arunc secțiunile fișierului obiect, ar trebui să văd că aceste noi secțiuni sunt deja definite:

$ 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

Încă mai au tracex4_user.c, programul principal. Practic, acest program ascultă evenimente kmem_cache_alloc_node. Când are loc un astfel de eveniment, este executat codul eBPF corespunzător. Codul salvează atributul IP al obiectului într-o hartă, iar obiectul este apoi trecut în buclă prin programul principal. Exemplu:

$ 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

Cum sunt legate un program de spațiu utilizator și un program eBPF? La inițializare tracex4_user.c încarcă un fișier obiect tracex4_kern.o folosind funcția 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;
}

În timp ce face load_bpf_file se adaugă probele definite în fișierul eBPF /sys/kernel/debug/tracing/kprobe_events. Acum ascultăm aceste evenimente și programul nostru poate face ceva atunci când se întâmplă.

$ 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

Toate celelalte programe din sample/bpf/ sunt structurate în mod similar. Acestea conțin întotdeauna două fișiere:

  • XXX_kern.c: programul eBPF.
  • XXX_user.c: programul principal.

Programul eBPF identifică hărți și funcții asociate unei secțiuni. Când nucleul emite un eveniment de un anumit tip (de exemplu, tracepoint), funcțiile legate sunt executate. Cardurile asigură comunicarea între programul kernel și programul spațiului utilizator.

Concluzie

Acest articol a discutat BPF și eBPF în termeni generali. Știu că există o mulțime de informații și resurse despre eBPF astăzi, așa că vă voi recomanda câteva resurse suplimentare pentru studii suplimentare

Vă recomand să citiți:

Sursa: www.habr.com

Adauga un comentariu