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

Përshëndetje, Habr! Po ju bëjmë me dije që po publikohet libri "Vëzhgimi i Linux-it me BPF".

Një hyrje e shkurtër në BPF dhe eBPF
Duke pasur parasysh se makineria virtuale BPF vazhdon të evoluojë dhe aplikohet aktivisht në praktikë, kemi përkthyer për ju një artikull që përshkruan aftësitë e saj kryesore dhe gjendjen aktuale.

Në vitet e fundit, janë bërë gjithnjë e më të njohura mjetet e programimit dhe teknikat që synojnë të kompensojnë kufizimet e bërthamës Linux në raste kur kërkohet përpunim i lartë i paketave. Një nga teknikat më të njohura të këtij lloji quhet anashkalimi i bërthamës (kernel bypass) dhe lejon që, duke anashkaluar nivelin rrjetit të bërthamës, të bëhet e gjithë përpunimi i pakove nga hapësira e përdoruesit. Anashkalimi i bërthamës gjithashtu nënkupton menaxhimin e kartës rrjetit nga hapësira e përdoruesit. Me fjalë të tjera, kur punojmë me kartën rrjetit, ne mbështetemi në pilotin hapësira e përdoruesit.

Duke kontrollin e plotë mbi kartën e rrjetit në programin nga hapësira e përdoruesit, ne reduktojmë kostot që lidhen me funksionimin e bërthamës (ndryshimi i kontekstit, përpunimi i nivelit të rrjetit, ndërprerjet, etj.), e cila është shumë e rëndësishme kur punojmë në shpejtësi prej 10Gb/s ose më të larta. Shmangia e bërthamës plus kombinimi i mundësive të tjera (procesimi i grupit) dhe rregullimi i kujdesshëm i performancës (shqyrtimi i NUMA, izolimi i CPU, etj.) i përputhen bazeve të përpunimit të rrjetit me performancë të lartë në hapësirën e përdoruesit. Një shembull i shkëlqyer i një qasje të tillë për përpunimin e paketave është DPDK nga Intel (Data Plane Development Kit), megjithatë, ekzistojnë edhe instrumente e metoda të tjera të njohura gjerësisht, midis të cilave VPP nga Cisco (Vector Packet Processing), Netmap dhe, natyrisht, Snabb.

Organizimi i ndërveprimeve të rrjetit në hapësirën e përdoruesit ka një mori disavantazhesh:

  • Baza e OS Ă«shtĂ« niveli i abstraksionit pĂ«r burimet harduerike. TĂ« dhĂ«nat e hapĂ«sirĂ«s sĂ« pĂ«rdoruesit duhet tĂ« menaxhojnĂ« burimet e tyre drejtpĂ«rdrejt, kĂ«shtu qĂ« ata gjithashtu duhet tĂ« menaxhojnĂ« pajisjet e tyre harduerike. Shpesh, kjo do tĂ« thotĂ« nevoja pĂ«r tĂ« programuar drejtuesit e tyre.
  • Duke hequr dorĂ« plotĂ«sisht nga hapĂ«sira e bĂ«rthamĂ«s, ne gjithashtu heqim dorĂ« nga tĂ« gjitha funksionalitetet rrjetit tĂ« ofruara nga bĂ«rthama. Programet nĂ« hapĂ«sirĂ«n e pĂ«rdoruesit duhet tĂ« zbatojnĂ« sĂ«rish ato funksione qĂ«, ndoshta, ofrohen tashmĂ« nga bĂ«rthama ose sistemi operativ.
  • Programet punojnĂ« nĂ« mĂ«nyrĂ« tĂ« izoluar, e cila kufizon ndjeshĂ«m mundĂ«sitĂ« e bashkĂ«punimit dhe e pengon ato tĂ« integrohen me pjesĂ« tĂ« tjera tĂ« sistemit operativ.

