Isang Maikling Panimula sa BPF at eBPF

Hello, Habr! Nais naming ipaalam sa iyo na naghahanda kami ng isang libro para sa pagpapalabas."Linux Observability na may BPF".

Isang Maikling Panimula sa BPF at eBPF
Dahil ang BPF virtual machine ay patuloy na umuunlad at aktibong ginagamit sa pagsasanay, isinalin namin para sa iyo ang isang artikulong naglalarawan sa mga pangunahing kakayahan at kasalukuyang estado nito.

Sa mga nagdaang taon, ang mga tool at teknik sa pagprograma ay lalong naging popular upang mabayaran ang mga limitasyon ng kernel ng Linux sa mga kaso kung saan kinakailangan ang pagproseso ng packet na may mataas na pagganap. Ang isa sa mga pinakasikat na pamamaraan ng ganitong uri ay tinatawag kernel bypass (kernel bypass) at nagbibigay-daan, pag-bypass sa layer ng kernel network, na isagawa ang lahat ng pagproseso ng packet mula sa espasyo ng gumagamit. Ang pag-bypass sa kernel ay nagsasangkot din ng pagkontrol sa network card mula sa espasyo ng gumagamit. Sa madaling salita, kapag nagtatrabaho sa isang network card, umaasa kami sa driver espasyo ng gumagamit.

Sa pamamagitan ng paglilipat ng ganap na kontrol ng network card sa isang user-space program, binabawasan namin ang kernel overhead (paglipat ng konteksto, pagpoproseso ng layer ng network, mga pagkagambala, atbp.), na lubos na mahalaga kapag tumatakbo sa bilis na 10Gb/s o mas mataas. Kernel bypass kasama ang kumbinasyon ng iba pang feature (batch processing) at maingat na pag-tune ng pagganap (NUMA accounting, Paghihiwalay ng CPU, atbp.) ay tumutugma sa mga batayan ng mataas na pagganap ng pagproseso ng network sa espasyo ng gumagamit. Marahil ang isang huwarang halimbawa ng bagong diskarte sa pagpoproseso ng packet ay DPDK mula sa Intel (Data Plane Development Kit), bagama't may iba pang mga kilalang tool at diskarte, kabilang ang Cisco's VPP (Vector Packet Processing), Netmap at, siyempre, snab.

Ang pag-aayos ng mga pakikipag-ugnayan sa network sa espasyo ng gumagamit ay may ilang mga kawalan:

  • Ang OS kernel ay isang abstraction layer para sa mga mapagkukunan ng hardware. Dahil ang mga programa sa espasyo ng gumagamit ay kailangang direktang pamahalaan ang kanilang mga mapagkukunan, kailangan din nilang pamahalaan ang kanilang sariling hardware. Madalas itong nangangahulugan ng pagkakaroon ng programa ng iyong sariling mga driver.
  • Dahil ibinibigay namin nang buo ang espasyo ng kernel, ibinibigay din namin ang lahat ng functionality ng networking na ibinigay ng kernel. Ang mga programa sa espasyo ng gumagamit ay dapat muling ipatupad ang mga tampok na maaaring naibigay na ng kernel o operating system.
  • Gumagana ang mga programa sa sandbox mode, na seryosong nililimitahan ang kanilang pakikipag-ugnayan at pinipigilan ang mga ito sa pagsasama sa ibang bahagi ng operating system.

Sa esensya, kapag nakikipag-network sa espasyo ng gumagamit, ang mga nadagdag sa pagganap ay nakakamit sa pamamagitan ng paglipat ng packet processing mula sa kernel patungo sa espasyo ng gumagamit. Eksaktong kabaligtaran ang ginagawa ng XDP: inililipat nito ang mga programa sa networking mula sa espasyo ng gumagamit (mga filter, solver, pagruruta, atbp.) patungo sa espasyo ng kernel. Binibigyang-daan kami ng XDP na magsagawa ng function ng network sa sandaling tumama ang isang packet sa isang interface ng network at bago ito magsimulang umakyat sa subsystem ng kernel network. Bilang resulta, ang bilis ng pagproseso ng packet ay tumataas nang malaki. Gayunpaman, paano pinapayagan ng kernel ang gumagamit na isagawa ang kanilang mga programa sa espasyo ng kernel? Bago sagutin ang tanong na ito, tingnan natin kung ano ang BPF.

BPF at eBPF

Sa kabila ng nakakalito na pangalan, ang BPF (Berkeley Packet Filtering) ay, sa katunayan, isang modelo ng virtual machine. Ang virtual machine na ito ay orihinal na idinisenyo upang hawakan ang packet filtering, kaya ang pangalan.

Ang isa sa mga pinakatanyag na tool gamit ang BPF ay tcpdump. Kapag kumukuha ng mga packet gamit tcpdump maaaring tukuyin ng user ang isang expression upang i-filter ang mga packet. Tanging mga packet na tumutugma sa expression na ito ang kukunin. Halimbawa, ang expression na "tcp dst port 80” ay tumutukoy sa lahat ng TCP packet na dumarating sa port 80. Maaaring paikliin ng compiler ang expression na ito sa pamamagitan ng pag-convert nito sa BPF bytecode.

