Kratak uvod u BPF i eBPF

Hej Habr! Obavještavamo vas da se pripremamo za izdavanje knjige"Linux Observability sa BPF-om".

Kratak uvod u BPF i eBPF
Kako BPF virtuelna mašina nastavlja da se razvija i aktivno se koristi u praksi, za vas smo preveli članak koji opisuje njegove glavne karakteristike i trenutno stanje.

Posljednjih godina, programski alati i tehnike su stekli popularnost kako bi kompenzirali ograničenja Linux kernela u slučajevima kada je potrebna obrada paketa visokih performansi. Jedna od najpopularnijih metoda ove vrste tzv core bypass (zaobilaženje kernela) i omogućava, preskačući mrežni sloj kernela, da izvrši svu obradu paketa iz korisničkog prostora. Zaobilaženje kernela također uključuje upravljanje mrežnom karticom iz korisnički prostor. Drugim riječima, kada radimo s mrežnom karticom, oslanjamo se na drajver korisnički prostor.

Prenošenjem potpune kontrole nad mrežnom karticom na program korisničkog prostora, smanjujemo troškove uzrokovane kernelom (kontekstualni prekidači, obrada mrežnog sloja, prekidi, itd.), što je vrlo važno kada se radi pri brzinama od 10Gb/s ili viši. Zaobilazeći kernel plus kombinaciju drugih karakteristika (serijska obrada) i pažljivo podešavanje performansi (NUMA računovodstvo, CPU izolacijaitd.) uklapaju se u osnove umrežavanja korisničkog prostora visokih performansi. Možda je primjer ovog novog pristupa obradi paketa DPDK od Intela (Komplet za razvoj ravni podataka), iako postoje i drugi dobro poznati alati i tehnike, uključujući VPP iz Cisco (vektorska obrada paketa), Netmap i, naravno, snab.

Organizacija mrežnih interakcija u korisničkom prostoru ima niz nedostataka:

  • OS kernel je sloj apstrakcije za hardverske resurse. Budući da programi korisničkog prostora moraju direktno upravljati svojim resursima, oni također moraju upravljati vlastitim hardverom. To često znači programiranje vlastitih drajvera.
  • Budući da se u potpunosti odričemo prostora kernela, također se odričemo svih mrežnih funkcionalnosti koje nudi kernel. Programi korisničkog prostora moraju ponovo implementirati karakteristike koje možda već nudi kernel ili operativni sistem.
  • Programi rade u sandbox modu, što ozbiljno ograničava njihovu interakciju i sprječava njihovu integraciju s drugim dijelovima operativnog sistema.

U suštini, kada se umrežava u korisničkom prostoru, poboljšanja performansi se postižu premeštanjem obrade paketa iz kernela u korisnički prostor. XDP radi upravo suprotno: premešta mrežne programe iz korisničkog prostora (filteri, pretvarači, rutiranje, itd.) u područje kernela. XDP nam omogućava da izvršimo mrežnu funkciju čim paket udari u mrežni interfejs i pre nego što počne da putuje do mrežnog podsistema kernela. Kao rezultat toga, brzina obrade paketa je značajno povećana. Međutim, kako kernel dozvoljava korisniku da izvodi svoje programe u prostoru kernela? Prije nego odgovorimo na ovo pitanje, pogledajmo šta je BPF.

BPF i eBPF

Uprkos ne sasvim jasnom nazivu, BPF (Packet Filtering, Berkeley) je, u stvari, model virtuelne mašine. Ova virtuelna mašina je prvobitno dizajnirana da rukuje filtriranjem paketa, otuda i ime.

Jedan od poznatijih alata koji koriste BPF je tcpdump. Prilikom hvatanja paketa sa tcpdump korisnik može specificirati izraz za filtriranje paketa. Biće uhvaćeni samo paketi koji odgovaraju ovom izrazu. Na primjer, izraz "tcp dst port 80” se odnosi na sve TCP pakete koji pristižu na port 80. Kompajler može skratiti ovaj izraz pretvarajući ga u BPF bajt kod.

$ 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

