Një hyrje e shkurtër në BPF dhe eBPF

Përshëndetje, Habr! Po ju njoftojmë se jemi në përgatitje të publikimit të librit "Vëzhgimi i Linux me BPF".

Një hyrje e shkurtër në BPF dhe eBPF
Pasi makina virtuale BPF vazhdon të evoluojë dhe aplikohet aktivisht në praktikë, ne kemi përkthyer për ju një artikull që përshkruan mundësitë e saj kryesore dhe gjendjen aktuale.

Në vitet e fundit, janë bërë popullore mjetet për programim dhe teknikat, të cilat synojnë të kompensojnë kufizimet e bërthamës Linux në rastet kur kërkohet përpunim me performancë të lartë të paketave. Një nga teknikat më të njohura të këtij lloji quhet shkëputja e bërthamës (kernel bypass) dhe lejon, duke kaluar mbi nivelin e rrjetit të bërthamës, të kryejnë të gjithë përpunimin e paketave nga hapësira e përdoruesit. Shkëputja e bërthamës gjithashtu supozon menaxhimin e kartelës së rrjetit nga hapësira e përdoruesit. Me fjalë të tjera, kur punojmë me kartelën e rrjetit, ne mbështetemi në drejtuesin hapësira e përdoruesit.

Duke i dhënë kontrollin e plotë mbi kartelën e rrjetit një programi nga hapësira e përdoruesit, ne zvogëlojmë kostot e shkaktuara nga puna e bërthamës (ndërrimi i kontekstit, përpunimi i nivelit të rrjetit, ndërprerjet etj.), që është mjaft e rëndësishme kur punojmë me shpejtësi 10Gb/s ose më shumë. Shkëputja e bërthamës plus kombinimi i mundësive të tjera (përpunimi i paketave) dhe rregullimi i saktë i performancës (përllogaritja e NUMA, izolimi i CPU, etj.) përkojnë me bazat e përpunimit të rrjetit me performancë të lartë në hapësirën e përdoruesit. Një shembull model i këtij qasjeje të re në përpunimin e paketave është DPDK nga Intel (Data Plane Development Kit), megjithatë, ekzistojnë edhe mjete dhe teknika të tjera të njohura, mes të cilave VPP nga Cisco (Vector Packet Processing), Netmap dhe, sigurisht, Snabb.

Organizimi i ndërveprimeve të rrjetit në hapësirën e përdoruesit ka disa disavantazhe:

  • BĂ«rthama e OS Ă«shtĂ« njĂ« nivel abstraktimi pĂ«r burimet harduerike. Pasi programet e hapĂ«sirĂ«s sĂ« pĂ«rdoruesit duhet tĂ« menaxhojnĂ« burimet e tyre direkt, atyre gjithashtu u duhet tĂ« menaxhojnĂ« harduerin e tyre. Shpesh kjo do tĂ« thotĂ« nevojĂ«n pĂ«r tĂ« programuar drejtuese tĂ« veta.
  • Duke qoftĂ« se ne heqim plotĂ«sisht çdo hapĂ«sirĂ« nĂ« bĂ«rthamĂ«, ne gjithashtu heqim dorĂ« nga tĂ« gjitha funksionet rrjetore qĂ« ofrohen nga bĂ«rthama. Programet nĂ« hapĂ«sirĂ«n e pĂ«rdoruesit duhet tĂ« implementojnĂ« pĂ«rsĂ«ri ato funksione, tĂ« cilat ndoshta, tashmĂ« ofrohen nga bĂ«rthama ose sistemi operativ.
  • Programet punojnĂ« nĂ« modin e kutisĂ« sandbox, gjĂ« qĂ« kufizon ndjeshĂ«m mundĂ«sitĂ« e tyre pĂ«r ndĂ«rveprim dhe pengon integrimin e tyre me pjesĂ« tĂ« tjera tĂ« sistemit operativ.

Praktikisht, në organizimin e ndërveprimeve rrjetore në hapësirën e përdoruesit, rritja e performancës arrihet duke shpërngulur përpunimin e pakove nga bërthama në hapësirën e përdoruesit. XDP e bën të kundërtën: ai shpërngul programet rrjetore nga hapësira e përdoruesit (filtrat, konvertuesit, rrugëzimi, etj.) në fushën e bërthamës. XDP na lejon të kryejmë funksionin rrjetor sapo paketa të arrijë në ndërfaqen rrjetësore dhe përpara se ajo të nisë lëvizjen lart në nën sistemin rrjetor të bërthames. Si rezultat, shpejtësia e përpunimit të pakove rritet ndjeshëm. Por, si e lejon bërthama përdoruesit të ekzekutojë programet e tij në hapësirën e bërthamës? Para se të përgjigjemi në këtë pyetje, le të shqyrtojmë se çfarë është BPF.

BPF dhe eBPF