$ 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

Ito ang karaniwang ginagawa ng programa sa itaas:

  • Tagubilin (000): Nilo-load ang packet sa offset 12, bilang isang 16-bit na salita, sa accumulator. Ang offset 12 ay tumutugma sa ethertype ng packet.
  • Instruction (001): inihahambing ang value sa accumulator sa 0x86dd, ibig sabihin, sa ethertype value para sa IPv6. Kung totoo ang resulta, ang counter ng programa ay pupunta sa pagtuturo (002), at kung hindi, pagkatapos ay sa (006).
  • Tagubilin (006): inihahambing ang halaga sa 0x800 (ethertype na halaga para sa IPv4). Kung totoo ang sagot, mapupunta ang programa sa (007), kung hindi, sa (015).

At iba pa hanggang sa magbalik ang packet filtering program ng resulta. Ito ay karaniwang isang Boolean. Ang pagbabalik ng di-zero na halaga (tagubilin (014)) ay nangangahulugan na ang packet ay tinanggap, at ang pagbabalik ng isang zero na halaga (tagubilin (015)) ay nangangahulugan na ang packet ay hindi tinanggap.

Ang BPF virtual machine at ang bytecode nito ay iminungkahi nina Steve McCann at Van Jacobson noong huling bahagi ng 1992 nang mailathala ang kanilang papel BSD Packet Filter: Bagong Arkitektura para sa User-Level Packet Capture, unang ipinakita ang teknolohiyang ito sa kumperensya ng Usenix noong taglamig ng 1993.

Dahil ang BPF ay isang virtual machine, tinutukoy nito ang kapaligiran kung saan tumatakbo ang mga programa. Bilang karagdagan sa bytecode, tinutukoy din nito ang batch memory model (ang mga tagubilin sa pag-load ay tahasang inilalapat sa batch), mga rehistro (A at X; accumulator at index registers), scratch memory storage, at isang implicit na program counter. Kapansin-pansin, ang BPF bytecode ay na-modelo pagkatapos ng Motorola 6502 ISA. Tulad ng naalala ni Steve McCann sa kanyang ulat sa plenaryo sa Sharkfest '11, pamilyar siya sa build 6502 mula sa kanyang high school days programming sa Apple II, at ang kaalamang ito ay nakaimpluwensya sa kanyang trabaho sa pagdidisenyo ng BPF bytecode.

Ang suporta ng BPF ay ipinatupad sa Linux kernel sa mga bersyong v2.5 at mas mataas, higit sa lahat ay idinagdag ng mga pagsisikap ni Jay Schullist. Ang BPF code ay nanatiling hindi nabago hanggang 2011, nang muling idisenyo ni Eric Dumaset ang BPF interpreter upang tumakbo sa JIT mode (Source: JIT para sa mga packet filter). Pagkatapos nito, ang kernel, sa halip na bigyang-kahulugan ang BPF bytecode, ay maaaring direktang i-convert ang mga programa ng BPF sa target na arkitektura: x86, ARM, MIPS, atbp.

Nang maglaon, noong 2014, iminungkahi ni Alexey Starovoitov ang isang bagong mekanismo ng JIT para sa BPF. Sa katunayan, ang bagong JIT na ito ay naging isang bagong arkitektura na nakabatay sa BPF at tinawag na eBPF. Sa tingin ko ang parehong mga VM ay magkakasamang umiral sa loob ng ilang panahon, ngunit kasalukuyang ipinapatupad ang packet filtering batay sa eBPF. Sa katunayan, sa maraming halimbawa ng modernong dokumentasyon, ang BPF ay nauunawaan na eBPF, at ang klasikal na BPF ay kilala ngayon bilang cBPF.

