Lühike sissejuhatus BPF-i ja eBPF-i

Tere Habr! Anname teada, et valmistume välja andma raamatut "Linuxi jälgitavus BPF-iga".

Lühike sissejuhatus BPF-i ja eBPF-i
Kuna BPF-i virtuaalmasin areneb edasi ja seda kasutatakse aktiivselt praktikas, oleme tõlkinud teile artikli, mis kirjeldab selle põhifunktsioone ja hetkeseisu.

Viimastel aastatel on populaarsust kogunud programmeerimisriistad ja -tehnikad, mis kompenseerivad Linuxi kerneli piiranguid juhtudel, kui on vaja suure jõudlusega paketitöötlust. Üks populaarsemaid selliseid meetodeid nimetatakse südamiku ümbersõit (kerneli ümbersõit) ja võimaldab tuuma võrgukihi vahele jättes teostada kogu pakettide töötlemise kasutajaruumist. Kernelist möödasõit hõlmab ka võrgukaardi haldamist kasutaja ruum. Teisisõnu, võrgukaardiga töötades toetume draiverile kasutaja ruum.

Võrgukaardi täieliku juhtimise üle andes kasutajaruumi programmile, vähendame kerneli üldkulusid (kontekstilülitid, võrgukihi töötlemine, katkestused jne), mis on üsna oluline, kui töötab kiirusel 10 Gb / s või kõrgemale. Kernelist ja muude funktsioonide kombinatsioonist mööda hiilimine (partii töötlemine) ja hoolikas jõudluse häälestamine (NUMA raamatupidamine, CPU isolatsioonjne) sobivad suure jõudlusega kasutajaruumi võrgunduse põhitõdedega. Võib-olla on selle uue lähenemisviisi eeskujulik näide pakettide töötlemisel DPDK Intelilt (Andmetasandi arenduskomplekt), kuigi on ka teisi tuntud tööriistu ja tehnikaid, sealhulgas Cisco VPP (Vector Packet Processing), Netmap ja loomulikult näppama.

Võrgu interaktsioonide korraldamisel kasutajaruumis on mitmeid puudusi:

  • OS-i tuum on riistvararessursside abstraktsioonikiht. Kuna kasutajaruumi programmid peavad oma ressursse otse haldama, peavad nad haldama ka oma riistvara. See tähendab sageli oma draiverite programmeerimist.
  • Kuna me loobume täielikult kerneli ruumist, siis loobume ka kõigist tuuma pakutavatest võrgufunktsioonidest. Kasutajaruumi programmid peavad uuesti juurutama funktsioone, mida kernel või operatsioonisüsteem võib juba pakkuda.
  • Programmid töötavad liivakastirežiimis, mis piirab tõsiselt nende suhtlust ja takistab nende integreerumist operatsioonisüsteemi muude osadega.

Sisuliselt saavutatakse kasutajaruumis võrgu loomisel jõudluse kasv pakettide töötlemise liigutamisega tuumast kasutajaruumi. XDP teeb täpselt vastupidist: see viib võrguprogrammid kasutajaruumist (filtrid, muundurid, marsruutimine jne) kerneli piirkonda. XDP võimaldab meil täita võrgufunktsiooni kohe, kui pakett jõuab võrguliidese ja enne kui see hakkab kerneli võrgu alamsüsteemi liikuma. Selle tulemusena suureneb oluliselt pakettide töötlemise kiirus. Kuidas aga võimaldab kernel kasutajal oma programme tuumaruumis käivitada? Enne sellele küsimusele vastamist vaatame, mis on BPF.

BPF ja eBPF

Vaatamata mitte täiesti selgele nimele on BPF (Packet Filtering, Berkeley) tegelikult virtuaalse masina mudel. See virtuaalmasin oli algselt mõeldud pakettide filtreerimiseks, sellest ka nimi.