Ovo u osnovi radi gornji program:

  • Instrukcija (000): Učitava paket na ofsetu 12, kao 16-bitnu riječ, u akumulator. Pomak 12 odgovara etertipu paketa.
  • Instrukcija (001): upoređuje vrijednost u akumulatoru sa 0x86dd, odnosno sa vrijednošću ethertype za IPv6. Ako je rezultat tačan, programski brojač ide na instrukciju (002), a ako nije, onda na (006).
  • Instrukcija (006): uspoređuje vrijednost sa 0x800 (vrijednost tipa etera za IPv4). Ako je odgovor tačan, program ide na (007), ako nije, onda na (015).

I tako dalje, sve dok program za filtriranje paketa ne vrati rezultat. Obično je boolean. Vraćanje vrijednosti koja nije nula (instrukcija (014)) znači da se paket podudara, a vraćanje nule (instrukcija (015)) znači da se paket ne podudara.

BPF virtuelnu mašinu i njen bajt kod predložili su Steve McCann i Van Jacobson krajem 1992. godine kada je izašao njihov rad. BSD filter paketa: Nova arhitektura za hvatanje paketa na nivou korisnika, prvi put je ova tehnologija predstavljena na Usenix konferenciji u zimu 1993. godine.

Budući da je BPF virtuelna mašina, ona definiše okruženje u kojem se programi izvode. Osim bajtkoda, on također definira model memorije paketa (instrukcije za učitavanje se implicitno primjenjuju na paket), registre (A i X; akumulatorski i indeksni registri), skladištenje memorije i implicitni programski brojač. Zanimljivo je da je BPF bajt kod napravljen po uzoru na Motorola 6502 ISA. Kako se Steve McCann prisjetio u svojoj plenarni izvještaj na Sharkfestu '11, bio je upoznat sa build 6502 iz srednje škole kada je programirao na Apple II, i ovo znanje je uticalo na njegov rad na dizajniranju BPF bajtkoda.

BPF podrška je implementirana u jezgru Linuxa u verziji v2.5 i novijim, koju je uglavnom dodao Jay Schullist. BPF kod je ostao nepromijenjen do 2011. godine, kada je Eric Dumaset redizajnirao BPF interpreter za rad u JIT modu (Izvor: JIT za filtere paketa). Nakon toga, umjesto da tumači BPF bajtkod, kernel bi mogao direktno konvertirati BPF programe u ciljnu arhitekturu: x86, ARM, MIPS, itd.

Kasnije, 2014. godine, Aleksej Starovoitov je predložio novi JIT mehanizam za BPF. U stvari, ovaj novi JIT je postao nova arhitektura zasnovana na BPF-u i nazvana je eBPF. Mislim da su oba VM-a koegzistirala neko vrijeme, ali filtriranje paketa je trenutno implementirano na vrhu eBPF-a. Zapravo, u mnogim modernim primjerima dokumentacije, BPF se naziva eBPF, a klasični BPF je danas poznat kao cBPF.

