A BPF és az eBPF rövid bemutatása

Szia Habr! Tájékoztatjuk Önöket, hogy egy könyv kiadására készülünk"Linux-megfigyelhetőség BPF-fel".

A BPF és az eBPF rövid bemutatása
Mivel a BPF virtuális gép folyamatosan fejlődik, és aktívan használják a gyakorlatban, lefordítottunk egy cikket, amely leírja főbb jellemzőit és jelenlegi állapotát.

Az elmúlt években népszerűvé váltak a programozási eszközök és technikák, amelyek kompenzálják a Linux kernel korlátait olyan esetekben, amikor nagy teljesítményű csomagfeldolgozásra van szükség. Az egyik legnépszerűbb ilyen módszer az ún mag bypass (kernel bypass), és lehetővé teszi a kernel hálózati rétegének kihagyásával, hogy minden csomagfeldolgozást a felhasználói területről hajtson végre. A kernel megkerülése magában foglalja a hálózati kártya kezelését is felhasználói tér. Más szóval, amikor hálózati kártyával dolgozunk, az illesztőprogramra hagyatkozunk felhasználói tér.

A hálózati kártya teljes vezérlésének egy felhasználói térbeli programra való átadásával csökkentjük a kernel többletterhelését (kontextuskapcsolók, hálózati réteg feldolgozás, megszakítások stb.), ami 10 Gb / s vagy nagyobb sebességen történő futás esetén nagyon fontos. A kernel megkerülése és egyéb szolgáltatások kombinációja (kötegelt feldolgozás) és gondos teljesítményhangolás (NUMA könyvelés, CPU leválasztásstb.) illeszkednek a nagy teljesítményű felhasználói térbeli hálózatépítés alapjaihoz. Talán példaértékű példa a csomagfeldolgozás ezen új megközelítésére DPDK az Inteltől (Data Plane Development Kit), bár vannak más jól ismert eszközök és technikák, köztük a Cisco VPP (Vector Packet Processing), a Netmap és természetesen, snab.

A hálózati interakciók felhasználói térben való megszervezésének számos hátránya van:

  • Az operációs rendszer kernel a hardver erőforrások absztrakciós rétege. Mivel a felhasználói területi programoknak közvetlenül kell kezelniük erőforrásaikat, saját hardverüket is kezelniük kell. Ez gyakran azt jelenti, hogy saját illesztőprogramokat kell programozni.
  • Mivel teljesen feladjuk a kernelteret, a kernel által biztosított összes hálózati funkciót is feladjuk. A felhasználói területi programoknak újra kell implementálniuk azokat a funkciókat, amelyeket a kernel vagy az operációs rendszer már biztosított.
  • A programok sandbox módban működnek, ami komolyan korlátozza interakciójukat, és megakadályozza, hogy integrálódjanak az operációs rendszer más részeivel.

Lényegében a felhasználói térben történő hálózatépítés során teljesítménynövekedés érhető el a csomagfeldolgozás áthelyezésével a kernelből a felhasználói térbe. Az XDP pont az ellenkezőjét teszi: a hálózati programokat a felhasználói térből (szűrők, konverterek, útválasztás stb.) a kernel területére helyezi át. Az XDP lehetővé teszi, hogy azonnal végrehajtsuk a hálózati funkciót, amint a csomag eléri a hálózati interfészt, és mielőtt elindulna a kernel hálózati alrendszerébe. Ennek eredményeként a csomagfeldolgozás sebessége jelentősen megnő. Azonban hogyan teszi lehetővé a kernel a felhasználó számára, hogy programjait kerneltérben futtassa? Mielőtt válaszolna erre a kérdésre, nézzük meg, mi az a BPF.

BPF és eBPF

A nem teljesen világos név ellenére a BPF (Packet Filtering, Berkeley) valójában egy virtuális gép modell. Ezt a virtuális gépet eredetileg csomagszűrésre tervezték, innen ered a név.

A BPF-et használó egyik legismertebb eszköz az tcpdump. Csomagok rögzítésekor a tcpdump a felhasználó megadhat egy kifejezést a csomagszűréshez. Csak azok a csomagok kerülnek rögzítésre, amelyek megfelelnek ennek a kifejezésnek. Például a "tcp dst port 80” a 80-as porton érkező összes TCP-csomagra vonatkozik. A fordító lerövidítheti ezt a kifejezést, ha BPF bájtkódra konvertálja.

$ 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

A fenti program lényegében ezt csinálja:

  • Utasítás (000): A 12-es eltolásnál lévő csomagot 16 bites szóként betölti az akkumulátorba. A 12 eltolás a csomag étertípusának felel meg.
  • Utasítás (001): összehasonlítja az akkumulátorban lévő értéket a 0x86dd értékkel, azaz az IPv6 ethertype értékével. Ha az eredmény igaz, akkor a programszámláló a (002) utasításra megy, ha nem, akkor a (006) utasításra.
  • Utasítás (006): összehasonlítja az értéket a 0x800 értékkel (ethertype érték IPv4 esetén). Ha a válasz igaz, akkor a program (007-re), ha nem, akkor (015-re) megy.

És így tovább, amíg a csomagszűrő program eredményt nem ad. Általában logikai érték. A nullától eltérő érték visszaadása (utasítás (014)) azt jelenti, hogy a csomag megegyezett, a nulla visszaadása (015) pedig azt, hogy a csomag nem egyezik.

A BPF virtuális gépet és a hozzá tartozó bájtkódot Steve McCann és Van Jacobson javasolta 1992 végén, amikor papírjuk megjelent. BSD csomagszűrő: Új architektúra a felhasználói szintű csomagrögzítéshez, ezt a technológiát először a Usenix konferencián mutatták be 1993 telén.

Mivel a BPF egy virtuális gép, ez határozza meg a környezetet, amelyben a programok futnak. A bájtkódon kívül meghatároz egy csomagmemória modellt (a betöltési utasításokat implicit módon alkalmazzák a csomagra), regisztereket (A és X; akkumulátor- és indexregiszterek), scratch memória tárolót és implicit programszámlálót. Érdekes módon a BPF bájtkódot a Motorola 6502 ISA után modellezték. Ahogy Steve McCann felidézte a sajátjában plenáris jelentés A Sharkfest '11-en a 6502-es buildet már középiskolás korában ismerte, amikor Apple II-n programozott, és ez a tudás befolyásolta a BPF bájtkód tervezésében végzett munkáját.

A BPF-támogatás a Linux kernelben van megvalósítva a v2.5-ös és újabb verziókban, amelyeket főként Jay Schulist adott hozzá. A BPF kód változatlan maradt 2011-ig, amikor is Eric Dumaset áttervezte a BPF tolmácsot, hogy JIT módban működjön (Forrás: JIT csomagszűrőkhöz). Ezt követően a kernel a BPF bájtkód értelmezése helyett közvetlenül konvertálhatta a BPF programokat a cél architektúrára: x86, ARM, MIPS stb.

Később, 2014-ben Alekszej Starovoitov új JIT-mechanizmust javasolt a BPF számára. Valójában ez az új JIT egy új, BPF-en alapuló architektúra lett, és eBPF-nek hívták. Azt hiszem, a két virtuális gép egy ideig együtt létezett, de a csomagszűrés jelenleg az eBPF-en felül van megvalósítva. Valójában sok modern dokumentációs példában a BPF-et eBPF-nek nevezik, a klasszikus BPF-et pedig ma cBPF-nek nevezik.