PavarĂ«sisht emrit tĂ« paqartĂ« BPF (Filtrimi i Pakove, Berkeley) – kjo, nĂ« tĂ« vĂ«rtetĂ«, Ă«shtĂ« njĂ« model virtual makine. Ky model virtual fillimisht u projektua pĂ«r tĂ« pĂ«rpunuar filtrimin e pakove, andaj edhe emri.

Një nga mjetet më të njohura që përdorin BPF është tcpdump. Kur kapen paketa duke përdorur tcpdump përdoruesi mund të vendosë një shprehje për filtrimin e pakove. Do të kapen vetëm paketat që përputhen me këtë shprehje. Për shembull, shprehja "tcp dst port 80" i përket të gjitha paketeve TCP që arrijnë në portin 80. Kompilatori mund ta shkurtëzojë këtë shprehje duke e shndërruar atë në kodin e bajtëve BPF.

$ 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

Kështu që, kjo është në thelb ajo që bën programi i mësipërm:

  • Instruksioni (000): ngarkon paketĂ«n me njĂ« offset prej 12, nĂ« formĂ«n e njĂ« fjalĂ« 16-bitĂ«sh nĂ« akumulator. Offseti 12 pĂ«rputhet me ethertype-in e paketĂ«s.
  • Instruksioni (001): krahasohet vlera nĂ« akumulator me 0x86dd, qĂ« do tĂ« thotĂ«, me vlerĂ«n ethertype pĂ«r IPv6. NĂ«se rezultati Ă«shtĂ« true, atĂ«herĂ« numri i programit kalon nĂ« instruksionin (002), nĂ«se jo – atĂ«herĂ« te (006).
  • UdhĂ«zimi (006): krahason vlerĂ«n me 0x800 (vlera ethrtype pĂ«r IPv4). NĂ«se pĂ«rgjigjja Ă«shtĂ« true, programi kalon te (007), nĂ«se jo – te (015).

Dhe kështu me radhë, derisa programi i filtrimit të paketeve të kthejë një rezultat. Normalisht është një boolean. Kthimi i një vlere jo zero (udhëzimi (014)) do të thotë se paketa përputhet, ndërsa kthimi zero (udhëzimi (015)) do të thotë se paketa nuk përputhet.

Makinën virtuale BPF dhe kodin e saj byte e propozuan Steve McKanna dhe Van Jacobson në fund të vitit 1992, kur doli artikulli i tyre Filtri i paketeve BSD: Një arkitekturë e re për kapjen e paketeve në nivelin e përdoruesit, teknologjia u paraqit për herë të parë në konferencën Usenix gjatë dimrit të vitit 1993.

Duke qenĂ« se BPF Ă«shtĂ« njĂ« makinĂ« virtuale, ajo pĂ«rcakton ambientin ku ekzekutohen programet. PĂ«rveç kodit byte, ajo gjithashtu pĂ«rcakton modelin e memories sĂ« paketave (udhĂ«zimet e ngarkimit zbatohen implikisht nĂ« paketĂ«), regjistrat (A dhe X; regjistrat e akumulatorit dhe indeksit), depozitimin e memories ndihmĂ«se dhe numĂ«ruesin e fshehtĂ« tĂ« programeve. ËshtĂ« interesante se kodi byte i BPF Ă«shtĂ« modeluar sipas ISA tĂ« Motorola 6502. Siç pĂ«rmendi Steve McKanna nĂ« fjalimin e tij plenare nĂ« Sharkfest ‘11, ai ishte njohur me montimin 6502 qĂ« nĂ« klasat e larta, kur programonte nĂ« Apple II, dhe kjo njohuri ndikoi nĂ« punĂ«n e tij pĂ«r dizajnimin e kodit byte tĂ« BPF.

Mbështetja për BPF është implementuar në kernelin Linux në versionin v2.5 dhe më tej, e shtuar kryesisht nga përpjekjet e Jay Shulist. Kodi BPF mbeti pa ndryshime të rëndësishme deri në vitin 2011, kur Eric Dumazet e ristrukturio interpretorin BPF për të punuar në modalitetin JIT (Burimi: JIT për filtrat e paketeve). Pas kësaj, kernelin, përveç interpretimit të kodit byte BPF, mundi të konvertonte direkt programet BPF për arkitekturën e synuar: x86, ARM, MIPS, etj.

Më vonë, në vitin 2014, Alexey Starovoitov propozoi një mekanizëm të ri JIT për BPF. Në fakt, ky JIT i ri u bë një arkitekturë e re mbi bazën e BPF dhe mori emrin eBPF. Mendoj se për një kohë të caktuar të dy makinat virtuale bashkëjetuan, por në ditët e sotme filtrimi i paketave realizohet në bazën e eBPF. Në fakt, në shumë mostra të dokumentacionit modern, nën BPF kuptohet eBPF, ndërsa BPF klasike sot njihet si cBPF.

