Stručný úvod do BPF a eBPF

Čau Habr! Informujeme vás, že připravujeme vydání knihy "Pozorovatelnost Linuxu s BPF".

Stručný úvod do BPF a eBPF
Protože se virtuální stroj BPF neustále vyvíjí a je aktivně využíván v praxi, přeložili jsme pro vás článek popisující jeho hlavní vlastnosti a aktuální stav.

V posledních letech si získaly oblibu programovací nástroje a techniky, které kompenzují omezení linuxového jádra v případech, kdy je vyžadováno vysoce výkonné zpracování paketů. Jedna z nejpopulárnějších metod tohoto druhu je tzv jádrový bypass (kernel bypass) a umožňuje, vynecháním síťové vrstvy jádra, provádět veškeré zpracování paketů z uživatelského prostoru. Obejití jádra také zahrnuje správu síťové karty z uživatelský prostor. Jinými slovy, při práci se síťovou kartou jsme odkázáni na ovladač uživatelský prostor.

Přenesením plné kontroly nad síťovou kartou na program v uživatelském prostoru snížíme režii způsobenou jádrem (kontextové přepínače, zpracování síťové vrstvy, přerušení atd.), což je poměrně důležité při běhu rychlostí 10Gb/s resp. vyšší. Vynechání jádra a kombinace dalších funkcí (dávkové zpracování) a pečlivé ladění výkonu (NUMA účetnictví, izolace CPUatd.) odpovídají základům vysoce výkonných sítí v uživatelském prostoru. Možná příkladným příkladem tohoto nového přístupu ke zpracování paketů je DPDK od společnosti Intel (Sada pro vývoj datové roviny), i když existují další známé nástroje a techniky, včetně VPP od společnosti Cisco (Vector Packet Processing), Netmap a samozřejmě, utrhnout.

Organizace síťových interakcí v uživatelském prostoru má řadu nevýhod:

  • Jádro OS je abstraktní vrstva pro hardwarové prostředky. Protože programy v uživatelském prostoru musí spravovat své zdroje přímo, musí také spravovat svůj vlastní hardware. To často znamená naprogramovat si vlastní ovladače.
  • Protože se zcela vzdáváme prostoru jádra, vzdáváme se také všech síťových funkcí, které jádro poskytuje. Programy v uživatelském prostoru musí znovu implementovat funkce, které již mohou být poskytovány jádrem nebo operačním systémem.
  • Programy pracují v režimu sandbox, což vážně omezuje jejich interakci a brání jim v integraci s jinými částmi operačního systému.

V podstatě se při vytváření sítí v uživatelském prostoru dosahuje zvýšení výkonu přesunem zpracování paketů z jádra do uživatelského prostoru. XDP dělá pravý opak: přesouvá síťové programy z uživatelského prostoru (filtry, konvertory, směrování atd.) do oblasti jádra. XDP nám umožňuje provést síťovou funkci, jakmile paket narazí na síťové rozhraní a než začne cestovat do síťového subsystému jádra. V důsledku toho se výrazně zvýší rychlost zpracování paketů. Jak však jádro umožňuje uživateli spouštět své programy v prostoru jádra? Než odpovíme na tuto otázku, podívejme se, co je to BPF.

BPF a eBPF

Navzdory ne zcela jasnému názvu je BPF (Packet Filtering, Berkeley) ve skutečnosti modelem virtuálního stroje. Tento virtuální stroj byl původně navržen pro filtrování paketů, odtud název.

Jedním z nejznámějších nástrojů využívajících BPF je tcpdump. Při zachytávání paketů pomocí tcpdump uživatel může zadat výraz pro filtrování paketů. Budou zachyceny pouze pakety, které odpovídají tomuto výrazu. Například výraz "tcp dst port 80” odkazuje na všechny TCP pakety přicházející na port 80. Kompilátor může tento výraz zkrátit převedením 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 v podstatě to, co výše uvedený program dělá:

  • Instrukce (000): Načte paket s offsetem 12 jako 16bitové slovo do akumulátoru. Offset 12 odpovídá ethertypu paketu.
  • Instrukce (001): porovnává hodnotu v akumulátoru s 0x86dd, tedy s hodnotou ethertype pro IPv6. Pokud je výsledek pravdivý, pak programový čítač přejde na instrukci (002), a pokud ne, pak na (006).
  • Instrukce (006): porovnává hodnotu s 0x800 (hodnota ethertype pro IPv4). Pokud je odpověď pravdivá, program přejde na (007), pokud ne, pak na (015).

A tak dále, dokud program pro filtrování paketů nevrátí výsledek. Obvykle je to booleovský. Vrácení nenulové hodnoty (instrukce (014)) znamená, že se paket shodoval, a vrácení nuly (instrukce (015)) znamená, že se paket neshodoval.

Virtuální stroj BPF a jeho bajtový kód navrhli Steve McCann a Van Jacobson koncem roku 1992, když vyšel jejich článek. BSD Packet Filter: Nová architektura pro zachycování paketů na uživatelské úrovni, tato technologie byla poprvé představena na konferenci Usenix v zimě 1993.

Protože BPF je virtuální stroj, definuje prostředí, ve kterém programy běží. Kromě bajtkódu definuje také paketový paměťový model (instrukce pro načítání jsou implicitně aplikovány na paket), registry (A a X; akumulátorové a indexové registry), úložiště odkládací paměti a implicitní programový čítač. Zajímavé je, že bajtový kód BPF byl vytvořen podle modelu Motorola 6502 ISA. Jak připomněl Steve McCann ve svém zpráva z pléna na Sharkfestu '11 byl obeznámen s buildem 6502 ze střední školy při programování na Apple II a tyto znalosti ovlivnily jeho práci při navrhování bajtkódu BPF.

Podpora BPF je implementována v linuxovém jádře ve verzi v2.5 a novější, kterou přidal především Jay Schullist. Kód BPF zůstal nezměněn až do roku 2011, kdy Eric Dumaset přepracoval interpret BPF tak, aby fungoval v režimu JIT (Zdroj: JIT pro paketové filtry). Poté, namísto interpretace bajtového kódu BPF, mohlo jádro přímo převést programy BPF na cílovou architekturu: x86, ARM, MIPS atd.

Později, v roce 2014, Alexej Starovoitov navrhl nový mechanismus JIT pro BPF. Ve skutečnosti se tento nový JIT stal novou architekturou založenou na BPF a nazýval se eBPF. Myslím, že oba virtuální počítače nějakou dobu koexistovaly, ale filtrování paketů je v současné době implementováno nad eBPF. Ve skutečnosti je v mnoha příkladech moderní dokumentace BPF označován jako eBPF a klasický BPF je dnes znám jako cBPF.

