Lühike sissejuhatus BPF-i ja eBPF-i

Tere, Habr! Teavitame, et meil on valmimas raamat "Linuxi jälgimine BPF-iga".

Lühike sissejuhatus BPF-i ja eBPF-i
Kuna BPF virtuaalmasin jätkab evolutsiooni ja seda rakendatakse aktiivselt praktikas, oleme teie jaoks tõlkinud artikli, mis kirjeldab selle põhivõimekusi ja praegust seisundit.

Viimastel aastatel on populaarsust kogunud programmeerimistööriistade komplektid ja tehnikad, mis on mõeldud Linuxi tuuma piirangute kompenseerimiseks olukordades, kus on vaja kõrgjõudlusega paketitöötlust. Üks sellistest populaarsetest tehnikatest on tuuma ümbersõit (kernel bypass), mis võimaldab, mööda minnes tuumast, teostada kogu paketitöötlust kasutajaruumi tasandilt. Tuuma ümbersõit eeldab ka võrkaartide haldamist kasutajaruumi tasandilt. Teisisõnu, võrkaardi kasutamisel toetume me juhenditele kasutajaruumi tasandilt.

Kuna me anname täieliku kontrolli võrkaardi üle kasutajaruumi programmale, vähendame tuuma tööga seotud kulusid (konttekstivahetus, võrktaseme töötlemine, katkestused jne), mis on piisavalt oluline töötades kiirusel 10 Gb/s või rohkem. Tuuma ümbersõit koos teiste võimetega (paketiprosessimine) ja hoolikas jõudluse häälestamine (NUMA arvestus, CPU isoleerimine, jne.) vastavad kõrge jõudlusega võrgu töötlemise aluspõhjadele kasutajaruumi tasandil. Üks näide sellisest uuest paketitöötluse lähenemisviisist on DPDK Inteli poolt (Andmeplaani arenduskits), ehkki ka teisi laialdaselt tuntud tööriistu ja tehnikaid, nagu Cisco VPP (Vector Packet Processing), Netmap ja loomulikult Snabb.

Kasutajaruumi võrgu interaktsioonide korraldamisel on mitmeid puudusi:

  • OS-i tuum on riistvararesursside abstraktsioonitasand. Kuna kasutajaruumi programm ei halda oma ressursse otse, peab ta juhtima ka oma riistvara. Sageli tähendab see vajadust oma juhendite programmeerimise järele.
  • Kuna me loobume täielikult tuumast, loobume ka igasugusest võrgufunktsionaalsusest, mida tuum pakub. Kasutajaruumi programmid peavad uuesti rakendama need funktsioonid, mida tuum või operatsioonisüsteem võib-olla juba pakuvad.
  • Programmid töötavad liivakastirežiimis, mis tõsiselt piirab nende suhtlemisvõimet ning takistab neil integreerimist teiste operatsioonisüsteemi osadega.

Tegelikult saavutatakse võrgus suhtlemisel kasutajaruumi korraldamisel jõudluse tõus pakettide töötlemise edasiviimisega tuumast kasutajaruumi. XDP teeb täpselt vastupidist: viib võrguprogrammid (filtrid, teisendajad, marsruudid jne) kasutajaruumist tuuma. XDP võimaldab meil teostada võrgufunktsiooni kohe, kui paketid jõuavad võrguliidesesse ja enne kui need hakkavad liikuma tuuma võrgu alamsüsteemi. Tulemuseks on pakettide töötlemise kiirus märkimisväärselt suurenenud. Kuidas aga tuum võimaldab kasutajatel oma programme tuumas käivitada? Enne sellele küsimusele vastamist vaatame, mis on BPF.

BPF ja eBPF

Kuigi BPF (Berkeley Packet Filtering) nimi ei pruugi olla kõige arusaadavam – see on tegelikult virtuaalmasina mudel. See virtuaalmasin loodi algselt pakettide filtreerimise jaoks, seega ka nimi.

Üks tuntumaid tööriistu, mis kasutab BPF-i, on tcpdump. Pakettide püüdmise korral tcpdump võib kasutaja määrata filtriavalduse pakettide filtreerimiseks. Püüda saab ainult need paketid, mis vastavad sellele avaldusele. Näiteks avaldus "tcp dst port 80" käsitleb kõiki TCP-pakette, mis jõuavad pordile 80. Kompilaator võib seda avaldust lühendada, muutes selle BPF-i baitkoodiks.

$ 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

See, mida eelnevalt nimetatud programm põhimõtteliselt teeb:

  • Käsk (000): laadib paketi nihkega 12, 16-bitise sõnana akumulaatorisse. Nihke 12 vastab paketi ethetüübile.
  • Käsk (001): võrreldakse akumulaatoris olevat väärtust 0x86dd-ga, see tähendab, et IPv6 ethetüübiväärtusega. Kui tulemus on tõene, siis läheb programmi counter käsule (002), ja kui ei – siis (006).
  • Juhend (006): võrdleb väärtust 0x800 (ethertype-väärtus IPv4 jaoks). Kui vastus on tõene, jätkab programm (007) juurde, muul juhul (015) juurde.

