Trumpas BPF ir eBPF įvadas

Sveiki, Habr! Informuojame, kad ruošiame išleisti knygą“.Linux stebėjimas su BPF".

Trumpas BPF ir eBPF įvadas
Kadangi BPF virtualioji mašina toliau tobulėja ir yra aktyviai naudojama praktikoje, mes išvertėme jums straipsnį, kuriame aprašomos pagrindinės jos galimybės ir dabartinė būsena.

Pastaraisiais metais programavimo įrankiai ir metodai tapo vis populiaresni, siekiant kompensuoti Linux branduolio apribojimus tais atvejais, kai reikalingas didelio našumo paketų apdorojimas. Vienas iš populiariausių tokio pobūdžio metodų vadinamas branduolio apėjimas (branduolio apėjimas) ir leidžia, apeinant branduolio tinklo sluoksnį, atlikti visą paketų apdorojimą iš vartotojo erdvės. Branduolio aplenkimas taip pat apima tinklo plokštės valdymą iš vartotojo erdvė. Kitaip tariant, dirbdami su tinklo plokšte pasikliaujame tvarkykle vartotojo erdvė.

Perduodami pilną tinklo plokštės valdymą į vartotojo erdvės programą, sumažiname branduolio pridėtines sąnaudas (konteksto perjungimas, tinklo sluoksnio apdorojimas, pertraukimai ir kt.), o tai gana svarbu dirbant 10Gb/s ir didesniu greičiu. Branduolio apėjimas ir kitų funkcijų derinys (paketinis apdorojimas) ir kruopštus veikimo derinimas (NUMA apskaita, CPU izoliacijair kt.) atitinka didelio našumo tinklo apdorojimo vartotojo erdvėje pagrindus. Galbūt pavyzdinis šio naujo požiūrio į paketų apdorojimą pavyzdys yra DPDK iš Intel (Duomenų plano kūrimo rinkinys), nors yra ir kitų gerai žinomų įrankių ir metodų, įskaitant Cisco VPP (Vector Packet Processing), Netmap ir, žinoma, snab.

Tinklo sąveikos organizavimas vartotojo erdvėje turi keletą trūkumų:

  • OS branduolys yra aparatinės įrangos išteklių abstrakcijos sluoksnis. Kadangi naudotojų erdvės programos turi tiesiogiai valdyti savo išteklius, jos taip pat turi valdyti savo aparatinę įrangą. Tai dažnai reiškia, kad turite programuoti savo tvarkykles.
  • Kadangi visiškai atsisakome branduolio vietos, taip pat atsisakome ir visų branduolio teikiamų tinklo funkcijų. Naudotojo erdvės programos turi iš naujo įdiegti funkcijas, kurias jau gali suteikti branduolys arba operacinė sistema.
  • Programos veikia smėlio dėžės režimu, o tai labai apriboja jų sąveiką ir neleidžia joms integruoti su kitomis operacinės sistemos dalimis.

Iš esmės, kai prisijungiate prie tinklo vartotojo erdvėje, našumo padidėjimas pasiekiamas perkeliant paketų apdorojimą iš branduolio į vartotojo erdvę. XDP elgiasi visiškai priešingai: perkelia tinklo programas iš vartotojo erdvės (filtrai, skyrikliai, maršruto parinkimas ir kt.) į branduolio erdvę. XDP leidžia mums atlikti tinklo funkciją, kai tik paketas patenka į tinklo sąsają ir prieš jam pradedant judėti į branduolio tinklo posistemį. Dėl to žymiai padidėja paketų apdorojimo greitis. Tačiau kaip branduolys leidžia vartotojui vykdyti savo programas branduolio erdvėje? Prieš atsakydami į šį klausimą, pažiūrėkime, kas yra BPF.

BPF ir eBPF

Nepaisant klaidinančio pavadinimo, BPF (Berkeley Packet Filtering) iš tikrųjų yra virtualios mašinos modelis. Ši virtuali mašina iš pradžių buvo sukurta paketų filtravimui, todėl ir pavadinimas.

Vienas iš žinomiausių įrankių, naudojančių BPF, yra tcpdump. Užfiksuojant paketus naudojant tcpdump vartotojas gali nurodyti paketų filtravimo išraišką. Bus užfiksuoti tik šią išraišką atitinkantys paketai. Pavyzdžiui, posakis „tcp dst port 80“ reiškia visus TCP paketus, gaunamus per 80 prievadą. Kompiliatorius gali sutrumpinti šią išraišką konvertuodamas į BPF baitinį kodą.

$ 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