Në thelb, kur organizoni ndërveprimet rrjet në hapësirën e përdoruesit, përmirësimi i performancës arrihet duke zhvendosur përpunimin e paketave nga bërthama në hapësirën e përdoruesit. XDP bën pikërisht të kundërtën: zhvendos programet rrjet në hapësirën e bërthamës (filtra, konvertues, ruter etj.). XDP na lejon të realizojmë funksionin rrjetor sapo paketa arrin në ndërfaqen rrjet dhe para se të fillojë lëvizjen up në nën-sistemin rrjetor të bërthamës. Si rezultat, shpejtësia e përpunimit të paketave rritet ndjeshëm. Por, si e lejon bërthama përdoruesin 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 Paketave, Berkley) – kjo, nĂ« fakt, Ă«shtĂ« njĂ« model makinerie virtuale. Kjo makineri virtuale Ă«shtĂ« ndĂ«rtuar fillimisht pĂ«r tĂ« trajtuar filtrimin e paketave, ndaj dhe emri.

NjĂ« prej mjeteve mĂ« tĂ« njohura qĂ« pĂ«rdor BPF Ă«shtĂ« tcpdump. NdĂ«rsa kapet paketat me anĂ« tĂ« tcpdump PĂ«rdoruesi mund tĂ« pĂ«rcaktojĂ« njĂ« shprehje pĂ«r filtrimin e paketeve. VetĂ«m pakete qĂ« pĂ«rputhen me kĂ«tĂ« shprehje do tĂ« kapen. PĂ«r shembull, shprehja “tcp dst port 80” ka tĂ« bĂ«jĂ« me tĂ« gjitha pakot TCP qĂ« vijnĂ« nĂ« portin 80. Kompilatori mund ta shkurtĂ«zojĂ« kĂ«tĂ« shprehje duke e transformuar nĂ« kodin bajt 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

Kjo është, në parim, ajo që bën programi i mësipërm:

  • Instruksioni (000): ngarkon paketĂ«n nga ofseti 12, nĂ« formĂ«n e njĂ« fjale 16-bit nĂ« akumulator. Ofseti 12 pĂ«rputhet me ethertype tĂ« paketĂ«s.
  • Instruksioni (001): krahasohet vlera nĂ« akumulator me 0x86dd, domethĂ«nĂ«, me vlerĂ«n ethertype pĂ«r IPv6. NĂ«se rezultati Ă«shtĂ« true, numĂ«ruesi i programit kalon nĂ« instruksionin (002), nĂ«se jo – nĂ« (006).
  • Instruksioni (006): krahasohet vlera me 0x800 (vlera ethertype pĂ«r IPv4). NĂ«se pĂ«rgjigjja Ă«shtĂ« true, programi kalon nĂ« (007), nĂ«se jo – nĂ« (015).

Dhe kështu me radhë, derisa programi i filtrimit të paketeve të kthejë rezultat. Zakonisht, kjo është një boolean. Kthimi i një vlere jo zero (instruksioni (014)) do të thotë se paketa kalon, ndërsa kthimi i zeros (instruksioni (015)) do të thotë se paketa nuk kalon.

Makinë virtuale BPF dhe kodi i saj byte u propozua nga Steve McCanne dhe Van Jacobson në fund të vitit 1992, kur doli artikulli i tyre. Filtri i paketave BSD: Një arkitekturë e re për kapjen e paketave në nivelin e përdoruesit., teknologjia fillimisht u prezantua në konferencën Usenix në dimrin e vitit 1993.

Duke qenĂ« se BPF Ă«shtĂ« njĂ« makinĂ« virtuale, ajo pĂ«rcakton mjedisin nĂ« tĂ« cilin ekzekutohen programet. PĂ«rveç kodit byte, ajo gjithashtu pĂ«rcakton modelin e memories tĂ« paketĂ«s (instruksionet e ngarkesĂ«s aplikohen nĂ« mĂ«nyrĂ« tĂ« heshtur nĂ« paketĂ«), regjistrat (A dhe X; regjistrat e akumulatorit dhe indeksit), hapĂ«sirĂ«n e memories sĂ« pĂ«rkohshme dhe numĂ«ruesin e heshtur tĂ« programeve. ËshtĂ« interesante se kodi byte i BPF Ă«shtĂ« modeluar sipas Motorola 6502 ISA. Siç e kujton Steve McCanne nĂ« fjalimin e tij tĂ« rĂ«ndĂ«sishĂ«m nĂ« Sharkfest ‘11, ai ishte njohur me pĂ«rbĂ«rjen 6502 qĂ« nga klasa e lartĂ«, kur programonte nĂ« Apple II, dhe kĂ«to njohuri ndihmuan nĂ« punĂ«n e tij pĂ«r projektimin e kodit byte tĂ« BPF.