eBPF në disa aspekte zgjeron makinën virtuale klasike BPF:

  • Bazohet nĂ« arkitekturave moderne 64-bit. eBPF pĂ«rdor regjistrat 64-bit dhe rrit numrin e regjistrave tĂ« disponueshĂ«m nga 2 (akumulatori dhe X) nĂ« 10. NĂ« eBPF gjithashtu ofrohen kode operacioni shtesĂ« (BPF_MOV, BPF_JNE, BPF_CALL
).
  • E shkĂ«putur nga nĂ«n-sistemi i nivelit rrjet. BPF ishte e lidhur me modelin e tĂ« dhĂ«nave tĂ« paketave. Duke qenĂ« se pĂ«rdorej pĂ«r filtrimin e paketimeve, kodi i saj ndodhej nĂ« nĂ«n-sistemin qĂ« mundĂ«sonte ndĂ«rveprimet rrjet. MegjithatĂ«, makina virtuale eBPF nuk Ă«shtĂ« mĂ« e lidhur me modelin e tĂ« dhĂ«nave dhe mund tĂ« pĂ«rdoret pĂ«r çdo qĂ«llim. Tani, programi eBPF mund tĂ« lidhet me njĂ« tracepoint ose me njĂ« kprobe. Kjo hap njĂ« rrugĂ« pĂ«r instrumentimin e eBPF, analizĂ«n e performancĂ«s dhe shumĂ« mundĂ«si tĂ« tjera pĂ«rdorimi nĂ« kontekstin e nĂ«n-sistemeve tĂ« tjera tĂ« bĂ«rthamĂ«s. Tani kodi i eBPF ndodhet nĂ« njĂ« rrugĂ« tĂ« vetĂ«n: kernel/bpf.
  • Depo globale tĂ« dhĂ«nash tĂ« quajtur Hartat. Hartat janĂ« depo tĂ« tipit "çelĂ«s-vlerĂ«", qĂ« mundĂ«sojnĂ« shkĂ«mbimin e tĂ« dhĂ«nave mes hapĂ«sirĂ«s sĂ« pĂ«rdoruesit dhe hapĂ«sirĂ«s sĂ« bĂ«rthamĂ«s. NĂ« eBPF ofrohen harta tĂ« disa tipave.
  • Funksione ndihmĂ«se. NĂ« veçanti, pĂ«r rishkrimin e paketave, llogaritjen e checksum-it ose klonimin e paketave. KĂ«to funksione kryhen brenda bĂ«rthamĂ«s dhe nuk i pĂ«rkasin programeve tĂ« hapĂ«sirĂ«s sĂ« pĂ«rdoruesit. PĂ«r mĂ« tepĂ«r, nga programet e eBPF mund tĂ« kryhen thirrje sistemike.
  • Thirrje pĂ«rfundimtare. MadhĂ«sia e programit nĂ« eBPF Ă«shtĂ« e kufizuar nĂ« 4096 byte. MundĂ«sia e thirrjes pĂ«rfundimtare lejon programit eBPF tĂ« kalojĂ« kontrollin nĂ« njĂ« program tĂ« ri eBPF dhe nĂ« kĂ«tĂ« mĂ«nyrĂ« tĂ« anashkalojĂ« kĂ«tĂ« kufizim (nĂ« kĂ«tĂ« mĂ«nyrĂ« mund tĂ« lidhen deri nĂ« 32 programe).

eBPF: shembull

Në burimet e bërthamës Linux ka disa shembuj për eBPF. Ata janë të disponueshëm në adresën samples/bpf/. Për t'i kompaktuar këta shembuj, thjesht futni:

$ sudo make samples/bpf/

Nuk do të shkruaj unë një shembull të ri për eBPF, por do të përdor një nga modelet e disponueshme në samples/bpf/. Do të shqyrtoj disa pjesë të kodit dhe do të shpjegoj se si funksionon. Si shembull, kam zgjedhur programin tracex4.

Në të vërtetë, secili nga shembujt në samples/bpf/ përbëhet nga dy skedare. Në këtë rast:

  • tracex4_kern.c, pĂ«rmban kodin burimor qĂ« duhet tĂ« ekzekutohet nĂ« bĂ«rthamĂ« si byte-code eBPF.
  • tracex4_user.c, pĂ«rmban programin nga hapĂ«sira e pĂ«rdoruesit.

Në këtë rast, na nevojitet ta kompaktosh tracex4_kern.c në kodin byte eBPF. Aktualisht, gcc mungon një pjesë serveri për eBPF. Fatmirësisht, clang mund të nxjerrë kodin byte eBPF. Makefile përdor clang për kompilim tracex4_kern.c në skedarin objekt.

Më sipër përmenda se një nga karakteristikat më interesante të eBPF janë hartat. tracex4_kern përcakton një hartë:

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 – njĂ« nga shumĂ« lloje hartash qĂ« ofrohen nga eBPF. NĂ« kĂ«tĂ« rast, Ă«shtĂ« thjesht njĂ« hash. Gjithashtu mund tĂ« keni vĂ«nĂ« re shpalljen SEC("maps"). SEC Ă«shtĂ« njĂ« makro e pĂ«rdorur pĂ«r tĂ« krijuar njĂ« seksion tĂ« ri tĂ« skedarit binar. NĂ« fakt, nĂ« shembujt e tracex4_kern pĂ«rcaktohen edhe dy seksione tĂ« tjera:

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;

    // marrim adresën IP të thirrësit të 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;
}   

Këto dy funksione lejojnë të fshihet një regjistër nga harta (kprobe/kmem_cache_free) dhe të shtohet një regjistër të ri në hartë (kretprobe/kmem_cache_alloc_node). Të gjitha emrat e funksioneve të shkruara me shkronja të mëdha përputhen me makrotë e përcaktuara në bpf_helpers.h.

Nëse unë nxjerr në dritë dumpin e seksioneve të skedarit objekt, do të duhet të shoh se këto seksione të reja janë tashmë të përcaktuara:

$ objdump -h tracex4_kern.o

tracex4_kern.o: formati i skedarit elf64-little

Seksionet:
Idx Emri Madhësia VMA LMA Pjesa off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
PËRMBATJA, ALLOKIM, NGJITJE, E FSHTIRË, KOD
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
PËRMBATJA, ALLOKIM, NGJITJE, RIKTHIM, E FSHTIRË, KOD
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
PËRMBATJA, ALLOKIM, NGJITJE, RIKTHIM, E FSHTIRË, KOD
3 harta 0000001c 0000000000000000 0000000000000000 00000148 2**2
PËRMBATJA, ALLOKIM, NGJITJE, DËRGIM
4 licenca 00000004 0000000000000000 0000000000000000 00000164 2**0
PËRMBATJA, ALLOKIM, NGJITJE, DËRGIM
5 versione 00000004 0000000000000000 0000000000000000 00000168 2**2
PËRMBATJA, ALLOKIM, NGJITJE, DËRGIM
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
PËRMBATJA, ALLOKIM, NGJITJE, RIKTHIM, E FSHTIRË, TË DHENAT

Ka gjithashtu tracex4_user.c, programi kryesor. Në thelb, ky program dëgjon ngjarjet kmem_cache_alloc_node.Kur ndodhin një ngjarje e tillë, ekzekutohet kodi përkatës eBPF. Kodi ruan atributin IP të objektit në hartë, dhe pastaj ky objekt përsëritet në programin kryesor. Shembulli:

$ sudo .\/tracex4
obj 0xffff8d6430f60a00 është 2 sekonda e vjetër, u alokua në ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 është 23 sekonda e vjetër, u alokua në ip ffffffff98090e8f
obj 0xffff8d5f80161780 është 6 sekonda e vjetër, u alokua në ip ffffffff98090e8f

Si janë të lidhura programi i hapësirës së përdoruesit dhe programi eBPF? Gjatë inicializimit tracex4_user.c ngarkon skedarin objekt tracex4_kern.o duke përdorur funksionin ngarko_bpf_sked.

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

Në përmbushje ngarko_bpf_sked sondat, të përcaktuara në skedarin eBPF, shtohen në /sys/kernel/debug/tracing/kprobe_events. Tani ne po i dëgjojmë këto ngjarje, dhe programi ynë mund të bëjë diçka në momentin kur ato ndodhin.

$ 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

Të gjitha programet e tjera në sample/bpf/ janë strukturuar në mënyrë të ngjashme. Ato gjithmonë përmbajnë dy skedarë:

  • XXX_kern.c: programi eBPF.
  • XXX_user.c: programi kryesor.

Programi eBPF përcakton karta dhe funksione të lidhura me seksionin. Kur bërthama lëshon një ngjarje të një lloji të caktuar (p.sh., në bërthamë. Në këtë moment, numri iReferencave gjithashtu do të rritet me një dhe ne do të mund të mbyllim deshifruesin e skedarit në programin ngarkues.), funksionet e lidhura ekzekutohen. Karta sigurojnë shkëmbim të të dhënave midis programit të bërthamës dhe programit të hapësirës së përdoruesit.

Përfundim

Në këtë artikull, u shqyrtuan përgjithësisht BPF dhe eBPF. E di që sot ka shumë informacion dhe burime mbi eBPF, prandaj do të rekomandoja disa materiale të tjera për studim të mëtejshëm.

Rekomandoj të lexoni:

Burimi: habr.com

Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Blini hosting tĂ« besueshĂ«m pĂ«r faqe interneti me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster