Stručný úvod do BPF a eBPF

Ahoj Habr! Radi by sme vás informovali, že pripravujeme vydanie knihy.“Pozorovateľnosť Linuxu s BPF".

Stručný úvod do BPF a eBPF
Keďže sa virtuálny stroj BPF neustále vyvíja a je aktívne využívaný v praxi, preložili sme pre vás článok popisujúci jeho hlavné možnosti a aktuálny stav.

V posledných rokoch sa programovacie nástroje a techniky stávajú čoraz obľúbenejšími na kompenzáciu obmedzení linuxového jadra v prípadoch, keď sa vyžaduje vysokovýkonné spracovanie paketov. Jedna z najpopulárnejších techník tohto druhu je tzv bypass jadra (obídenie jadra) a umožňuje, obídením sieťovej vrstvy jadra, vykonávať všetko spracovanie paketov z užívateľského priestoru. Obídenie jadra zahŕňa aj ovládanie sieťovej karty z užívateľský priestor. Inými slovami, pri práci so sieťovou kartou sa spoliehame na ovládač užívateľský priestor.

Prenesením plnej kontroly nad sieťovou kartou na program v užívateľskom priestore znižujeme réžiu jadra (prepínanie kontextu, spracovanie sieťovej vrstvy, prerušenia atď.), čo je dosť dôležité pri rýchlostiach 10Gb/s alebo vyšších. Obídenie jadra plus kombinácia ďalších funkcií (dávkové spracovanie) a starostlivé ladenie výkonu (účtovníctvo NUMA, izolácia CPU, atď.) zodpovedajú základom vysokovýkonného sieťového spracovania v užívateľskom priestore. Možno je exemplárnym príkladom tohto nového prístupu k spracovaniu paketov DPDK od Intelu (Súprava na vývoj dátovej roviny), aj keď existujú aj iné dobre známe nástroje a techniky, vrátane Cisco VPP (Vector Packet Processing), Netmap a, samozrejme, Snabb.

Organizácia sieťových interakcií v užívateľskom priestore má niekoľko nevýhod:

  • Jadro OS je abstraktná vrstva pre hardvérové ​​prostriedky. Pretože programy používateľského priestoru musia spravovať svoje zdroje priamo, musia tiež spravovať svoj vlastný hardvér. To často znamená, že si musíte naprogramovať vlastné ovládače.
  • Pretože sa úplne vzdávame priestoru jadra, vzdávame sa aj všetkých sieťových funkcií, ktoré jadro poskytuje. Programy v užívateľskom priestore musia znovu implementovať funkcie, ktoré už môže poskytovať jadro alebo operačný systém.
  • Programy fungujú v režime sandbox, čo vážne obmedzuje ich interakciu a bráni im v integrácii s inými časťami operačného systému.

V podstate, keď sa sieťovanie vyskytuje v užívateľskom priestore, zvýšenie výkonu sa dosiahne presunom spracovania paketov z jadra do užívateľského priestoru. XDP robí presný opak: presúva sieťové programy z používateľského priestoru (filtre, resolvery, smerovanie atď.) do priestoru jadra. XDP nám umožňuje vykonávať sieťovú funkciu hneď, ako paket zasiahne sieťové rozhranie a predtým, ako sa začne presúvať do podsystému siete jadra. V dôsledku toho sa rýchlosť spracovania paketov výrazne zvyšuje. Ako však jadro umožňuje používateľovi spúšťať svoje programy v priestore jadra? Pred zodpovedaním tejto otázky sa pozrime na to, čo je BPF.

BPF a eBPF

Napriek nie celkom jasnému názvu je BPF (Berkeley Packet Filtering) v skutočnosti modelom virtuálneho stroja. Tento virtuálny stroj bol pôvodne navrhnutý tak, aby zvládal filtrovanie paketov, odtiaľ názov.