Pinapalawak ng eBPF ang klasikong BPF virtual machine sa ilang paraan:

  • Batay sa mga modernong 64-bit na arkitektura. Gumagamit ang eBPF ng mga 64-bit na register at pinapataas ang bilang ng mga available na register mula 2 (accumulator at X) hanggang 10. Nagbibigay din ang eBPF ng mga karagdagang opcode (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Nahiwalay sa network layer subsystem. Ang BPF ay nakatali sa batch data model. Dahil ito ay ginamit para sa packet filtering, ang code nito ay matatagpuan sa subsystem na nagbibigay ng mga komunikasyon sa network. Gayunpaman, ang eBPF virtual machine ay hindi na nakatali sa modelo ng data at maaaring gamitin para sa anumang layunin. Kaya, ngayon ang programa ng eBPF ay maaaring konektado sa tracepoint o kprobe. Binubuksan nito ang paraan sa instrumentasyon ng eBPF, pagsusuri sa pagganap, at marami pang ibang kaso ng paggamit sa konteksto ng iba pang mga kernel subsystem. Ngayon ang eBPF code ay matatagpuan sa sarili nitong landas: kernel/bpf.
  • Mga pandaigdigang tindahan ng data na tinatawag na Maps. Ang mga mapa ay mga key-value store na nagbibigay-daan sa pagpapalitan ng data sa pagitan ng espasyo ng user at kernel space. Nagbibigay ang eBPF ng ilang uri ng mga mapa.
  • Mga pangalawang function. Sa partikular, upang muling isulat ang isang pakete, kalkulahin ang isang checksum, o i-clone ang isang pakete. Ang mga function na ito ay tumatakbo sa loob ng kernel at hindi mga user-space program. Maaari ka ring gumawa ng mga system call mula sa mga programang eBPF.
  • Tapusin ang mga tawag. Ang laki ng programa sa eBPF ay limitado sa 4096 bytes. Ang tampok na tail call ay nagbibigay-daan sa isang eBPF program na ilipat ang kontrol sa isang bagong eBPF program at sa gayon ay lampasan ang limitasyong ito (hanggang sa 32 na mga programa ang maaaring iugnay sa ganitong paraan).

eBPF: halimbawa

Mayroong ilang mga halimbawa para sa eBPF sa Linux kernel source. Available ang mga ito sa samples/bpf/. Upang i-compile ang mga halimbawang ito, ipasok lamang ang:

$ sudo make samples/bpf/

Hindi ako magsusulat ng bagong halimbawa para sa eBPF sa aking sarili, ngunit gagamitin ko ang isa sa mga sample na magagamit sa mga sample/bpf/. Titingnan ko ang ilang bahagi ng code at ipaliwanag kung paano ito gumagana. Bilang halimbawa, pinili ko ang programa tracex4.

Sa pangkalahatan, ang bawat isa sa mga halimbawa sa mga sample/bpf/ ay binubuo ng dalawang file. Sa kasong ito:

  • tracex4_kern.c, naglalaman ng source code na isasagawa sa kernel bilang eBPF bytecode.
  • tracex4_user.c, ay naglalaman ng isang programa mula sa espasyo ng gumagamit.

Sa kasong ito, kailangan nating mag-compile tracex4_kern.c sa eBPF bytecode. Kasalukuyang nasa gcc walang backend para sa eBPF. Sa kabutihang-palad, clang maaaring mag-output ng eBPF bytecode. Makefile gumagamit clang para sa compilation tracex4_kern.c sa object file.

Nabanggit ko sa itaas na ang isa sa mga pinaka-kagiliw-giliw na tampok ng eBPF ay mga mapa. Tinutukoy ng tracex4_kern ang isang mapa:

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 ay isa sa maraming uri ng card na inaalok ng eBPF. Sa kasong ito, ito ay isang hash lamang. Maaaring may napansin ka ring ad SEC("maps"). Ang SEC ay isang macro na ginagamit upang lumikha ng isang bagong seksyon ng isang binary file. Sa totoo lang, sa halimbawa tracex4_kern dalawa pang seksyon ang tinukoy:

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

Binibigyang-daan ka ng dalawang function na ito na tanggalin ang isang entry mula sa mapa (kprobe/kmem_cache_free) at magdagdag ng bagong entry sa mapa (kretprobe/kmem_cache_alloc_node). Ang lahat ng pangalan ng function na nakasulat sa malalaking titik ay tumutugma sa mga macro na tinukoy sa bpf_helpers.h.

Kung itatapon ko ang mga seksyon ng object file, dapat kong makita na ang mga bagong seksyong ito ay tinukoy na:

$ 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

meron din tracex4_user.c, pangunahing programa. Karaniwan, ang programang ito ay nakikinig sa mga kaganapan kmem_cache_alloc_node. Kapag nangyari ang naturang kaganapan, ang katumbas na eBPF code ay isasagawa. Ang code ay nagse-save ng IP attribute ng object sa isang mapa, at ang object ay i-loop sa pangunahing programa. Halimbawa:

$ 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

Paano nauugnay ang isang user space program at isang eBPF program? Sa pagsisimula tracex4_user.c naglo-load ng object file tracex4_kern.o gamit ang function 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;
}

Sa paggawa load_bpf_file Ang mga probe na tinukoy sa eBPF file ay idinaragdag sa /sys/kernel/debug/tracing/kprobe_events. Ngayon nakikinig kami sa mga kaganapang ito at may magagawa ang aming programa kapag nangyari ang mga ito.

$ 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

Ang lahat ng iba pang mga programa sa sample/bpf/ ay nakaayos nang katulad. Palagi silang naglalaman ng dalawang file:

  • XXX_kern.c: programang eBPF.
  • XXX_user.c: pangunahing programa.

Tinutukoy ng programang eBPF ang mga mapa at function na nauugnay sa isang seksyon. Kapag nag-isyu ang kernel ng isang kaganapan ng isang partikular na uri (halimbawa, tracepoint), ang mga nakatali na function ay isinasagawa. Ang mga card ay nagbibigay ng komunikasyon sa pagitan ng kernel program at ng user space program.

Konklusyon

Tinalakay ng artikulong ito ang BPF at eBPF sa mga pangkalahatang tuntunin. Alam kong maraming impormasyon at mapagkukunan tungkol sa eBPF ngayon, kaya magrerekomenda ako ng ilang higit pang mapagkukunan para sa karagdagang pag-aaral

Inirerekumenda kong basahin:

Pinagmulan: www.habr.com

Magdagdag ng komento