Üks tuntumaid BPF-i kasutavaid tööriistu on tcpdump. Pakkide hõivamisel tcpdump kasutaja saab määrata pakettfiltrimise avaldise. Jäädvustatakse ainult paketid, mis vastavad sellele väljendile. Näiteks väljend "tcp dst port 80” viitab kõigile pordi 80 kaudu saabuvatele TCP-pakettidele. Kompilaator saab seda avaldist lühendada, teisendades selle BPF-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 on põhimõtteliselt see, mida ülaltoodud programm teeb:

  • Juhend (000): laadib paketi nihkega 12 16-bitise sõnana akumulaatorisse. Nihe 12 vastab paketi eetritüübile.
  • Juhend (001): võrdleb akumulaatori väärtust 0x86dd-ga, st IPv6 eetritüübi väärtusega. Kui tulemus on tõene, läheb programmiloendur käsule (002) ja kui ei, siis (006).
  • Juhend (006): võrdleb väärtust 0x800-ga (ethertype väärtus IPv4 jaoks). Kui vastus on tõene, läheb programm punktile (007), kui ei, siis (015).

Ja nii edasi, kuni pakettide filtreerimise programm tagastab tulemuse. Tavaliselt on see tõeväärtus. Nullist erineva väärtuse tagastamine (juhis (014)) tähendab, et pakett sobis, ja nulli tagastamine (juhis (015)) tähendab, et pakett ei ühti.

BPF-i virtuaalmasina ja selle baitkoodi pakkusid välja Steve McCann ja Van Jacobson 1992. aasta lõpus, kui nende paber ilmus. BSD paketifilter: uus arhitektuur kasutajatasemel pakettide hõivamiseks, esimest korda esitleti seda tehnoloogiat Usenixi konverentsil 1993. aasta talvel.

Kuna BPF on virtuaalne masin, määrab see keskkonna, milles programmid töötavad. Lisaks baitkoodile määratleb see ka pakettmälu mudeli (paketile rakendatakse kaudselt laadimisjuhised), registrid (A ja X; aku- ja indeksiregistrid), kraapimismälu salvestusruumi ja kaudse programmiloenduri. Huvitaval kombel modelleeriti BPF baitkood Motorola 6502 ISA järgi. Nagu Steve McCann omas meenutas täiskogu raport Sharkfestil '11 tundis ta build 6502 juba keskkoolist Apple II-l programmeerimisel ja need teadmised mõjutasid tema tööd BPF baitkoodi kujundamisel.

BPF-i tugi on juurutatud Linuxi tuumas versioonis v2.5 ja uuemates versioonides, mille on lisanud peamiselt Jay Schullist. BPF-i kood jäi muutumatuks kuni 2011. aastani, mil Eric Dumaset kujundas BPF-i tõlgi ümber JIT-režiimis töötamiseks (Allikas: JIT pakettfiltrite jaoks). Pärast seda saab kernel BPF-i baitkoodi tõlgendamise asemel teisendada BPF-programmid otse sihtarhitektuuriks: x86, ARM, MIPS jne.

Hiljem, 2014. aastal, pakkus Aleksei Starovoitov BPF-i jaoks välja uue JIT-mehhanismi. Tegelikult sai sellest uuest JIT-st uus arhitektuur, mis põhineb BPF-il ja seda kutsuti eBPF-iks. Ma arvan, et mõlemad VM-id eksisteerisid mõnda aega koos, kuid pakettide filtreerimine on praegu rakendatud eBPF-i peale. Tegelikult nimetatakse paljudes kaasaegsetes dokumentatsiooninäidetes BPF-i eBPF-iks ja klassikalist BPF-i tuntakse tänapäeval kui cBPF-i.

eBPF laiendab klassikalist BPF-i virtuaalmasinat mitmel viisil:

  • Toetub kaasaegsetele 64-bitistele arhitektuuridele. eBPF kasutab 64-bitisi registreid ja suurendab saadaolevate registrite arvu kahelt (akumulaator ja X) 2-ni. eBPF pakub ka täiendavaid opkoode (BPF_MOV, BPF_JNE, BPF_CALL…).
  • Eraldatud võrgukihi alamsüsteemist. BPF oli seotud partii andmemudeliga. Kuna seda kasutati pakettide filtreerimiseks, oli selle kood võrgu interaktsiooni pakkuvas alamsüsteemis. eBPF-i virtuaalmasin ei ole aga enam seotud andmemudeliga ja seda saab kasutada mis tahes eesmärgil. Nüüd saab eBPF programmi ühendada tracepointi või kprobe'iga. See avab ukse eBPF-i instrumentidele, jõudlusanalüüsile ja paljudele muudele kasutusjuhtudele teiste kerneli alamsüsteemide kontekstis. Nüüd asub eBPF-kood omal teel: kernel/bpf.
  • Ülemaailmsed andmehoidlad nimega Maps. Kaardid on võtmeväärtuste hoidlad, mis pakuvad andmevahetust kasutajaruumi ja kerneli ruumi vahel. eBPF pakub mitut tüüpi kaarte.
  • Sekundaarsed funktsioonid. Eelkõige paketi ülekirjutamiseks, kontrollsumma arvutamiseks või paketi kloonimiseks. Need funktsioonid töötavad kernelis ja ei kuulu kasutajaruumi programmidesse. Lisaks saab eBPF programmidest teha süsteemikõnesid.
  • Kõnede lõpetamine. Programmi suurus eBPF-is on piiratud 4096 baidiga. Kõne lõpetamise funktsioon võimaldab eBPF-i programmil juhtimise üle kanda uuele eBPF-programmile ja seega sellest piirangust mööda minna (sel viisil saab aheldada kuni 32 programmi).

eBPF näide

Linuxi kerneli allikates on eBPF-i kohta mitu näidet. Need on saadaval aadressil samples/bpf/. Nende näidete koostamiseks tippige lihtsalt:

$ sudo make samples/bpf/

Ise ma eBPF-i uut näidet ei kirjuta, vaid kasutan ühte samples/bpf/-s saadaolevatest näidistest. Vaatan mõnda koodi osa ja selgitan, kuidas see töötab. Näitena valisin programmi tracex4.

Üldiselt koosneb iga näites samples/bpf/ kahest failist. Sel juhul:

  • tracex4_kern.c, sisaldab lähtekoodi, mis käivitatakse tuumas eBPF baitkoodina.
  • tracex4_user.c, sisaldab programmi kasutajaruumist.

Sel juhul peame koostama tracex4_kern.c eBPF baitkoodiks. Hetkel sisse gcc eBPF-i jaoks pole serveriosa. Õnneks clang suudab toota eBPF baitkoodi. Makefile kasutab clang koostama tracex4_kern.c objektifaili.

Mainisin eespool, et eBPF-i üks 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 on üks paljudest eBPF-i pakutavatest kaarditüüpidest. Sel juhul on see lihtsalt räsi. Võib-olla olete ka reklaami märganud SEC("maps"). SEC on makro, mida kasutatakse binaarfaili uue jaotise loomiseks. Tegelikult näites tracex4_kern määratletakse veel kaks osa:

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

Need kaks funktsiooni võimaldavad teil eemaldada kaardilt kirje (kprobe/kmem_cache_free) ja lisage kaardile uus kirje (kretprobe/kmem_cache_alloc_node). Kõik suurtähtedega kirjutatud funktsioonide nimed vastavad defineeritud makrodele bpf_helpers.h.

Kui ma tühjendan objektifaili jaotised, peaksin nägema, et need uued jaotised on juba määratletud:

$ 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

On olemas ka tracex4_user.c, põhiprogramm. Põhimõtteliselt kuulab see programm sündmusi kmem_cache_alloc_node. Sellise sündmuse toimumisel käivitatakse vastav eBPF kood. Kood salvestab objekti IP-atribuudi kaardile ja seejärel suunatakse objekt läbi põhiprogrammi. Näide:

$ 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

Kuidas on seotud kasutajaruumi programm ja eBPF programm? Initsialiseerimisel tracex4_user.c laadib objektifaili tracex4_kern.o funktsiooni kasutades 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;
}

Tehes load_bpf_file eBPF-failis määratletud sondid lisatakse /sys/kernel/debug/tracing/kprobe_events. Nüüd kuulame neid sündmusi ja meie programm saab midagi ära 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 teised proovi/bpf/ programmid on üles ehitatud sarnaselt. Need sisaldavad alati kahte faili:

  • XXX_kern.c: eBPF programm.
  • XXX_user.c: põhiprogramm.

Programm eBPF määratleb jaotisega seotud kaardid ja funktsioonid. Kui kernel väljastab teatud tüüpi sündmuse (näiteks tracepoint), täidetakse seotud funktsioonid. Kaardid pakuvad sidet kerneli programmi ja kasutajaruumi programmi vahel.

Järeldus

Selles artiklis käsitleti BPF-i ja eBPF-i üldiselt. Tean, et täna on eBPF-i kohta palju teavet ja ressursse, seega soovitan edasiseks uurimiseks veel mõnda materjali.

Soovitan lugeda:

Allikas: www.habr.com

Lisa kommentaar