Mbështetja e BPF është realizuar në bërthamën Linux në versionin v2.5 dhe më lart, e shtuar kryesisht falë përpjekjeve të Jay Shullist. Kodi i BPF mbeti pa ndryshime të mëdha deri në vitin 2011, kur Eric Dumazet ripunoi interpretorin BPF për të punuar në modalitetin JIT (Burimi: JIT për filtra paketash). Pas kësaj, bërthama përveç interpretimit të kodit byte të BPF mund të konvertonte drejtpërdrejt programet BPF për arkitekturën e caktuar: x86, ARM, MIPS, etj.

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

eBPF në disa mënyra zgjerohet mbi makinerinë klasike virtuale BPF:

  • Bazohet nĂ« arkitektura 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 operacione tĂ« tjera (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 me paketĂ«. Duke qenĂ« se ajo pĂ«rdorej pĂ«r filtrimin e paketimeve, kodi i saj ndodhej nĂ« nĂ«n-sistemin qĂ« siguroi ndĂ«rveprimin rrjet. MegjithatĂ«, makina virtuale eBPF nuk Ă«shtĂ« mĂ« e lidhur me modelin e tĂ« dhĂ«nave dhe mund tĂ« pĂ«rdoret pĂ«r qĂ«llime tĂ« ndryshme. Tani, programi eBPF mund tĂ« lidhet me tracepoint ose kprobe. Kjo hap rrugĂ«n pĂ«r instrumentimin e eBPF, analizimin e performancĂ«s dhe shumĂ« mundĂ«si tĂ« tjera nĂ« kontekstin e nĂ«n-sistemeve tĂ« tjera tĂ« kernelit. Tani kodi i eBPF ndodhet nĂ« rrugĂ«n e tij tĂ« vet: kernel/bpf.
  • Depot globale tĂ« dhĂ«nash tĂ« quajtura Kartat. Kartat janĂ« depo tĂ« tipit "çelĂ«s-vlerĂ«", duke mundĂ«suar shkĂ«mbimin e tĂ« dhĂ«nave midis hapĂ«sirĂ«s sĂ« pĂ«rdoruesit dhe hapĂ«sirĂ«s sĂ« kernelit. NĂ« eBPF ofrohen karta disa llojesh.
  • Funksionet ndihmĂ«se. NĂ« veçanti, pĂ«r sovrimin e paketave, llogaritjen e checksum-it ose klonimin e paketave. KĂ«to funksione kryhen brenda kernelit 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 sistemore.
  • Thirrjet finale. MadhĂ«sia e programit nĂ« eBPF Ă«shtĂ« e kufizuar nĂ« 4096 byte. MundĂ«sia e thirrjes finale lejon programit eBPF tĂ« transferojĂ« kontrollin nĂ« njĂ« program tĂ« ri eBPF dhe kĂ«shtu tĂ« anashkalojĂ« kĂ«tĂ« kufizim (nĂ« kĂ«tĂ« mĂ«nyrĂ« mund tĂ« lidhen deri nĂ« 32 programe).

eBPF: shembull

Në burimet e kernelit Linux ka disa shembuj për eBPF. Ato janë të disponueshme në samples/bpf/. Për t'i kompaktuar këto shembuj, thjesht shkruani:

$ sudo make samples/bpf/

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

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

  • tracex4_kern.c, pĂ«rmban kodin burimor qĂ« duhet tĂ« ekzekutohet nĂ« kernel si bytecode eBPF.
  • tracex4_user.c, pĂ«rmban programin nga hapĂ«sira e pĂ«rdoruesit.

Në këtë rast, na nevojitet të kompilojmë tracex4_kern.c në kodin e bajtave eBPF. Aktualisht, gcc mungon një server për eBPF. Fatmirësisht, clang mund të gjenerojë kod bajt eBPF. Makefile përdor clang për kompilimin tracex4_kern.c në një skedar objekti.

Më sipër përmenda se një nga veçoritë më interesante të eBPF janë hartat. tracex4_kern definon 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Ă« ofron eBPF. NĂ« kĂ«tĂ« rast, Ă«shtĂ« thjesht njĂ« hash. Gjithashtu mund tĂ« keni vĂ«nĂ« re shpalljen SEC("maps"). SEC Ă«shtĂ« njĂ« makros qĂ« pĂ«rdoret pĂ«r tĂ« krijuar njĂ« seksion tĂ« ri tĂ« skedarit binar. NĂ« fakt, nĂ« shembullin tracex4_kern definohen 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 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ë fshihen shënimet nga harta (kprobe/kmem_cache_free) dhe shtoni një regjistrim të ri në hartë (kretprobe/kern_cache_alloc_node). Të gjitha emrat e funksioneve, të shkruara me shkronja të mëdha, korrespondon me makrosat e definuara në bpf_helpers.h.

Nëse unë nxjerr një dump të seksioneve të skedarit objekt, duhet të shoh se këto seksione të reja janë tashmë të definuara:

$ objdump -h tracex4_kern.o

tracex4_kern.o: formati i skedarit elf64-little

Seksionet:
Idx Emri Madhësia VMA LMA Off skedari Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
PËRBËRJA, ALLOKIMI, NGLOJ, READONLY, KOD
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
PËRBËRJA, ALLOKIMI, NGLOJ, RIKTHIM, READONLY, KOD
2 kretprobe/kern_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
PËRBËRJA, ALLOKIMI, NGLOJ, RIKTHIM, READONLY, KOD
3 hartat 0000001c 0000000000000000 0000000000000000 00000148 2**2
PËRBËRJA, ALLOKIMI, NGLOJ, TË DHËNAT
4 licenca 00000004 0000000000000000 0000000000000000 00000164 2**0
PËRBËRJA, ALLOKIMI, NGLOJ, TË DHËNAT
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
PËRBËRJA, ALLOKIMI, NGLOJ, TË DHËNAT
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
PËRBËRJA, ALLOKIMI, NGLOJ, RIKTHIM, READONLY, TË DHËNAT

Ka gjithashtu tracex4_user.c, programi kryesor. Në parim, ky program dëgjon ngjarje kmem_cache_alloc_node. Kur ndodh një ngjarje e tillë, ekzekutohet kodi përkatës eBPF. Kodi ruan atributin IP të objektit në hartë, dhe më pas ky objekt printohet ciklikisht në programin kryesor. Shembull:

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

Si janĂ« tĂ« lidhura programi i hapĂ«sirĂ«s sĂ« pĂ«rdoruesit dhe programi eBPF? NĂ« inicializim tracex4_user.c ngarkon skedarin e objektit tracex4_kern.o про ĐżĐŸĐŒĐŸŃ‰Đž Ń„ŃƒĐœĐșцоо ngarko_skedarin_bpf.

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

Me kërkesë ngarko_skedarin_bpf sondat, të përcaktuara në skedarin e eBPF, shtohen në /sys/kernel/debug/tracing/kprobe_events. Tani ne po dëgjojmë këto ngjarje dhe programi ynë mund të bëjë diçka 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 në sample/bpf/ janë struktuar në një mënyrë të ngjashme. Ato gjithmonë kanë dy skeda:

  • 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ë caktuar (p.sh., tracepoint), funksionet e lidhura ekzekutohen. Kartat sigurojnë shkëmbimin e të dhënave midis programit të bërthamës dhe programit të hapësirës së përdoruesit.

Përfundimi

Ky artikull përmbledh në një farë mënyre BPF dhe eBPF. E di që sot ka shumë informacion dhe burime mbi eBPF, prandaj rekomandoj disa materiale për studim të mëtejmë.

Rekomandoj të lexoni:

Burimi: habr.com

Bli njĂ« hosting tĂ« besueshĂ«m pĂ«r faqet me mbrojtje DDoS, VPS VDS serverĂ« đŸ”„ Bli njĂ« hosting tĂ« besueshĂ«m pĂ«r faqet me mbrojtje DDoS, VPS VDS serverĂ« | ProHoster