Stručný úvod do BPF a eBPF

Ahoj, Habr! Oznamujeme, že sa pripravuje na vydanie kniha "Linux Observability with BPF".

Stručný úvod do BPF a eBPF
Keďže virtuálny stroj BPF stále evolúuje a aktívne sa používa v praxi, preložili sme pre vás článok, ktorý popisuje jeho základné funkcie a aktuálny stav.

V posledných rokoch sa stali populárnymi nástroje na programovanie a techniky určené na kompenzáciu obmedzení jadra Linux v prípadoch, kde je potrebné vykonávať vysoko výkonnú spracovanie paketov. Jedna z najznámejších techník tohto druhu sa nazýva obídenie jadra (kernel bypass) a umožňuje, aby sa celá spracovanie paketov vykonávala z používateľského priestoru, pričom sa obchádza sieťová úroveň jadra. Obídenie jadra tiež predpokladá správu sieťovej karty z používateľského priestoru. Inými slovami, pri práci so sieťovou kartou sa spoliehame na ovládač používateľského priestoru.

Poskytujúci plnú kontrolu nad sieťovou kartou aplikácii z používateľského priestoru, znižujeme náklady spojené s prácou jadra (prepínanie kontextu, spracovanie sieťovej úrovne, prerušení atď.), čo je veľmi dôležité pri prevádzke na rýchlostiach 10Gb / s alebo vyšších. Obídenie jadra plus kombinácia iných funkcií (spracovanie paketov) a starostlivé ladění výkonu (zohľadnenie NUMA, izolácia CPU, atď.) zodpovedá základom vysoko výkonného spracovania sietí v používateľskom priestore. Možno ukážkovým príkladom takého nového prístupu k spracovaniu paketov je DPDK od Intelu (Data Plane Development Kit), hoci existujú aj iné známe nástroje a techniky, medzi ktorými sú VPP od Cisco (Vector Packet Processing), Netmap a samozrejme, Snabb.

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

  • Jadro OS je úroveň abstrakcie pre hardvérové zdroje. Keďže programy z používateľského priestoru musia priamo spravovať svoje zdroje, musia taktiež spravovať vlastný hardvér. To často znamená potrebu programovania vlastných ovládačov.
  • Keďže sa úplne vzdávame priestoru jadra, vzdávame sa aj všetkých sieťových funkcií, ktoré jadro poskytuje. Programy v používateľskom priestore musia znova implementovať tie funkcie, ktoré možno už poskytuje jadro alebo operačný systém.
  • Programy fungujú v pieskovisku, čo vážne obmedzuje možnosti ich interakcie a bráni im integrovať sa s inými časťami operačného systému.

V podstate, pri organizovaní sieťových interakcií v používateľskom priestore sa zvyšovanie výkonu dosahuje presunom spracovania paketov z jadra do používateľského priestoru. XDP robí presne naopak: presúva sieťové programy z používateľského priestoru (filtery, transformátory, smerovanie atď.) do oblasti jadra. XDP nám umožňuje vykonať sieťovú funkciu hneď, ako paket dorazí na sieťové rozhranie a skôr, než začne stúpať do sieťového subsystému jadra. V dôsledku toho sa rýchlosť spracovania paketov podstatne zvyšuje. Avšak, ako umožňuje jadro používateľovi spustiť jeho programy v priestore jadra? Predtým, než na túto otázku odpovieme, pozrime sa, čo je BPF.

BPF a eBPF

Napriek nie úplne zrozumiteľnému názvu BPF (Berkeley Packet Filter) – ide v zásade o model virtuálneho stroja. Tento virtuálny stroj bol pôvodne navrhnutý na spracovanie filtrovania paketov, odtiaľ aj názov.

Jedným z najznámejších nástrojov využívajúcich BPF je tcpdump. Pri zachytávaní paketov pomocou tcpdump môže používateľ zadať výraz na filtrovanie paketov. Zachytávané budú iba tie pakety, ktoré zodpovedajú tomuto výrazu. Napríklad, výraz "tcp dst port 80" sa týka všetkých TCP paketov prichádzajúcich na port 80. Kompilátor môže tento výraz skrátiť a pretransformovať ho na BPF bajtový kód.