Az eBPF több módon is kiterjeszti a klasszikus BPF virtuális gépet:

  • Modern 64 bites architektúrákra támaszkodik. Az eBPF 64 bites regisztereket használ, és 2-ről (akkumulátor és X) 10-re növeli a rendelkezésre álló regiszterek számát. Az eBPF további műveleti kódokat is biztosít (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Leválasztva a hálózati réteg alrendszeréről. A BPF a kötegelt adatmodellhez volt kötve. Mivel csomagok szűrésére használták, kódja a hálózati interakciókat biztosító alrendszerben volt. Az eBPF virtuális gép azonban már nincs adatmodellhez kötve, és bármilyen célra használható. Tehát most az eBPF program a tracepointhoz vagy a kprobe-hoz köthető. Ez megnyitja az ajtót az eBPF-műszerezés, a teljesítményelemzés és sok más felhasználási eset előtt más kernel-alrendszerekkel összefüggésben. Most az eBPF kód a saját elérési útjában található: kernel/bpf.
  • Térképek nevű globális adattárak. A térképek kulcsérték-tárolók, amelyek adatcserét biztosítanak a felhasználói terület és a kernelterület között. Az eBPF többféle kártyát kínál.
  • Másodlagos funkciók. Különösen egy csomag felülírásához, ellenőrző összeg kiszámításához vagy egy csomag klónozásához. Ezek a függvények a kernelen belül futnak, és nem tartoznak a felhasználói programokhoz. Ezenkívül az eBPF programokból rendszerhívások is indíthatók.
  • Hívások befejezése. Az eBPF-ben a program mérete 4096 bájtra korlátozódik. A hívás befejezése funkció lehetővé teszi az eBPF program számára, hogy átadja a vezérlést egy új eBPF programnak, és így megkerülje ezt a korlátozást (akár 32 program is láncolható így).

eBPF példa

Számos példa található az eBPF-re a Linux kernelforrásokban. Elérhetők a samples/bpf/ oldalon. A példák összeállításához írja be:

$ sudo make samples/bpf/

Magam nem írok új példát az eBPF-hez, hanem a samples/bpf/-ben elérhető minták egyikét fogom használni. Megnézem a kód néhány részét, és elmagyarázom, hogyan működik. Példaként a programot választottam tracex4.

Általában a samples/bpf/ minden egyes példája két fájlból áll. Ebben az esetben:

  • tracex4_kern.c, tartalmazza a kernelben eBPF bájtkódként végrehajtandó forráskódot.
  • tracex4_user.c, felhasználói térből származó programot tartalmaz.

Ebben az esetben össze kell állítanunk tracex4_kern.c eBPF bájtkódra. Abban a pillanatban gcc az eBPF-hez nincs szerver rész. Szerencsére, clang képes eBPF bájtkódot előállítani. Makefile felhasznál clang összeállít tracex4_kern.c az objektumfájlba.

Fentebb említettem, hogy az eBPF egyik legérdekesebb funkciója a térképek. A tracex4_kern egy térképet határoz meg:

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 az eBPF által kínált számos kártyatípus egyike. Ebben az esetben ez csak egy hash. Lehet, hogy Ön is észrevette a hirdetést SEC("maps"). A SEC egy makró, amely egy bináris fájl új szakaszának létrehozására szolgál. Valójában a példában tracex4_kern két további szakasz van meghatározva:

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

Ez a két funkció lehetővé teszi egy bejegyzés eltávolítását a térképről (kprobe/kmem_cache_free) és adjon hozzá egy új bejegyzést a térképhez (kretprobe/kmem_cache_alloc_node). Minden nagybetűvel írt függvénynév megfelel a -ban definiált makróknak bpf_helpers.h.

Ha kiírom az objektumfájl szakaszait, látnom kell, hogy ezek az új szakaszok már meg vannak határozva:

$ objdump -h tracex4_kern.o

tracex4_kern.o: fájlformátum elf64-little

Részek:
Azonosító neve Méret VMA LMA Fájl ki Algn
0 .szöveg 00000000 0000000000000000 0000000000000000 00000040 2**2
TARTALOM, KIOSZTÁS, BETÖLTÉS, CSAK OLVASHATÓ, KÓD
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
TARTALOM, KIOSZTÁS, BETÖLTÉS, ÁTTÖLTÉS, CSAK OLVASHATÓ, KÓD
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
TARTALOM, KIOSZTÁS, BETÖLTÉS, ÁTTÖLTÉS, CSAK OLVASHATÓ, KÓD
3 térkép 0000001c 0000000000000000 0000000000000000 00000148 2**2
TARTALOM, ALLOKÁCIÓ, TERHELÉS, ADATOK
4-es licenc 00000004 0000000000000000 0000000000000000 00000164 2**0
TARTALOM, ALLOKÁCIÓ, TERHELÉS, ADATOK
5-ös verzió 00000004 0000000000000000 0000000000000000 00000168 2**2
TARTALOM, ALLOKÁCIÓ, TERHELÉS, ADATOK
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
TARTALOM, KIOSZTÁS, BETÖLTÉS, ÁTTÖLTÉS, CSAK OLVASHATÓ, ADATOK

Van még tracex4_user.c, fő program. Ez a program alapvetően eseményeket figyel kmem_cache_alloc_node. Ilyen esemény esetén a megfelelő eBPF kód végrehajtásra kerül. A kód elmenti az objektum IP attribútumát egy térképre, majd az objektumot a főprogramon keresztül hurkolja. Példa:

$ 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

Hogyan kapcsolódik a felhasználói térprogram és az eBPF program? Inicializáláskor tracex4_user.c betölti az objektumfájlt tracex4_kern.o funkció használatával 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;
}

A cselekvés által load_bpf_file Az eBPF fájlban meghatározott szondák hozzáadódnak /sys/kernel/debug/tracing/kprobe_events. Most figyeljük ezeket az eseményeket, és a programunk tehet valamit, ha megtörténik.

$ 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

A sample/bpf/ összes többi programja hasonló felépítésű. Mindig két fájlt tartalmaznak:

  • XXX_kern.c: eBPF program.
  • XXX_user.c: fő program.

Az eBPF program meghatározza a szakaszokhoz tartozó térképeket és függvényeket. Amikor a kernel egy bizonyos típusú eseményt bocsát ki (pl. tracepoint), a kötött függvények végrehajtásra kerülnek. A térképek kommunikációt biztosítanak a kernelprogram és a felhasználói térprogram között.

Következtetés

Ebben a cikkben a BPF-et és az eBPF-et általánosságban tárgyaltuk. Tudom, hogy ma rengeteg információ és forrás található az eBPF-ről, ezért ajánlok még néhány anyagot további tanulmányozásra.

Javaslom elolvasásra:

Forrás: will.com