Kratki uvod u BPF i eBPF

Hej Habr! Obavještavamo Vas da pripremamo izdavanje knjige "Linux vidljivost s BPF-om".

Kratki uvod u BPF i eBPF
Kako se BPF virtualni stroj nastavlja razvijati i aktivno koristi u praksi, za vas smo preveli članak koji opisuje njegove glavne značajke i trenutno stanje.

Posljednjih godina programski alati i tehnike stekli su popularnost kako bi nadoknadili ograničenja Linux kernela u slučajevima kada je potrebna obrada paketa visokih performansi. Jedna od najpopularnijih metoda ove vrste je tzv premosnica jezgre (kernel bypass) i omogućuje, preskačući mrežni sloj kernela, obavljanje svih obrada 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 upravljački program korisnički prostor.

Prijenosom potpune kontrole nad mrežnom karticom na program korisničkog prostora, smanjujemo opterećenje uzrokovano kernelom (kontekstne sklopke, obrada mrežnog sloja, prekidi itd.), što je vrlo važno kada se radi na brzinama od 10Gb/s ili viši. Zaobilaženje kernela plus kombinacija drugih značajki (skupna obrada) i pažljivo podešavanje performansi (NUMA računovodstvo, CPU izolacija, itd.) odgovaraju osnovama visokoučinkovitog umrežavanja korisničkog prostora. Možda je primjer ovog novog pristupa obradi paketa DPDK od Intela (Data Plane Development Kit), iako postoje i drugi dobro poznati alati i tehnike, uključujući VPP tvrtke Cisco (Vector Packet Processing), Netmap i, naravno, snab.

Organizacija mrežnih interakcija u korisničkom prostoru ima brojne nedostatke:

  • Jezgra OS-a je sloj apstrakcije za hardverske resurse. Budući da programi korisničkog prostora moraju izravno upravljati svojim resursima, moraju upravljati i vlastitim hardverom. To često znači programiranje vlastitih upravljačkih programa.
  • Budući da se potpuno odričemo prostora kernela, također se odričemo svih mrežnih funkcija koje pruža kernel. Programi korisničkog prostora moraju ponovno implementirati značajke koje možda već pružaju kernel ili operativni sustav.
  • Programi rade u modu sandboxa, što ozbiljno ograničava njihovu interakciju i sprječava njihovu integraciju s drugim dijelovima operativnog sustava.

U biti, pri umrežavanju u korisničkom prostoru, dobici performansi se postižu premještanjem obrade paketa iz kernela u korisnički prostor. XDP radi upravo suprotno: premješta mrežne programe iz korisničkog prostora (filtri, pretvarači, usmjeravanje itd.) u područje jezgre. XDP nam omogućuje da izvršimo mrežnu funkciju čim paket pogodi mrežno sučelje i prije nego što počne putovati do mrežnog podsustava jezgre. Kao rezultat, brzina obrade paketa je značajno povećana. Međutim, kako kernel dopušta korisniku pokretanje svojih programa u kernel prostoru? Prije nego odgovorimo na ovo pitanje, pogledajmo što je BPF.

BPF i eBPF

Unatoč ne sasvim jasnom nazivu, BPF (Packet Filtering, Berkeley) je zapravo model virtualnog stroja. Ovo virtualno računalo izvorno je dizajnirano za upravljanje filtriranjem paketa, otuda i naziv.

Jedan od poznatijih alata koji koristi BPF je tcpdump. Prilikom hvatanja paketa sa tcpdump korisnik može odrediti izraz za filtriranje paketa. Bit će snimljeni samo paketi koji odgovaraju ovom izrazu. Na primjer, izraz "tcp dst port 80” odnosi se na sve TCP pakete koji stižu na port 80. Prevodilac 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 je u osnovi ono što gornji program radi:

  • Uputa (000): učitava paket na pomaku 12, kao 16-bitnu riječ, u akumulator. Offset 12 odgovara tipu ether paketa.
  • Uputa (001): uspoređuje vrijednost u akumulatoru s 0x86dd, odnosno s vrijednošću ethertype za IPv6. Ako je rezultat točan, onda programski brojač ide na instrukciju (002), a ako nije, onda na (006).
  • Uputa (006): uspoređuje vrijednost s 0x800 (vrijednost ethertype za IPv4). Ako je odgovor točan, tada program ide na (007), ako nije, onda na (015).

I tako dalje, dok program za filtriranje paketa ne vrati rezultat. Obično je Boolean. Vraćanje vrijednosti različite od nule (uputa (014)) znači da se paket podudara, a vraćanje nule (uputa (015)) znači da se paket ne podudara.

BPF virtualni stroj i njegov bajt kod predložili su Steve McCann i Van Jacobson krajem 1992. kada je izašao njihov rad. BSD filter paketa: nova arhitektura za hvatanje paketa na razini korisnika, prvi put je ova tehnologija predstavljena na Usenix konferenciji u zimu 1993. godine.

