Ahoj, Habr! Informujeme vás, že se nám chystá na vydání kniha "".

Protože virtuální stroj BPF neustále evoluuje a je aktivně používán v praxi, přeložili jsme pro vás článek, který popisuje jeho hlavní schopnosti a aktuální stav.
V posledních letech nabírají na popularitě nástroje pro programování a techniky určené k překonání omezení jádra Linuxu v situacích, kdy je zapotřebí vysoce výkonného zpracování paketů. Jeden z nejpopulárnějších postupů tohoto druhu se nazývá obejití jádra (kernel bypass) a umožňuje, přeskočením síťové úrovně jádra, provádět celé zpracování paketů z uživatelského prostoru. Obejité jádro také předpokládá řízení síťové karty z uživatelského prostoru. Jinými slovy, při práci se síťovou kartou se spoléháme na ovladač uživatelského prostoru.
Přenosem plné kontroly nad síťovou kartou do programu z uživatelského prostoru snižujeme náklady spojené s činností jádra (přepínání kontextu, zpracování síťové úrovně, přerušení atd.), což je dost důležité při provozu na rychlostech 10 Gb/s nebo vyšších. Obejité jádro v kombinaci s dalšími možnostmi (batch processing) a pečlivé ladění výkonu (zohlednění NUMA, izolace CPU, atd.) odpovídají základům vysoce výkonného zpracování sítí v uživatelském prostoru. Možná, vzorový příklad takového nového přístupu ke zpracování paketů je od společnosti Intel (Data Plane Development Kit), i když existují také jiné široce známé nástroje a postupy, mezi které patří VPP od Cisco (Vector Packet Processing), Netmap a samozřejmě, .
U organizace síťových interakcí v uživatelském prostoru je řada nevýhod:
- Jádro OS je úroveň abstrakce pro hardwarové prostředky. Protože programy v uživatelském prostoru musí spravovat své prostředky přímo, musí také řídit své vlastní hardwarové vybavení. To často znamená potřebu programování vlastních ovladačů.
- Jelikož se zcela vzdáváme jádrového prostoru, vzdáváme se také veškeré síťové funkčnosti, kterou jádro poskytuje. Programy uživatelského prostoru musí znovu implementovat ty funkce, které mohou být jádrem nebo operačním systémem již poskytovány.
- Programy fungují v pískoviště, což vážně omezuje možnosti jejich interakce a brání jim v integraci s dalšími částmi operačního systému.
V podstatě, při organizaci síťových interakcí v uživatelském prostoru se zvýšení výkonu dosahuje přenesením zpracování paketů z jádra do uživatelského prostoru. XDP dělá opak: přesouvá síťové programy z uživatelského prostoru (filtry, transformace, směrování atd.) do oblasti jádra. XDP nám umožňuje provést síťovou funkci, jakmile paket vstoupí na síťové rozhraní a než začne stoupat v síťovém subsystému jádra. Výsledkem je výrazné zvýšení rychlosti zpracování paketů. Jak ale jádro umožňuje uživatelům vykonávat své programy v jádru? Než odpovíme na tuto otázku, podívejme se, co je to BPF.
BPF a eBPF
Přestože název BPF (Berkeley Packet Filter) není zcela jasný, skutečně se jedná o model virtuálního stroje. Tento virtuální stroj byl původně navržen pro zpracování filtrování paketů, a odtud také jeho název.
Jedním z nejznámějších nástrojů využívajících BPF je tcpdump. Při zachycení paketů pomocí tcpdump může uživatel zadat výraz pro filtrování paketů. Zachyceny budou pouze pakety, které odpovídají tomuto výrazu. Například výraz "tcp dst port 80" se týká všech TCP paketů přicházejících na port 80. Kompilátor může tento výraz zkrátit převedením na BPF byte-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
To je v zásadě to, co dělá výše uvedený program:
- Instrukce (000): načítá paket s posunem 12, v podobě 16bitového slova do akumulátoru. Posun 12 odpovídá ethertype paketu.
- Instrukce (001): porovnává hodnotu v akumulátoru s 0x86dd, což je ethertype hodnota pro IPv6. Pokud je výsledek pravdivý, programový počítadlo přechází k instrukci (002), jinak k (006).
- Instrukce (006): porovnává hodnotu s 0x800 (ethertype-hodnota pro IPv4). Pokud je odpověď true, program přejde k (007), jinak k (015).
A tak dále, dokud program filtru paketů nevrátí výsledek. Obvykle je to boolean. Návrat nenulové hodnoty (instrukce (014)) znamená, že paket vyhovuje, a návrat nulové hodnoty (instrukce (015)) znamená, že paket nevyhovuje.
Virtuální stroj BPF a jeho bajtový kód byly navrženy Steveem McCanem a Vanem Jacobsonem na konci roku 1992, kdy vyšel jejich článek. , poprvé byla tato technologie představena na konferenci Usenix v zimě 1993.
Protože BPF je virtuální stroj, určuje prostředí, ve kterém se programy vykonávají. Kromě bajtového kódu také definuje model paměti paketů (pokyny pro načítání se implicitně aplikují na paket), registry (A a X; registry akumulátoru a indexu), úložiště scratch paměti a implicitní programový čítač. Zajímavé je, že bajtový kód BPF byl modelován podle ISA Motorola 6502. Jak vzpomínal Steve McCann ve svém na Sharkfest ‘11, byl obeznámen se stavbou 6502 už od střední školy, když programoval na Apple II, a tyto znalosti ovlivnily jeho práci na návrhu bajtového kódu BPF.
Podpora BPF byla implementována v jádře Linuxu od verze v2.5 a vyšší, byla přidána především díky úsilí Jaye Shullista. Kód BPF zůstal bez významných změn až do roku 2011, kdy Eric Dumazet přepracoval interpretátor BPF pro práci v režimu JIT (Zdroj: ). Po tomto jádro místo interpretace bajtového kódu BPF mohlo přímo převádět programy BPF na cílovou architekturu: x86, ARM, MIPS atd.
Později, v roce 2014, Alexey Starovoitov navrhl nový JIT mechanismus pro BPF. Tento nový JIT se v podstatě stal novou architekturou na základě BPF a byl nazván eBPF. Myslím, že nějakou dobu spolu existovaly obě virtuální stroje, ale v současnosti se filtrování paketů realizuje na základě eBPF. V podstatě v mnoha příkladech současné dokumentace se pod BPF rozumí eBPF, zatímco klasická BPF je dnes známá jako cBPF.
eBPF rozšiřuje klasický virtuální stroj BPF v několika ohledech:
- Opírá se o moderní 64bitové architektury. eBPF používá 64bitové registry a zvyšuje počet dostupných registrů z 2 (akumulátor a X) na 10. V eBPF jsou k dispozici také další operace (BPF_MOV, BPF_JNE, BPF_CALL…).
- Odpoutána od pod systému síťové úrovně. BPF byla vázána na model datových paketů. Používala se k filtrování paketů, a její kód se nacházel v podsystému, který zajišťoval síťové interakce. Virtuální stroj eBPF však již není vázán na model dat a může být použit k různým účelům. Program eBPF je nyní možné připojit k tracepointu nebo k kprobe. To otevírá cestu k instrumentaci eBPF, analýze výkonu a mnoha dalším užití v kontextu jiných subsystémů jádra. Kód eBPF nyní existuje na vlastní cestě: kernel/bpf.
- Globální datové úložiště nazývané Mapy. Mapy jsou úložiště typu "klíč-hodnota", která zajišťují výměnu dat mezi uživatelským prostorem a jádrem. V eBPF jsou k dispozici mapy několika typů.
- Pomocné funkce. Konkrétně pro přepisování paketu, výpočet kontrolního součtu nebo klonování paketu. Tyto funkce se vykonávají uvnitř jádra a netýkají se programů uživatelského prostoru. Z programů eBPF jsou také možné systémové volání.
- Koncové volání. Velikost programu v eBPF je omezena na 4096 bytů. Možnost koncového volání umožňuje programu eBPF předat řízení nové eBPF programu a tak obejít toto omezení (lze tak propojit až 32 programů).
eBPF: příklad
V zdrojových kódech jádra Linuxu je několik příkladů pro eBPF. Jsou dostupné na adrese samples/bpf/. Pro kompilaci těchto příkladů stačí zadat:
$ sudo make samples/bpf/
Nebudu psát nový příklad pro eBPF, místo toho využiji jeden z ukázek, které jsou k dispozici v samples/bpf/. Podívám se na některé úseky kódu a vysvětlím, jak funguje. Jako příklad jsem vybral program tracex4.
Každý z příkladů v samples/bpf/ se skládá ze dvou souborů. V tomto případě:
tracex4_kern.c, obsahuje zdrojový kód, který má být vykonáván v jádře jako bytecode eBPF.tracex4_user.c, obsahuje program z uživatelského prostoru.
V takovém případě je třeba zkompilovat tracex4_kern.c do bytecode eBPF. V současné době chybí gcc serverová část pro eBPF. Naštěstí clang může generovat bytecode eBPF. používá clang pro kompilaci tracex4_kern.c do objektového souboru.
Zmínil jsem, ž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 – jeden z mnoha typů map, které eBPF nabízí. V tomto případě se jedná o jednoduchý hash. Také jste si možná všimli deklarace SEC("maps"). SEC je makro, které se používá k vytvoření nové sekce binárního souboru. Co se týče příkladu, tracex4_kern se definují ještě dvě sekce:
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;
\/\/ získáváme ip-adresu volající strany 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řidat do mapy nový záznam (kretprobe/kmem_cache_alloc_node). Všechna jména funkcí, napsaná velkými písmeny, odpovídají makrům definovaným v .
Pokud provedu dump sekcí objektového souboru, měl bych vidět, že tyto nové sekce jsou již definovány:
$ objdump -h tracex4_kern.o
tracex4_kern.o: formát souboru elf64-little
Sekce:
Idx Název Velikost VMA LMA Offset souboru Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
OBSAH, PŘIDĚLENÍ, NAČTENÍ, POUZE KE ČTENÍ, KÓD
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
OBSAH, PŘIDĚLENÍ, NAČTENÍ, RELOKACE, POUZE KE ČTENÍ, KÓD
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
OBSAH, PŘIDĚLENÍ, NAČTENÍ, RELOKACE, POUZE KE ČTENÍ, KÓD
3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2
OBSAH, PŘIDĚLENÍ, NAČTENÍ, DATA
4 license 00000004 0000000000000000 0000000000000000 00000164 2**0
OBSAH, PŘIDĚLENÍ, NAČTENÍ, DATA
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
OBSAH, PŘIDĚLENÍ, NAČTENÍ, DATA
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
OBSAH, PŘIDĚLENÍ, NAČTENÍ, RELOKACE, POUZE KE ČTENÍ, DATA
Také existuje , hlavní program. V podstatě tento program sleduje události kmem_cache_alloc_node. Když nastane taková událost, provede se příslušný kód eBPF. Kód uloží atribut IP objektu do mapy a pak se tento objekt cyklicky vypisuje v hlavním programu. Příklad:
$ sudo ./tracex4
obj 0xffff8d6430f60a00 je 2 sekundy starý, byl alokován na ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 je 23 sekund starý, byl alokován na ip ffffffff98090e8f
obj 0xffff8d5f80161780 je 6 sekund starý, byl alokován na ip ffffffff98090e8f
Jak jsou propojena uživatelskoprocesní programa a program eBPF? Při inicializaci tracex4_user.c načte objektový soubor tracex4_kern.o pomocí funkce nahrát_bpf_soubor.
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í sondy definované v souboru eBPF jsou přidány do /sys/kernel/debug/tracing/kprobe_events. Nyní sledujeme tyto události a náš program může něco udělat, když nastanou.
$ 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 ve vzoru /bpf/ jsou strukturovány podobně. Vždy obsahují dva soubory:
XXX_kern.c: program eBPF.XXX_user.c: hlavní program.
Program eBPF definuje mapy a funkce připojené k sekci. Když jádro vyprodukuje událost určitého typu (například tracepoint), připojené funkce se vykonávají. Mapy zajišťují výměnu dat mezi programem jádra a programem uživatelské oblasti.
Závěr
V tomto článku byly v obecných rysech probrány BPF a eBPF. Vím, že dnes existuje mnoho informací a zdrojů o eBPF, proto doporučím ještě několik materiálů pro další studium.
Doporučuji přečíst:
- Jonathana Corbetta. Úvod do BPF a příběh o tom, jak se vyvinula v eBPF.
- Brendana Gregga. Článek na webu LWN.net. Brendan často tweetuje o eBPF a udržuje seznam zdrojů na toto téma na svém .
- Julie Evans. Komentáře k prezentaci Suchakry Sharmy 'The BSD Packet Filter: A New Architecture for User-level Packet Capture'. Komentáře jsou dobré a skutečně pomáhají pochopit snímky.
- Ferrise Ellise. Dlouhý článek s , ale stojí za to si přečíst. Jeden z nejlepších článků o eBPF, které jsem viděl.
Zdroj: habr.com