$ 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

Toto je v podstate to, čo robí vyššie uvedený program:

  • Inštrukcia (000): načíta paket s posunom 12, vo forme 16-bitového slova do akumulátora. Posun 12 zodpovedá ethertype paketu.
  • Inštrukcia (001): porovnáva hodnotu v akumulátore s 0x86dd, teda s ethertype hodnotou pre IPv6. Ak je výsledok true, programový počítadlo prechádza na inštrukciu (002), ak nie – tak na (006).
  • Inštrukcia (006): porovnáva hodnotu s 0x800 (ethertype-hodnota pre IPv4). Ak je odpoveď true, program prejde na (007), inak na (015).

A tak ďalej, kým program na filtrovanie pakiet nevráti výsledok. Zvyčajne ide o boolean. Návrat nenulovej hodnoty (inštrukcia (014)) znamená, že paket sa hodí, zatiaľ čo návrat nulovej hodnoty (inštrukcia (015)) znamená, že paket sa nehodí.

Virtuálny stroj BPF a jeho bajtkód boli navrhnuté Steveom McCanom a Vanom Jacobsonom na konci roku 1992, keď vyšla ich práca. Filter paketov BSD: Nová architektúra pre zachytávanie paketov na úrovni používateľov., prvýkrát bola táto technológia predstavená na konferencii Usenix v zime 1993.

Keďže BPF je virtuálny stroj, definuje prostredie, v ktorom sa programy vykonávajú. Okrem bajtkódu tiež definuje paketový model pamäte (pokyny načítania sa implicitne aplikujú na paket), registre (A a X; registre akumulátora a indexu), úložisko skratch-pamäte a implicitný programový čítač. Zaujímavé je, že bajtkód BPF bol modelovaný podľa jasného modelu Motorola 6502. Ako si pamätal Steve McCann na svojom hlavnom prednáške na Sharkfest ‘11, bol oboznámený so zostavou 6502 už na strednej škole, keď programoval na Apple II, a tieto znalosti ovplyvnili jeho prácu na návrhu bajtkódu BPF.

Podpora BPF bola implementovaná v jadre Linuxu vo verzii v2.5 a vyššie, pridala ju najmä snaha Jaya Shulista. Kód BPF zostal bez vážnych zmien až do roku 2011, keď Eric Dumazet prepracoval interpretátor BPF na prácu v režime JIT (Zdroj: JIT pre filtre paketov). Po tomto jadro namiesto interpretácie bajtkódu BPF mohlo priamo transformovať programy BPF na cieľovú architektúru: x86, ARM, MIPS a pod.

Neskôr, v roku 2014, Alexej Starovoitov navrhol nový JIT mechanizmus pre BPF. V skutočnosti tento nový JIT sa stal novou architektúrou založenou na BPF a nazýva sa eBPF. Myslím, že nejaký čas existovali obe virtuálne stroje súčasne, ale v súčasnosti sa filtrovanie paketov implementuje na základe eBPF. V skutočnosti v mnohých moderných dokumentoch sa pod BPF rozumie eBPF, zatiaľ čo klasický BPF je dnes známy ako cBPF.