eBPF rozšiřuje klasický virtuální stroj BPF několika způsoby:

  • Spoléhá na moderní 64bitové architektury. eBPF používá 64bitové registry a zvyšuje počet dostupných registrů ze 2 (akumulátor a X) na 10. eBPF také poskytuje další operační kódy (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Odpojeno od subsystému síťové vrstvy. BPF byl svázán s dávkovým datovým modelem. Protože byl použit k filtrování paketů, jeho kód byl v subsystému, který poskytoval síťové interakce. Virtuální stroj eBPF však již není vázán na datový model a lze jej použít k libovolnému účelu. Nyní tedy lze program eBPF připojit ke sledovacímu bodu nebo kprobe. To otevírá dveře k instrumentaci eBPF, analýze výkonu a mnoha dalším případům použití v kontextu jiných subsystémů jádra. Nyní je kód eBPF umístěn ve své vlastní cestě: kernel/bpf.
  • Globální úložiště dat nazývané Mapy. Mapy jsou úložiště klíč-hodnota, která zajišťují výměnu dat mezi uživatelským prostorem a prostorem jádra. eBPF nabízí několik typů karet.
  • Sekundární funkce. Zejména pro přepsání balíčku, výpočet kontrolního součtu nebo klonování balíčku. Tyto funkce běží uvnitř jádra a nepatří do programů v uživatelském prostoru. Kromě toho lze systémová volání provádět z programů eBPF.
  • Ukončete hovory. Velikost programu v eBPF je omezena na 4096 bajtů. Funkce end call umožňuje programu eBPF přenést řízení na nový program eBPF a obejít tak toto omezení (takto lze zřetězit až 32 programů).

Příklad eBPF

Ve zdrojích linuxového jádra je několik příkladů pro eBPF. Jsou k dispozici na sample/bpf/. Pro sestavení těchto příkladů stačí napsat:

$ sudo make samples/bpf/

Nebudu psát nový příklad pro eBPF sám, ale použiji jeden ze vzorků dostupných v sample/bpf/. Podívám se na některé části kódu a vysvětlím, jak to funguje. Jako příklad jsem zvolil program tracex4.

Obecně platí, že každý z příkladů v sample/bpf/ se skládá ze dvou souborů. V tomto případě:

  • tracex4_kern.c, obsahuje zdrojový kód, který má být spuštěn v jádře jako bajtový kód eBPF.
  • tracex4_user.c, obsahuje program z uživatelského prostoru.

V tomto případě musíme zkompilovat tracex4_kern.c na bajtkód eBPF. V tuto chvíli v gcc neexistuje žádná serverová část pro eBPF. Naštěstí, clang může vytvářet bajtový kód eBPF. Makefile použití clang sestavit tracex4_kern.c do souboru objektu.

Výše jsem zmínil, že jednou z nejzajímavějších funkcí eBPF jsou 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 mnoha typů karet nabízených eBPF. V tomto případě je to jen hash. Možná jste si také všimli reklamy SEC("maps"). SEC je makro používané k vytvoření nové části binárního souboru. Vlastně v příkladu tracex4_kern jsou definovány další dva oddíly:

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

Tyto dvě funkce umožňují odstranit záznam z mapy (kprobe/kmem_cache_free) a přidejte na mapu nový záznam (kretprobe/kmem_cache_alloc_node). Všechny názvy funkcí napsané velkými písmeny odpovídají makrům definovaným v bpf_helpers.h.

Pokud vypíšu sekce objektového souboru, měl bych vidět, že tyto nové sekce jsou již definovány:

$ 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

Stále ano tracex4_user.c, hlavní program. V podstatě tento program naslouchá událostem kmem_cache_alloc_node. Když taková událost nastane, provede se odpovídající kód eBPF. Kód uloží atribut IP objektu do mapy a poté objekt prochází hlavním programem. Pří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

Jak spolu souvisí program uživatelského prostoru a program eBPF? Při inicializaci tracex4_user.c načte soubor objektu tracex4_kern.o pomocí funkce 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;
}

Při provádění load_bpf_file jsou přidány sondy definované v souboru eBPF /sys/kernel/debug/tracing/kprobe_events. Nyní těmto událostem nasloucháme a náš program může něco udělat, když k nim dojde.

$ 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šechny ostatní programy v sample/bpf/ jsou strukturovány podobně. Obsahují vždy dva soubory:

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

Program eBPF definuje mapy a funkce spojené s úsekem. Když jádro vyšle událost určitého typu (např. tracepoint), provedou se vázané funkce. Mapy zajišťují komunikaci mezi programem jádra a programem v uživatelském prostoru.

Závěr

V tomto článku byly BPF a eBPF diskutovány obecně. Vím, že informací a zdrojů o eBPF je dnes spousta, proto doporučím ještě pár materiálů k dalšímu studiu.

Doporučuji přečíst:

Zdroj: www.habr.com

Přidat komentář