Jedným z najznámejších nástrojov využívajúcich BPF je tcpdump. Pri zachytávaní paketov pomocou tcpdump užívateľ môže zadať výraz na filtrovanie paketov. Zachytia sa iba pakety zodpovedajúce tomuto výrazu. Napríklad výraz „tcp dst port 80” označuje všetky TCP pakety prichádzajúce na port 80. Kompilátor môže tento výraz skrátiť jeho konverziou na bajtový kód 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 to, čo vyššie uvedený program v podstate robí:

  • Inštrukcia (000): Načíta paket s ofsetom 12 ako 16-bitové slovo do akumulátora. Offset 12 zodpovedá éterovému typu paketu.
  • Inštrukcia (001): porovnáva hodnotu v akumulátore s 0x86dd, teda s hodnotou ethertype pre IPv6. Ak je výsledok pravdivý, potom programové počítadlo prejde na inštrukciu (002), a ak nie, potom na (006).
  • Inštrukcia (006): porovnáva hodnotu s 0x800 (hodnota ethertype pre IPv4). Ak je odpoveď pravdivá, program prejde na (007), ak nie, potom na (015).

A tak ďalej, kým program na filtrovanie paketov nevráti výsledok. Toto je zvyčajne boolovská hodnota. Vrátenie nenulovej hodnoty (inštrukcia (014)) znamená, že paket bol prijatý, a vrátenie nulovej hodnoty (inštrukcia (015)) znamená, že paket nebol prijatý.

Virtuálny stroj BPF a jeho bajtový kód navrhli Steve McCann a Van Jacobson koncom roku 1992, keď bol publikovaný ich článok. Filter paketov BSD: Nová architektúra pre zachytávanie paketov na užívateľskej úrovni, táto technológia bola prvýkrát predstavená na konferencii Usenix v zime 1993.

Pretože BPF je virtuálny stroj, definuje prostredie, v ktorom programy bežia. Okrem bajtkódu definuje aj model dávkovej pamäte (inštrukcie načítania sú implicitne aplikované na dávku), registre (A a X; akumulátorové a indexové registre), úložisko stieracej pamäte a implicitné počítadlo programu. Zaujímavé je, že bajtový kód BPF bol modelovaný podľa Motorola 6502 ISA. Ako pripomenul Steve McCann vo svojom správa z pléna na Sharkfest '11 bol oboznámený so zostavou 6502 z programovania na strednej škole na Apple II a tieto znalosti ovplyvnili jeho prácu pri navrhovaní bajtkódu BPF.

Podpora BPF je implementovaná v linuxovom jadre vo verziách v2.5 a vyšších, pridaná najmä snahou Jaya Schullista. Kód BPF zostal nezmenený až do roku 2011, keď Eric Dumaset prepracoval prekladač BPF tak, aby fungoval v režime JIT (Zdroj: JIT pre paketové filtre). Potom by jadro namiesto interpretácie bajtkódu BPF mohlo priamo konvertovať programy BPF na cieľovú architektúru: x86, ARM, MIPS atď.

Neskôr, v roku 2014, Alexey Starovoitov navrhol nový mechanizmus JIT pre BPF. V skutočnosti sa tento nový JIT stal novou architektúrou založenou na BPF a nazýval sa eBPF. Myslím, že oba VM existovali nejaký čas vedľa seba, ale v súčasnosti je filtrovanie paketov implementované na základe eBPF. V skutočnosti sa v mnohých príkladoch modernej dokumentácie BPF chápe ako eBPF a klasický BPF je dnes známy ako cBPF.

eBPF rozširuje klasický virtuálny stroj BPF niekoľkými spôsobmi:

  • Založené na moderných 64-bitových architektúrach. eBPF používa 64-bitové registre a zvyšuje počet dostupných registrov z 2 (akumulátor a X) na 10. eBPF poskytuje aj ďalšie operačné kódy (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Oddelené od subsystému sieťovej vrstvy. BPF bol prepojený s dávkovým dátovým modelom. Keďže sa používal na filtrovanie paketov, jeho kód bol umiestnený v podsystéme, ktorý zabezpečuje sieťovú komunikáciu. Virtuálny stroj eBPF však už nie je viazaný na dátový model a môže byť použitý na akýkoľvek účel. Takže teraz môže byť program eBPF pripojený k sledovaciemu bodu alebo kprobe. To otvára cestu k inštrumentácii eBPF, analýze výkonu a mnohým ďalším prípadom použitia v kontexte iných subsystémov jadra. Teraz je kód eBPF umiestnený vo svojej vlastnej ceste: kernel/bpf.
  • Globálne dátové úložiská nazývané Mapy. Mapy sú úložiská kľúč-hodnota, ktoré umožňujú výmenu údajov medzi užívateľským priestorom a priestorom jadra. eBPF poskytuje niekoľko typov máp.
  • Sekundárne funkcie. Najmä na prepísanie balíka, výpočet kontrolného súčtu alebo klonovanie balíka. Tieto funkcie bežia v jadre a nie sú programami v užívateľskom priestore. Systémové volania môžete uskutočniť aj z programov eBPF.
  • Ukončiť hovory. Veľkosť programu v eBPF je obmedzená na 4096 bajtov. Funkcia tail call umožňuje programu eBPF preniesť riadenie na nový program eBPF a obísť tak toto obmedzenie (takto je možné prepojiť až 32 programov).

eBPF: príklad

V zdrojoch jadra Linuxu je niekoľko príkladov pre eBPF. Sú dostupné na sample/bpf/. Ak chcete zostaviť tieto príklady, jednoducho zadajte:

$ sudo make samples/bpf/

Nebudem písať nový príklad pre eBPF sám, ale použijem jednu zo vzoriek dostupných v sample/bpf/. Pozriem sa na niektoré časti kódu a vysvetlím, ako to funguje. Ako príklad som si vybral program tracex4.

Vo všeobecnosti každý z príkladov v sample/bpf/ pozostáva z dvoch súborov. V tomto prípade:

  • tracex4_kern.c, obsahuje zdrojový kód, ktorý sa má spustiť v jadre ako bajtový kód eBPF.
  • tracex4_user.c, obsahuje program z užívateľského priestoru.

V tomto prípade musíme skompilovať tracex4_kern.c na bajtový kód eBPF. Aktuálne v gcc neexistuje žiadny backend pre eBPF. Našťastie clang môže vydávať bajtový kód eBPF. Makefile používa clang na zostavenie tracex4_kern.c do súboru objektu.

Vyššie som spomenul, že jednou z najzaujímavejších funkcií eBPF sú mapy. tracex4_kern definuje 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 jedným z mnohých typov kariet, ktoré eBPF ponúka. V tomto prípade ide len o hash. Možno ste si všimli aj reklamu SEC("maps"). SEC je makro používané na vytvorenie novej sekcie binárneho súboru. Vlastne v príklade tracex4_kern sú definované ďalšie dve sekcie:

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

Tieto dve funkcie vám umožňujú vymazať záznam z mapy (kprobe/kmem_cache_free) a pridajte nový záznam na mapu (kretprobe/kmem_cache_alloc_node). Všetky názvy funkcií napísané veľkými písmenami zodpovedajú makrám definovaným v bpf_helpers.h.

Ak vypíšem sekcie objektového súboru, mal by som vidieť, že tieto nové sekcie sú už definované:

$ 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 tu tiež tracex4_user.c, hlavný program. V podstate tento program počúva udalosti kmem_cache_alloc_node. Keď k takejto udalosti dôjde, vykoná sa zodpovedajúci kód eBPF. Kód uloží atribút IP objektu do mapy a objekt potom prechádza cez hlavný program. Príklad:

$ 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

Ako spolu súvisia program používateľského priestoru a program eBPF? Pri inicializácii tracex4_user.c načíta súbor objektu tracex4_kern.o pomocou funkcie 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;
}

Počas toho load_bpf_file pridajú sa sondy definované v súbore eBPF /sys/kernel/debug/tracing/kprobe_events. Teraz počúvame tieto udalosti a náš program môže niečo urobiť, keď sa stanú.

$ 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

Všetky ostatné programy v sample/bpf/ sú štruktúrované podobne. Vždy obsahujú dva súbory:

  • XXX_kern.c: program eBPF.
  • XXX_user.c: hlavný program.

Program eBPF identifikuje mapy a funkcie spojené s úsekom. Keď jadro vydá udalosť určitého typu (napr. tracepoint), vykonajú sa viazané funkcie. Karty zabezpečujú komunikáciu medzi programom jadra a programom užívateľského priestoru.

Záver

Tento článok diskutoval o BPF a eBPF vo všeobecnosti. Viem, že o eBPF je dnes veľa informácií a zdrojov, preto vám odporučím niekoľko ďalších zdrojov na ďalšie štúdium

Odporúčam prečítať:

Zdroj: hab.com

Pridať komentár