Ja nii edasi, kuni pakettide filtreerimissüsteem tagastab tulemuse. Tüüpiliselt on see boolean. Mitte-null väärtuse tagastamine (juhend (014)) tähendab, et pakett sobib, samas kui null väärtuse tagastamine (juhend (015)) tähendab, et pakett ei sobi.

Virtuaalne BPF-masin ja selle baitkood pakkusid Steve McCan ja Van Jacobson 1992. aasta lõpus, kui ilmus nende artikel BSD pakettide filter: uus arhitektuur pakettide haaramiseks kasutaja tasemel, esmakordselt tutvustati seda tehnoloogiat Usenixi konverentsil talvel 1993.

Kuna BPF on virtuaalne masin, määratleb see keskkonna, kus programmid käivitatakse. Lisaks baitkoodile määratleb see ka mälumudeli pakettide jaoks (laadimisjuhised rakendatakse kaudselt paketile), register A ja X; akumulaator ja indeksi register, skratch-mälu salvestus ja kaudne programmi loenduri. Huvitaval kombel on BPF baitkood mudeldatud Motorola 6502 ISA järgi. Nagu meenutas Steve McCann oma plenaarses ettekandes Sharkfest '11-l, oli ta 6502 kogumisega tuttav juba keskkoolist, kui programmeeris Apple II peal, ja need teadmised mõjutasid tema tööd BPF baitkoodi projekteerimisel.

BPF-i toetamine on rakendatud Linuxi tuumas versioonis v2.5 ja kõrgemates, peamiselt Jay Schullisti jõupingutustega. BPF kood jäi ilma tõsiste muudatusteta kuni 2011. aastani, mil Eric Dumazet ümber tegi BPF tõlgendi töötamiseks JIT-režiimis (Allikas: JIT pakettide filter). Pärast seda võis kernel BPF baitkoodi tõlgendamise asemel otse BPF programme sihtarhitektuuriks, nagu x86, ARM, MIPS jne.

Hiljem 2014. aastal pakkus Aleksei Starovoitov välja uue JIT-mehhanismi BPF jaoks. Tegelikult muutus see uus JIT uue alusena BPF ja sai nimeks eBPF. Arvan, et mõnda aega eksisteerisid mõlemad virtuaalsed masinad kõrvuti, kuid praegu põhineb pakettide filtreerimine eBPF-l. Tegelikult mõistetakse paljudes tänapäevastes dokumentides BPF all eBPF-d, samas kui klassikaline BPF on tänapäeval tuntud kui cBPF.

eBPF laieneb klassikalise BPF virtuaalse masina mõnes osas:

  • Tugineb 64-bit arhitektuuridel. eBPF kasutab 64-bitiseid registreid ja suurendab saadaval olevate registreid arvu kahest (akumulaator ja X) kümneni. eBPF-s on samuti saadaval täiendavad käsud (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Eraldatud võrgualaste süsteemide alamkonstruktsioonist. BPF oli seotud pakettide andmemudeliga. Kuna seda kasutati pakettide filtreerimiseks, oli selle kood seotud süsteemiga, mis haldab võrguühendusi. Kuid eBPF virtuaalne masin ei ole enam andmemudeliga seotud ja seda saab kasutada igasugustel eesmärkidel. Nüüd saab eBPF programmi ühendatud tracepointi või kprobe'iga. See avab tee eBPF instrumenteerimisele, jõudluse analüüsile ja paljudele muudele rakendustele teiste südamiku alamkonstruktsioonide kontekstis. Nüüd asub eBPF kood oma rada: kernel/bpf.
  • Globaalne andmete ladustamine nimega Kaardid. Kaardid on võtme-väärtuse tüüpi ladustamised, mis võimaldavad andmete vahetamist kasutajaruumi ja tuuma ruumi vahel. eBPF-s pakutakse mitut tüüpi kaarte.
  • Abifunktsioonid. Eelkõige paketi ümberkirjutamiseks, kontrollsummat arvutamiseks või paketi kloonimiseks. Need funktsioonid täidetakse tuumas ja ei kuulu kasutajaruumi programmide alla. Lisaks saab eBPF programmide kaudu teostada süsteemi kutsungeid.
  • Lõppkutsed. eBPF programmi suurus on piiratud 4096 baitiga. Lõppkutsumise võimalus võimaldab eBPF programmi suunata uuele eBPF programmile ja seeläbi selle piirangu üle saada (sel viisil saab siduda kuni 32 programmi).

eBPF: näide

Linuxi tuuma allikates on mitmeid näiteid eBPF jaoks. Need on saadaval aadressil samples/bpf/. Nende näidiste kompileerimiseks sisestage lihtsalt:

$ sudo make samples/bpf/

Ma ei hakka uut eBPF näidet ise kirjutama, vaid kasutan ühte näidist, mis on saadaval aadressil samples/bpf/. Vaatan läbi mõned koodilõigud ja selgitan, kuidas see töötab. Näiteks valisin programmi tracex4.

Üldiselt koosneb iga näide aadressil samples/bpf/ kahest failist. Antud juhul:

  • tracex4_kern.c, sisaldab allika koodi, mis peab tuumas töötama eBPF bait-koodina.
  • tracex4_user.c, sisaldab programmi kasutajaruumi.

Sellisel juhul peame selle kompileerima tracex4_kern.c eBPF байт-код. Praegu puudub gcc eBPF jaoks serveripool. Õnneks clang saab genereerida eBPF bytecode. Makefile kasutab clang kompileerimiseks tracex4_kern.c objektifaili.

Mainisin varem, et üks eBPF-i huvitavamaid funktsioone on kaardid. tracex4_kern määratleb ühe kaardi:

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 – üks paljusid eBPF-i pakutavaid kaarti tüüpe. Käesoleval juhul on see lihtsalt hash. Samuti olete võinud märgata kuulutust SEC("maps"). SEC on makro, mida kasutatakse uue jaos loomiseks binaarfailis. Tegelikult määratletakse näites tracex4_kern veel kaks jaotust:

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;

    \\ saame kmem_cache_alloc_node() kutsuja IP-aadressi 
    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;
}   