Budući da je BPF virtualni stroj, on definira okruženje u kojem se programi izvode. Osim bajt-koda, također definira model memorije paketa (instrukcije učitavanja implicitno se primjenjuju na paket), registre (A i X; registri akumulatora i indeksa), pohranu scratch memorije i implicitni programski brojač. Zanimljivo je da je BPF bajt kod napravljen po uzoru na Motorola 6502 ISA. Kao što se Steve McCann prisjetio u svojoj plenarno izvješće na Sharkfestu '11, bio je upoznat s verzijom 6502 iz srednje škole dok je programirao na Appleu II, a to je znanje utjecalo na njegov rad na dizajniranju BPF bajt koda.

Podrška za BPF implementirana je u Linux kernelu u verziji v2.5 i novijoj, koju je uglavnom dodao Jay Schullist. BPF kod ostao je nepromijenjen do 2011., kada je Eric Dumaset redizajnirao BPF interpreter da radi u JIT modu (Izvor: JIT za filtere paketa). Nakon toga, umjesto tumačenja BPF bajt koda, kernel je mogao izravno pretvoriti BPF programe u ciljnu arhitekturu: x86, ARM, MIPS, itd.

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

eBPF proširuje klasični BPF virtualni stroj na nekoliko načina:

  • Oslanja se na moderne 64-bitne arhitekture. eBPF koristi 64-bitne registre i povećava broj dostupnih registara s 2 (akumulator i X) na 10. eBPF također nudi dodatne operativne kodove (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Odvojen od podsustava mrežnog sloja. BPF je bio vezan za batch model podataka. Budući da se koristio za filtriranje paketa, njegov se kod nalazio u podsustavu koji je osiguravao mrežne interakcije. Međutim, eBPF virtualni stroj više nije vezan za podatkovni model i može se koristiti u bilo koju svrhu. Dakle, sada se eBPF program može spojiti na tracepoint ili kprobe. Ovo otvara vrata eBPF instrumentaciji, analizi performansi i mnogim drugim slučajevima upotrebe u kontekstu drugih podsustava jezgre. Sada se eBPF kod nalazi na vlastitoj stazi: kernel/bpf.
  • Globalna spremišta podataka koja se nazivaju Karte. Karte su pohrane ključeva i vrijednosti koje omogućuju razmjenu podataka između korisničkog prostora i prostora jezgre. eBPF nudi nekoliko vrsta kartica.
  • Sekundarne funkcije. Konkretno, za prepisivanje paketa, izračunavanje kontrolne sume ili kloniranje paketa. Ove se funkcije izvode unutar kernela i ne pripadaju programima korisničkog prostora. Osim toga, sistemski pozivi mogu se napraviti iz eBPF programa.
  • Završi pozive. Veličina programa u eBPF-u ograničena je na 4096 bajtova. Značajka završetka poziva omogućuje eBPF programu da prenese kontrolu na novi eBPF program i tako zaobiđe ovo ograničenje (do 32 programa mogu biti ulančana na ovaj način).

primjer eBPF-a

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

$ sudo make samples/bpf/

Neću sam napisati 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 odabrao sam program tracex4.

Općenito, 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 sastaviti tracex4_kern.c objektnoj datoteci.

Gore sam spomenuo da su jedna od najzanimljivijih značajki eBPF-a karte. 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 jedna je od mnogih vrsta kartica koje nudi eBPF. U ovom slučaju, to je samo hash. Možda ste i vi primijetili oglas SEC("maps"). SEC je makronaredba koja se koristi za stvaranje novog odjeljka binarne datoteke. Zapravo, u primjeru tracex4_kern definirana su još dva odjeljka:

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 omogućuju vam uklanjanje unosa s karte (kprobe/kmem_cache_free) i dodajte novi unos na kartu (kretprobe/kmem_cache_alloc_node). Svi nazivi funkcija napisani velikim slovima odgovaraju makronaredbama definiranim u bpf_helpers.h.

Ako izbacim odjeljke objektne datoteke, trebao bih vidjeti da su ti 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

Postoji također tracex4_user.c, glavni program. U osnovi, ovaj program osluškuje događaje kmem_cache_alloc_node. Kada se dogodi takav događaj, izvršava se odgovarajući eBPF kod. Kod sprema IP atribut objekta na kartu, a zatim se objekt ponavlja 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 program korisničkog prostora i program eBPF povezani? Kod inicijalizacije tracex4_user.c učitava objektnu datoteku 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 radiš load_bpf_file dodaju se sonde definirane u eBPF datoteci /sys/kernel/debug/tracing/kprobe_events. Sada slušamo te 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/ strukturirani su na sličan način. Uvijek sadrže dvije datoteke:

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

Program eBPF definira karte i funkcije povezane s dionicom. Kada kernel emitira događaj određene vrste (npr. tracepoint), izvršavaju se vezane funkcije. Mape omogućuju komunikaciju između programa jezgre i programa korisničkog prostora.

Zaključak

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

Preporučujem čitanje:

Izvor: www.habr.com

Dodajte komentar