eBPF proširuje klasičnu BPF virtuelnu mašinu na nekoliko načina:

  • Oslanja se na moderne 64-bitne arhitekture. eBPF koristi 64-bitne registre i povećava broj dostupnih registara sa 2 (akumulator i X) na 10. eBPF takođe pruža dodatne kodove operacija (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Odvojen od podsistema mrežnog sloja. BPF je bio vezan za model serije podataka. Pošto je korišćen za filtriranje paketa, njegov kod je bio u podsistemu koji je obezbeđivao mrežne interakcije. Međutim, eBPF virtuelna mašina više nije vezana za model podataka i može se koristiti u bilo koju svrhu. Dakle, sada se eBPF program može povezati na tracepoint ili kprobe. Ovo otvara vrata eBPF instrumentaciji, analizi performansi i mnogim drugim slučajevima upotrebe u kontekstu drugih podsistema kernela. Sada se eBPF kod nalazi na vlastitoj putanji: kernel/bpf.
  • Globalna skladišta podataka pod nazivom Mape. Mape su skladišta ključ/vrijednost koja obezbjeđuju razmjenu podataka između korisničkog prostora i prostora kernela. eBPF nudi nekoliko vrsta kartica.
  • Sekundarne funkcije. Konkretno, da biste prepisali paket, izračunajte kontrolni zbroj ili klonirajte paket. Ove funkcije rade unutar kernela i ne pripadaju programima korisničkog prostora. Osim toga, sistemski pozivi se mogu izvršiti iz eBPF programa.
  • Završi pozive. Veličina programa u eBPF-u ograničena je na 4096 bajtova. Funkcija završetka poziva omogućava eBPF programu da prenese kontrolu na novi eBPF program i tako zaobiđe ovo ograničenje (na ovaj način se mogu povezati do 32 programa).

eBPF primjer

Postoji nekoliko primjera za eBPF u izvorima Linux kernela. Dostupni su na samples/bpf/. Da kompajlirate ove primjere, samo upišite:

$ sudo make samples/bpf/

Neću sam pisati novi primjer za eBPF, već ću koristiti jedan od uzoraka dostupnih u samples/bpf/. Pogledat ću neke dijelove koda i objasniti kako funkcionira. Kao primjer, izabrao sam program tracex4.

Generalno, svaki od primjera u samples/bpf/ sastoji se od dvije datoteke. U ovom slučaju:

  • tracex4_kern.c, sadrži izvorni kod koji se izvršava u kernelu kao eBPF bajt kod.
  • tracex4_user.c, sadrži program iz korisničkog prostora.

U ovom slučaju, moramo kompajlirati tracex4_kern.c u eBPF bajt kod. Trenutno u gcc ne postoji serverski dio za eBPF. srećom, clang može proizvesti eBPF bajt kod. Makefile koristi clang kompajlirati tracex4_kern.c u objektnu datoteku.

Gore sam spomenuo da su mape jedna od najzanimljivijih karakteristika eBPF-a. tracex4_kern definira jednu mapu:

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 jedna od mnogih vrsta kartica koje nudi eBPF. U ovom slučaju, to je samo heš. Možda ste također primijetili oglas SEC("maps"). SEC je makro koji se koristi za kreiranje novog odjeljka binarne datoteke. Zapravo, u primjeru tracex4_kern definirana su još dva dijela:

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

Ove dvije funkcije vam omogućavaju da uklonite unos sa karte (kprobe/kmem_cache_free) i dodajte novi unos na kartu (kretprobe/kmem_cache_alloc_node). Sva imena funkcija napisana velikim slovima odgovaraju makroima definiranim u bpf_helpers.h.

Ako izbacim odjeljke objektne datoteke, trebao bih vidjeti da su ovi novi odjeljci već 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

Tu je i tracex4_user.c, glavni program. U osnovi, ovaj program osluškuje događaje kmem_cache_alloc_node. Kada dođe do takvog događaja, izvršava se odgovarajući eBPF kod. Kod sprema IP atribut objekta na mapu, a zatim se objekt provlači kroz glavni program. primjer:

$ 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 su povezani korisnički prostor program i eBPF program? Prilikom inicijalizacije tracex4_user.c učitava objektni fajl tracex4_kern.o koristeći funkciju 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;
}

Dok radite load_bpf_file dodaju se sonde definirane u eBPF datoteci /sys/kernel/debug/tracing/kprobe_events. Sada slušamo ove događaje i naš program može učiniti nešto kada se dogode.

$ 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

Svi ostali programi u sample/bpf/ su strukturirani slično. Uvek sadrže dva fajla:

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

Program eBPF definira mape i funkcije povezane s sekcijom. Kada kernel emituje događaj određenog tipa (npr. tracepoint), vezane funkcije se izvršavaju. Mape pružaju komunikaciju između programa kernela i programa korisničkog prostora.

zaključak

U ovom članku se o BPF-u i eBPF-u raspravljalo općenito. Znam da danas postoji mnogo informacija i resursa o eBPF-u, pa ću preporučiti još nekoliko materijala za dalje proučavanje.

Preporučujem da pročitate:

izvor: www.habr.com

Dodajte komentar