Need kaks funktsiooni võimaldavad kaardilt kustutada kirje (kprobe/kmem_cache_free) ja lisada kaardile uus kirje (kretprobe/kmem_cache_alloc_node). Kõik suurtähtedega kirjutatud funktsioonide nimed vastavad makrodele, mis on määratletud bpf_helpers.h.

Kui ma prindin objekti faili sektsioonid, peaksin nägema, et need uued sektsioonid on juba määratletud:

$ objdump -h tracex4_kern.o

tracex4_kern.o: faili formaat elf64-little

Sektsioonid:
Idx Nimi Suurus VMA LMA Faili off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
SISU, ALLOC, LAADIDA, LOE KESKMINE, KOOD
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
SISU, ALLOC, LAADIDA, RELOC, LOE KESKMINE, KOOD
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
SISU, ALLOC, LAADIDA, RELOC, LOE KESKMINE, KOOD
3 kaardid 0000001c 0000000000000000 0000000000000000 00000148 2**2
SISU, ALLOC, LAADIDA, ANDMED
4 litsents 00000004 0000000000000000 0000000000000000 00000164 2**0
SISU, ALLOC, LAADIDA, ANDMED
5 versioon 00000004 0000000000000000 0000000000000000 00000168 2**2
SISU, ALLOC, LAADIDA, ANDMED
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
SISU, ALLOC, LAADIDA, RELOC, LOE KESKMINE, ANDMED

Seal on ka tracex4_user.c, peamine programm. Põhimõtteliselt kuulab see programm sündmusi kmem_cache_alloc_node. Kui selline sündmus toimub, käivitub vastav eBPF kood. Kood salvestab objekti IP-attribuudi kaardile ja seejärel väljastatakse see objekt peamise programmi kaudu päripäeva. Näide:

$ sudo .\/tracex4
obj 0xffff8d6430f60a00 on 2 sekundi vana, suleti ip-lt ffffffff9891ad90
obj 0xffff8d6062ca5e00 on 23 sekundi vana, suleti ip-lt ffffffff98090e8f
obj 0xffff8d5f80161780 on 6 sekundi vana, suleti ip-lt ffffffff98090e8f

Kuidas on seotud kasutaja ruumi programm ja eBPF programm? Alguses tracex4_user.c laeb objekti faili tracex4_kern.o NtUnmapViewOfSection lae_bpf_fail.

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

Teostamisel lae_bpf_fail sonde, mis on määratletud eBPF failis, lisatakse /sys/kernel/debug/tracing/kprobe_events. Nüüd kuuleme neid sündmusi ja meie programm võib midagi teha, kui need juhtuvad.

$ 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

Kõik ülejäänud programmid sample/bpf/ on üles ehitatud sarnaselt. Igas neist on alati kaks faili:

  • XXX_kern.c: eBPF programm.
  • XXX_user.c: peaprogramm.

eBPF programm defineerib kaardid ja funktsioonid, mis on seotud sektsiooniga. Kui tuumik väljastab teatud tüüpi sündmuse (nt tracepoint), siis käivitatakse seotud funktsioonid. Kaardid võimaldavad andmete vahetamist tuumaprogrammi ja kasutajaruumi programmi vahel.

Kokkuvõte

Selles artiklis vaadeldi BPF-i ja eBPF-i üldiselt. Ma tean, et tänapäeval on palju teavet ja ressursse eBPF-i kohta, seetõttu soovitan veel mõned materjalid edasiseks uurimiseks

Soovitan lugeda:

Allikas: habr.com

Osta usaldusväärne veebimajutus DDoS-kaitsega veebisaitidele, VPS VDS serverid 🔥 Osta usaldusväärne veebimajutus DDoS-kaitsega veebisaitidele, VPS VDS serverid - ProHoster