eBPF v niekoľkých ohľadoch rozširuje klasický virtuálny stroj BPF:

  • Opiera sa na moderných 64-bitových architektúrach. eBPF využíva 64-bitové registry a zvyšuje počet dostupných registrov z 2 (akumulátor a X) na 10. eBPF tiež ponúka ďalšie operácie (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Uvoľnená od pod systému sieťovej úrovne. BPF bola viazaná na paketový model dát. Keďže sa používala na filtrovanie paketov, jej kód bol v pod systéme zaisťujúcom sieťové interakcie. Avšak, virtuálna mašina eBPF už nie je viazaná na model dát a môže sa používať na akékoľvek účely. Takže, teraz je program eBPF možné pripojiť k tracepointu alebo k kprobe. To otvára cestu k instrumentácii eBPF, analýze výkonu a mnohým ďalším možnostiam využitia v kontexte iných pod systémov jadra. Teraz je kód eBPF umiestnený na vlastnej ceste: kernel/bpf.
  • Globálne úložiská dát nazývané Mapy. Mapy sú úložiská typu „kľúč-hodnota“, ktoré zabezpečujú výmenu dát medzi používateľským priestorom a priestorom jadra. V eBPF sú k dispozícii mapy viacerých typov.
  • Pomocné funkcie. Najmä na prepisovanie paketu, výpočet kontrolného súčtu alebo klonovanie paketu. Tieto funkcie sa vykonávajú v jadre a netýkajú sa programov používateľského priestoru. Okrem toho z programov eBPF možno vykonávať systémové volania.
  • Koncové volania. Veľkosť programu v eBPF je obmedzená na 4096 bajtov. Možnosť koncového volania umožňuje programu eBPF odovzdať riadenie novej eBPF-programu a tak obísť toto obmedzenie (týmto spôsobom je možné spájať až 32 programov).

eBPF: príklad

V zdrojových kódoch jadra Linux je niekoľko príkladov pre eBPF. Sú dostupné na adrese samples/bpf/. Aby ste skompilovali tieto príklady, jednoducho zadajte:

$ sudo make samples/bpf/

Nebudem písať nový príklad pre eBPF, ale využijem jeden z ukážok dostupných v samples/bpf/. Preskúmam niektoré časti kódu a vysvetlím, ako funguje. Ako príklad som si vybral program tracex4.

V skutočnosti sa každý z príkladov v samples/bpf/ skladá z dvoch súborov. V tomto prípade:

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

V takom prípade musíme skompilovať tracex4_kern.c do bytecode eBPF. Currently, there is gcc no server part for eBPF. Fortunately, clang it can output eBPF bytecode. Makefile používa clang for compilation tracex4_kern.c into an object file.

As mentioned above, one of the most interesting features of eBPF are maps. tracex4_kern defines one map:

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 – one of the many types of maps offered by eBPF. In this case, it is just a hash. You might also have noticed the declaration SEC("maps"). SEC is a macro used to create a new section of the binary file. In fact, in the example tracex4_kern two more sections are defined:

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;

    // getting the IP address of the caller of 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;
}   

These two functions allow to remove an entry from the map (kprobe/kmem_cache_free) and to add a new entry to the map (kretprobe/kmem_cache_alloc_node). All function names written in uppercase correspond to macros defined in bpf_helpers.h.

If I output a dump of the sections of the object file, I should see that these new sections are already defined:

$ 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

Ešte je tu tracex4_user.c, the main program. In essence, this program listens for events kmem_cache_alloc_node. When such an event occurs, the corresponding eBPF code is executed. The code saves the IP attribute of the object in the map, and then this object is cyclically output in the main program. Example:

$ sudo ./tracex4
obj 0xffff8d6430f60a00 je 2 sekundy starý, bol pridelený na ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 je 23 sekúnd starý, bol pridelený na ip ffffffff98090e8f
obj 0xffff8d5f80161780 je 6 sekúnd starý, bol pridelený na ip ffffffff98090e8f

How are the user space program and the eBPF program related? During initialization, tracex4_user.c loads the object file tracex4_kern.o pomocou funkcie načítať_bpf_súbor.

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

Pri vykonávaní načítať_bpf_súbor sondy, definované v súbore eBPF, sa pridávajú do /sys/kernel/debug/tracing/kprobe_events. Teraz počúvame tieto udalosti a náš program môže niečo robiť, keď nastanú.

$ 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 v nich sú dve súbory:

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

Program eBPF definuje mapy a funkcie, ktoré sú priradené k sekcii. Keď jadro generuje udalosť určitého typu (napríklad, tracepoint), priradené funkcie sa vykonávajú. Mapy zabezpečujú výmenu dát medzi programom jadra a programom používateľského priestoru.

Záver

V tomto článku sme v hrubých obrysoch preskúmali BPF a eBPF. Viem, že dnes existuje veľa informácií a zdrojov o eBPF, preto odporúčam niekoľko materiálov na ďalšie štúdium

Odporúčam si prečítať:

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster