Üdv, Habr! Értesítünk, hogy hamarosan megjelenik a "".

Mivel a BPF virtuális gép folyamatosan fejlődik és széles körben alkalmazzák a gyakorlatban, lefordítottuk számodra azt a cikket, amely az alapvető lehetőségeit és aktuális állapotát írja le.
Az utóbbi években egyre népszerűbbé váltak azok az eszközök és technikák, amelyek célja a Linux kernel korlátainak kompenzálása olyan esetekben, amikor nagy teljesítményű csomagfeldolgozásra van szükség. Az ilyen típusú egyik legismertebb technika a kernel bypass (kernel megkerülés), amely lehetővé teszi, hogy a hálózati szintet megkerülve, a csomagok feldolgozását a felhasználói térből végezzük. A kernel megkerülés azt is feltételezi, hogy a hálózati kártyát a felhasználói térbőlkezeljük. Más szavakkal, amikor a hálózati kártyával dolgozunk, a vezérlőre támaszkodunk. felhasználói térből.
A hálózati kártya teljes kontrolljának átadása egy felhasználói térben futó programnak csökkenti a kernel működésével kapcsolatos költségeket (kontekstswitch, hálózati szint feldolgozás, megszakítások, stb.), ami különösen fontos, ha 10 Gb/s vagy annál magasabb sebességgel dolgozunk. A kernel megkerülés plusz más lehetőségek kombinációja (csomagfeldolgozás) és a teljesítmény finomhangolása (NUMA figyelembevétele, CPU elkülönítése, stb.) képezi a felhasználói térben végzett nagy teljesítményű hálózati feldolgozás alapját. Talán a legjobb példa erre az új csomagfeldolgozási megközelítésre az Intel-től (Data Plane Development Kit), bár más széles körben ismert eszközök és technikák is léteznek, mint például a Cisco VPP (Vector Packet Processing), Netmap és természetesen a .
A felhasználói térben történő hálózati interakciók szervezése számos hátránnyal jár:
- A kernel szintű operációs rendszer az硬件 ресурсok absztrakt szintje. Mivel a felhasználói térben futó programoknak közvetlenül kell kezelniük a saját erőforrásaikat, ezért a saját hardverüket is kezelniük kell. Ez gyakran azt jelenti, hogy saját meghajtók programozására van szükség.
- Mivel teljesen lemondunk a kernel teréről, az összes hálózati funkciót is elutasítjuk, amelyet a kernel biztosít. A felhasználói térbeli programoknak újra kell implementálniuk azokat a funkciókat, amelyeket esetleg már a kernel vagy az operációs rendszer biztosít.
- A programok homokozó módban működnek, ami jelentősen korlátozza a kölcsönhatás lehetőségeiket, és megnehezíti számukra az integrációt az operációs rendszer más részeivel.
Lényegében a felhasználói térbeli hálózati interakciók szervezésekor a teljesítmény növelése azzal érhető el, hogy a csomagok feldolgozása a kernelből a felhasználói térbe kerül. Az XDP éppen az ellenkezőjét teszi: a hálózati programokat a felhasználói térből (szűrők, átalakítók, routing stb.) a kernel területére helyezi át. Az XDP lehetővé teszi számunkra, hogy hálózati funkciót hajtsunk végre, amint a csomag megérkezik a hálózati interfészhez, és mielőtt elkezdene felfelé haladni a kernel hálózati alrendszerében. Ennek eredményeként a csomagok feldolgozásának sebessége lényegesen megnő. Hogyan lehet azonban a kernel lehetővé, hogy a felhasználó programokat hajtson végre a kernel terében? Mielőtt erre a kérdésre válaszolnánk, nézzük meg, mi az a BPF.
BPF és eBPF
A BPF (Berkeley Packet Filter) elnevezése bár némileg zavaró, valójában egy virtuális gép modell. Ezt a virtuális gépet eredetileg csomagszűrési feladatokhoz tervezték, ezért kapta ezt a nevet.
Az egyik legismertebb eszköz, amely a BPF-et használja, tcpdump. Csomagok elfogásakor a felhasználó megadhat egy kifejezést a csomagok szűrésére. Csak azokat a csomagokat fogják elkapni, amelyek megfelelnek ennek a kifejezésnek. Például a kifejezés “ tcpdump tcp dst port 80” minden olyan TCP csomagra vonatkozik, amely a 80-as portra érkezik. A fordító ezt a kifejezést le tudja egyszerűsíteni, és BPF bájt-kódra alakíthatja.Ez az, amit a fent említett program alapvetően tesz:
$ 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
Utasítás (000): betölti a csomagot 12-es eltolással, 16 bites szót formájában az akkumulátorba. A 12-es eltolás az ethertype csomagra vonatkozik.
- Utasítás (001): összehasonlítja az akkumulátorban lévő értéket 0x86dd-del, azaz az IPv6 ethertype értékével. Ha az eredmény true, a program számlálója a (002) utasításra lép, ha nem, a (006) utasításra.
- A program tehát a lekérdezett ethertype szerint dönt a továbblépésről.
- Útmutató (006): összehasonlítja az értéket a 0x800-zal (ethertype érték IPv4-hez). Ha a válasz igaz, a program a (007)-hez lép, ellenkező esetben a (015)-höz.
És így tovább, amíg a csomagszűrő program eredményt nem ad vissza. Ez általában logikai érték. A nem nulla érték visszaadása (útmutató (014)) azt jelenti, hogy a csomag megfelelő volt, míg a nulla visszaadása (útmutató (015)) azt jelenti, hogy a csomag nem volt megfelelő.
A BPF virtuális gépét és bytekódját Steve McCanne és Van Jacobson javasolta 1992 végén, amikor megjelent a cikkük. , először ezt a technológiát az Usenix konferencián mutatták be 1993 telén.
Mivel a BPF egy virtuális gép, meghatározza a környezetet, amelyben a programok futnak. A bytekód mellett meghatározza a csomagmempá-modellt (a betöltési utasítások implicit módon alkalmazva vannak a csomagra), a regisztereket (A és X; a véletlen és indexregiszterek), a memória tárolóját és az implicit programszámlálót. Érdekes módon a BPF bytekódja a Motorola 6502 ISA mintájára készült. Ahogy Steve McCanne emlékezett vissza a a Sharkfest ‘11-en, már az iskolás évei óta ismerte a 6502 assembly-t, amikor az Apple II-n programozott, és ez a tudás befolyásolta a BPF bytekód tervezését.
A BPF támogatása a Linux kernelben a v2.5-ös verzióban és annál magasabb szinten valósult meg, főként Jay Schulist erőfeszítéseinek köszönhetően. A BPF kód jelentős változtatások nélkül maradt 2011-ig, amikor Eric Dumazet átdolgozta a BPF értelmezőt JIT üzemmódra (Forrás: ). Ekkor a kernel a BPF bytekód értelmezése helyett közvetlenül konvertálta a BPF programokat a céleszköz architektúrájára: x86, ARM, MIPS stb.
Később, 2014-ben, Alexey Starovoytov új JIT mechanizmust javasolt a BPF számára. Valójában ez az új JIT egy új architektúraként jött létre a BPF alapján, és eBPF néven vált ismertté. Úgy gondolom, hogy egy ideig mindkét virtuális gép együtt létezett, de manapság a csomagszűrés eBPF alapú. Valójában, a modern dokumentációkban gyakran az eBPF értendő BPF alatt, míg a klasszikus BPF-t ma cBPF néven ismerjük.
Az eBPF több szempontból is kiterjeszti a klasszikus virtuális BPF gépet:
- A modern 64-bit architecture is utilized. eBPF uses 64-bit registers and increases the number of available registers from 2 (accumulator and X) to 10. eBPF also provides additional operation codes (BPF_MOV, BPF_JNE, BPF_CALL…).
- Decoupled from the network layer subsystem. BPF was tied to a packet data model. As it was used for packet filtering, its code resided in the subsystem that provided network interactions. However, the eBPF virtual machine is no longer tied to the data model and can be used for any purposes. Thus, the eBPF program can now be attached to a tracepoint or kprobe. This opens the way for eBPF instrumentation, performance analysis, and many other use cases in the context of other kernel subsystems. Now eBPF code resides in its own path: kernel/bpf.
- Global data storage known as Maps. Maps are key-value storage that facilitate data exchange between user space and kernel space. eBPF provides several types of maps.
- Helper functions. Specifically, for packet rewriting, checksum calculation, or packet cloning. These functions are executed inside the kernel and are not related to user-space programs. Additionally, system calls can be made from eBPF programs.
- Tail calls. The size of a program in eBPF is limited to 4096 bytes. The tail call capability allows an eBPF program to pass control to a new eBPF program, thus bypassing this limitation (allowing up to 32 programs to be chained).
eBPF: example
The Linux kernel sources contain several examples for eBPF. They are available at samples/bpf/. To compile these examples, simply enter:
$ sudo make samples/bpf/
I will not write a new eBPF example myself but will use one of the samples available in samples/bpf/. I will examine some sections of the code and explain how it works. As an example, I have chosen the program tracex4.
Generally, each of the examples in samples/bpf/ consists of two files. In this case:
tracex4_kern.c, contains the source code that should run in the kernel as eBPF bytecode.tracex4_user.c, contains the user-space program.
In that case, we need to compile tracex4_kern.c bájt-kód eBPF. Jelenleg nincs az eBPF-hez kapcsolódó szerveroldal. gcc Szerencsére képes bájt-kódot generálni eBPF-hez. clang a fordításhoz használja clang objektum fájlba. tracex4_kern.c Az előbb említettem, hogy az eBPF egyik legérdekesebb funkciója a térképek. A tracex4_kern definiál egy térképet:
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, };
ez az eBPF által kínált sok féle térkép egyike. Ebben az esetben ez egyszerűen egy hash. Szintén észlelhette a deklarációt BPF_MAP_TYPE_HASH SEC("maps") . A SEC egy makró, amely új szekció létrehozására szolgál a bináris fájlban. Valójában a példábantracex4_kern két új szekciót határoz meg: 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;// megszerezzük a megjelenítő IP-címét a kmem_cache_alloc_node() hívásához 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; }
Ezek a funkciók lehetővé teszik egy bejegyzés eltávolítását a térképből ( kprobe/kmem_cache_free) és egy új bejegyzés hozzáadását a térképhez (kretprobe/kmem_cache_alloc_node). Minden elnevezett funkció, amelyet nagybetűkkel írtak, aHa kiírom az objektumfájl szekcióinak dumpját, látni fogom, hogy ezek az új szekciók már definiálva vannak: .
$ objdump -h tracex4_kern.o
tracex4_kern.o: fájlformátum elf64-little
Szekciók:
Idx Név Méret VMA LMA Fájl off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
TARTALOM, ALAKÍTHATÓ, BETÖLTÖTT, OLVASÁSI
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
TARTALOM, ALAKÍTHATÓ, BETÖLTÖTT, ÁTALAKÍTHATÓ, OLVASÁSI
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
3 térképek 0000001c 0000000000000000 0000000000000000 00000148 2**2
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
TARTALOM, ALAKÍTHATÓ, BETÖLTÖTT, ADAT
4 engedélyezés 00000004 0000000000000000 0000000000000000 00000164 2**0
5 verzió 00000004 0000000000000000 0000000000000000 00000168 2**2
4 engedélyezés 00000004 0000000000000000 0000000000000000 00000164 2**0
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
4 engedélyezés 00000004 0000000000000000 0000000000000000 00000164 2**0
TARTALOM, ALAKÍTHATÓ, BETÖLTÖTT, ÁTALAKÍTHATÓ, OLVASÁSI
, a fő program. Elvileg ez a program eseményeket figyel meg
Továbbá van kmem_cache_alloc_node . Amikor egy ilyen esemény történik, a megfelelő eBPF kód végrehajtódik. A kód elmenti az objektum IP-attribútumát a térképbe, és ezután az objektum ciklikusan kiírásra kerül a fő programban. Példa:Hogyan kapcsolódik a felhasználói tér programja és az eBPF program? Kezdetkor
$ sudo .\/tracex4
obj 0xffff8d6430f60a00 2 másodperce régi, itt lett lefoglalva az ip címen: ffffffff9891ad90
obj 0xffff8d6062ca5e00 23 másodperce régi, itt lett lefoglalva az ip címen: ffffffff98090e8f
obj 0xffff8d5f80161780 6 másodperce régi, itt lett lefoglalva az ip címen: ffffffff98090e8f
betölti az objektum fájlt tracex4_user.c tracex4_kern.o a funkció segítségével a bpf_fájl_betekintése.
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;
} A végrehajtás során az eBPF fájlban meghatározott szondák hozzáadásra kerülnek a /sys/kernel/debug/tracing/kprobe_events. Most hallgatjuk ezeket az eseményeket, és programunk bármit tehet, amikor ezek bekövetkeznek.
$ 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
Minden más program a sample/bpf/ könyvtárban hasonlóan van struktúrálva. Mindig két fájl található bennük:
XXX_kern.c: eBPF program.XXX_user.c: az alap program.
Az eBPF program definálja a térképeket és a szekcióhoz rendelt funkciókat. Amikor a kernel egy adott típusú eseményt generál (például tracepoint-hoz), a hozzárendelt funkciók végrehajtásra kerülnek. A térképek lehetővé teszik az adatok cseréjét a kernel program és a felhasználói tér programja között.
Zárás
Ebben a cikkben általánosan szó esett a BPF-ről és az eBPF-ről. Tudom, hogy ma már nagyon sok információ és forrás található az eBPF-ről, ezért javasolnék még néhány anyagot a további tanulmányozásra.
Ajánlom a következő olvasmányokat:
- Jonathan Corbett. Bevezetés a BPF-be, és arról, hogyan fejlődött az eBPF-vé.
- Brendan Gregg. Cikk az LWN.net-ről. Brendan gyakran tweetel eBPF-ről és vezet egy forrástartalmat ezzel a témával kapcsolatban. .
- Julia Evans. Kommentárok a Suchakra Sharma előadásához "A BSD Csomagszűrő: Új Architektúra Felhasználói Csomag Rögzítéshez". A kommentárok hasznosak, és valóban segítenek megérteni a diát.
- Ferris Ellis. Hosszú cikk az , de érdemes elolvasni. Az egyik legjobb eBPF cikk, amit olvastam.
Forrás: habr.com