Štai ką iš esmės daro aukščiau pateikta programa:

  • Instrukcija (000): įkelia paketą, esantį 12 poslinkyje, kaip 16 bitų žodį, į akumuliatorių. 12 poslinkis atitinka paketo eterinį tipą.
  • Instrukcija (001): lygina reikšmę akumuliatoriuje su 0x86dd, ty su IPv6 etertype reikšme. Jei rezultatas teisingas, tada programos skaitiklis pereina prie nurodymo (002), o jei ne, tada į (006).
  • Instrukcija (006): lygina reikšmę su 0x800 (IPv4 eterio tipo reikšmė). Jei atsakymas teisingas, programa pereina į (007), jei ne, tada į (015).

Ir taip, kol paketų filtravimo programa nepateiks rezultato. Paprastai tai yra loginė vertė. Ne nulinės reikšmės grąžinimas (instrukcija (014)) reiškia, kad paketas buvo priimtas, o nulinės reikšmės grąžinimas (instrukcija (015)) reiškia, kad paketas nebuvo priimtas.

BPF virtualią mašiną ir jos baitinį kodą pasiūlė Steve'as McCannas ir Van Jacobsonas 1992 m. pabaigoje, kai buvo paskelbtas jų straipsnis BSD paketų filtras: nauja vartotojo lygio paketų fiksavimo architektūra, ši technologija pirmą kartą buvo pristatyta Usenix konferencijoje 1993 m. žiemą.

Kadangi BPF yra virtuali mašina, ji apibrėžia aplinką, kurioje veikia programos. Be baito kodo, jis taip pat apibrėžia paketinės atminties modelį (pakrovimo instrukcijos netiesiogiai taikomos paketui), registrus (A ir X; akumuliatoriaus ir indekso registrai), įbrėžimų atminties saugyklą ir numanomą programos skaitiklį. Įdomu tai, kad BPF baito kodas buvo sukurtas pagal Motorola 6502 ISA. Kaip savo knygoje prisiminė Steve'as McCannas plenarinis pranešimas „Sharkfest '11“ jis buvo susipažinęs su build 6502 dar vidurinės mokyklos laikais programuodamas „Apple II“, ir šios žinios turėjo įtakos jo darbui kuriant BPF baitinį kodą.

BPF palaikymas įdiegtas Linux branduolyje 2.5 ir naujesnėse versijose, kurias daugiausia padėjo Jay Schullist. BPF kodas liko nepakitęs iki 2011 m., kai Ericas Dumasetas perkūrė BPF vertėją, kad jis veiktų JIT režimu (Šaltinis: JIT paketų filtrams). Po to branduolys, užuot interpretavęs BPF baitinį kodą, gali tiesiogiai konvertuoti BPF programas į tikslinę architektūrą: x86, ARM, MIPS ir kt.

Vėliau, 2014 m., Aleksejus Starovoitovas pasiūlė naują BPF JIT mechanizmą. Tiesą sakant, ši nauja JIT tapo nauja BPF pagrįsta architektūra ir buvo pavadinta eBPF. Manau, kad kurį laiką abi VM egzistavo kartu, tačiau šiuo metu paketų filtravimas įgyvendinamas remiantis eBPF. Tiesą sakant, daugelyje šiuolaikinės dokumentacijos pavyzdžių BPF suprantamas kaip eBPF, o klasikinis BPF šiandien žinomas kaip cBPF.

eBPF išplečia klasikinę BPF virtualią mašiną keliais būdais:

  • Remiantis modernia 64 bitų architektūra. eBPF naudoja 64 bitų registrus ir padidina galimų registrų skaičių nuo 2 (akumuliatorius ir X) iki 10. eBPF taip pat suteikia papildomų operacijų kodus (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Atskirtas nuo tinklo sluoksnio posistemio. BPF buvo susietas su partijos duomenų modeliu. Kadangi jis buvo naudojamas paketų filtravimui, jo kodas buvo tinklo ryšius teikiančiame posistemyje. Tačiau eBPF virtuali mašina nebėra susieta su duomenų modeliu ir gali būti naudojama bet kokiam tikslui. Taigi, dabar eBPF programą galima prijungti prie tracepoint arba kprobe. Tai atveria kelią eBPF prietaisams, našumo analizei ir daugeliui kitų naudojimo atvejų kitų branduolio posistemių kontekste. Dabar eBPF kodas yra savo kelyje: kernel/bpf.
  • Pasaulinės duomenų saugyklos, vadinamos žemėlapiais. Žemėlapiai yra raktų reikšmių saugyklos, leidžiančios keistis duomenimis tarp vartotojo erdvės ir branduolio erdvės. eBPF siūlo kelių tipų žemėlapius.
  • Antrinės funkcijos. Visų pirma, norėdami perrašyti paketą, apskaičiuoti kontrolinę sumą arba klonuoti paketą. Šios funkcijos veikia branduolio viduje ir nėra vartotojo erdvės programos. Taip pat galite skambinti iš eBPF programų.
  • Baigti skambučius. Programos dydis eBPF yra ribojamas iki 4096 baitų. Uodegos iškvietimo funkcija leidžia eBPF programai perduoti valdymą naujai eBPF programai ir taip apeiti šį apribojimą (šiuo būdu galima susieti iki 32 programų).

eBPF: pavyzdys

Yra keletas eBPF pavyzdžių Linux branduolio šaltiniuose. Juos rasite samples/bpf/. Norėdami sudaryti šiuos pavyzdžius, tiesiog įveskite:

$ sudo make samples/bpf/

Pats nerašysiu naujo pavyzdžio eBPF, o naudosiu vieną iš pavyzdžių, esančių samples/bpf/. Pažiūrėsiu į kai kurias kodo dalis ir paaiškinsiu, kaip jis veikia. Kaip pavyzdį pasirinkau programą tracex4.

Apskritai, kiekvienas iš pavyzdžių/bpf/ pavyzdžių susideda iš dviejų failų. Tokiu atveju:

  • tracex4_kern.c, yra šaltinio kodas, kuris turi būti vykdomas branduolyje kaip eBPF baitinis kodas.
  • tracex4_user.c, yra programa iš vartotojo erdvės.

Šiuo atveju turime kompiliuoti tracex4_kern.c į eBPF baitinį kodą. Šiuo metu yra gcc eBPF nėra backend. Laimei, clang gali išvesti eBPF baitinį kodą. Makefile naudoja clang kompiliavimui tracex4_kern.c į objekto failą.

Jau minėjau, kad viena įdomiausių eBPF funkcijų yra žemėlapiai. tracex4_kern apibrėžia vieną žemėlapį:

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 yra viena iš daugelio eBPF siūlomų kortelių tipų. Šiuo atveju tai tik maiša. Galbūt pastebėjote ir skelbimą SEC("maps"). SEC yra makrokomanda, naudojama naujai dvejetainio failo sekcijai sukurti. Tiesą sakant, pavyzdyje tracex4_kern yra apibrėžti dar du skyriai:

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

Šios dvi funkcijos leidžia ištrinti įrašą iš žemėlapio (kprobe/kmem_cache_free) ir pridėti naują įrašą į žemėlapį (kretprobe/kmem_cache_alloc_node). Visi didžiosiomis raidėmis parašyti funkcijų pavadinimai atitinka makrokomandas, apibrėžtas bpf_helpers.h.

Jei išmessiu objekto failo dalis, turėčiau pamatyti, kad šios naujos sekcijos jau apibrėžtos:

$ 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

Taip pat yra tracex4_user.c, pagrindinė programa. Iš esmės ši programa klausosi įvykių kmem_cache_alloc_node. Kai įvyksta toks įvykis, vykdomas atitinkamas eBPF kodas. Kodas išsaugo objekto IP atributą žemėlapyje, o tada objektas perkeliamas per pagrindinę programą. Pavyzdys:

$ 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

Kaip vartotojo erdvės programa ir eBPF programa yra susijusios? Dėl inicijavimo tracex4_user.c įkelia objekto failą tracex4_kern.o naudojant funkciją 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;
}

Darant load_bpf_file eBPF faile apibrėžti zondai pridedami prie /sys/kernel/debug/tracing/kprobe_events. Dabar mes klausomės šių įvykių ir mūsų programa gali ką nors padaryti, kai jie įvyksta.

$ 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

Visos kitos programos, esančios sample/bpf/, yra panašios struktūros. Juose visada yra du failai:

  • XXX_kern.c: eBPF programa.
  • XXX_user.c: pagrindinė programa.

eBPF programa identifikuoja žemėlapius ir funkcijas, susijusias su skyriumi. Kai branduolys išleidžia tam tikro tipo įvykį (pvz., tracepoint), vykdomos susietos funkcijos. Kortelės užtikrina ryšį tarp branduolio programos ir vartotojo erdvės programos.

išvada

Šiame straipsnyje BPF ir eBPF aptariami bendrais bruožais. Žinau, kad šiandien yra daug informacijos ir išteklių apie eBPF, todėl rekomenduosiu dar keletą išteklių tolesniam tyrimui

Aš rekomenduoju perskaityti:

Šaltinis: www.habr.com

Добавить комментарий