Mallonga Enkonduko al BPF kaj eBPF

Saluton, Habr! Ni ŝatus informi vin, ke ni preparas libron por liberigo."Linukso Observeblo kun BPF".

Mallonga Enkonduko al BPF kaj eBPF
Ĉar la virtuala maŝino de BPF daŭre evoluas kaj estas aktive uzata en la praktiko, ni tradukis por vi artikolon priskribanta ĝiajn ĉefajn kapablojn kaj nunan staton.

En la lastaj jaroj, programaj iloj kaj teknikoj fariĝis ĉiam pli popularaj por kompensi la limigojn de la Linukso-kerno en kazoj kie alt-efikeca pakaĵprilaborado estas postulata. Unu el la plej popularaj teknikoj de ĉi tiu speco nomiĝas kerno pretervojo (kernel-preterpaso) kaj permesas, preterirante la kernan rettavolon, fari ĉiun pakaĵetan pretigon de uzantspaco. Preterpasi la kernon ankaŭ implikas kontroli la retkarton de uzantspaco. Alivorte, kiam ni laboras kun retkarto, ni fidas je la ŝoforo uzantspaco.

Transdonante plenan kontrolon de la retkarto al uzantspaca programo, ni reduktas kernan superkoston (kunteksta ŝanĝado, rettavola prilaborado, interrompoj, ktp.), kio estas sufiĉe grava kiam funkcias kun rapidoj de 10Gb/s aŭ pli alta. Kernel-pretervojo plus kombinaĵo de aliaj trajtoj (bata prilaborado) kaj zorgema agordado (NUMA kontado, CPU-izolado, ktp.) egalrilatas al la bazaĵoj de alt-efikeca retpretigo en uzantspaco. Eble ekzempla ekzemplo de ĉi tiu nova aliro al paka pretigo estas DPDK de Intel (Datumplana Disvolva Ilaro), kvankam ekzistas aliaj konataj iloj kaj teknikoj, inkluzive de VPP (Vector Packet Processing) de Cisco, Netmap kaj, kompreneble, Snabb.

Organizi retajn interagojn en uzantspaco havas kelkajn malavantaĝojn:

  • La OS-kerno estas abstrakta tavolo por hardvarresursoj. Ĉar uzantspacaj programoj devas rekte administri siajn rimedojn, ili ankaŭ devas administri sian propran aparataron. Ĉi tio ofte signifas devi programi viajn proprajn ŝoforojn.
  • Ĉar ni tute rezignas la kernan spacon, ni ankaŭ rezignas ĉiujn retajn funkciojn provizitajn de la kerno. Uzantspacaj programoj devas reefektigi funkciojn, kiuj eble jam estas disponigitaj de la kerno aŭ operaciumo.
  • Programoj funkcias en sandbox-reĝimo, kiu serioze limigas ilian interagon kaj malhelpas ilin integriĝi kun aliaj partoj de la operaciumo.

Esence, kiam interkonektanta en uzantspaco, rendimentogajnoj estas atingitaj movante pakaĵetpretigon de la kerno al uzantspaco. XDP faras ĝuste la malon: ĝi movas retprogramojn de uzantspaco (filtriloj, solviloj, vojigo, ktp.) en kernspacon. XDP permesas al ni plenumi retan funkcion tuj kiam pako trafas retan interfacon kaj antaŭ ol ĝi komencas supreniri en la kernan retan subsistemon. Kiel rezulto, la pakaĵa pretigrapideco pliiĝas signife. Tamen, kiel la kerno permesas al la uzanto ekzekuti siajn programojn en kernspaco? Antaŭ respondi ĉi tiun demandon, ni rigardu kio estas BPF.

BPF kaj eBPF

Malgraŭ la konfuza nomo, BPF (Berkeley Packet Filtering) estas, fakte, virtuala maŝina modelo. Ĉi tiu virtuala maŝino estis origine dizajnita por pritrakti pakaĵetfiltradon, tial la nomo.

Unu el la plej famaj iloj uzantaj BPF estas tcpdump. Kiam oni kaptas pakojn uzante tcpdump la uzanto povas specifi esprimon por filtri pakaĵetojn. Nur pakaĵetoj kongruantaj kun ĉi tiu esprimo estos kaptitaj. Ekzemple, la esprimo "tcp dst port 80” rilatas al ĉiuj TCP-pakoj alvenantaj sur havenon 80. La kompililo povas mallongigi ĉi tiun esprimon konvertante ĝin al BPF-bajtokodo.

$ 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

Jen kion la supra programo esence faras:

  • Instrukcio (000): Ŝarĝas la pakaĵeton ĉe ofseto 12, kiel 16-bita vorto, en la akumulilon. Ofseto 12 egalrilatas al la eterspeco de la pakaĵeto.
  • Instrukcio (001): komparas la valoron en la akumulilo kun 0x86dd, tio estas, kun la etertipvaloro por IPv6. Se la rezulto estas vera, tiam la programkalkulilo iras al instrukcio (002), kaj se ne, tiam al (006).
  • Instrukcio (006): komparas la valoron kun 0x800 (etertipa valoro por IPv4). Se la respondo estas vera, tiam la programo iras al (007), se ne, tiam al (015).

Kaj tiel plu ĝis la paka filtra programo resendas rezulton. Ĉi tio estas kutime Bulea. Resendi ne-nulan valoron (instrukcio (014)) signifas ke la pakaĵeto estis akceptita, kaj resendi nulan valoron (instrukcio (015)) signifas ke la pakaĵeto ne estis akceptita.

La BPF virtuala maŝino kaj ĝia bajtokodo estis proponitaj fare de Steve McCann kaj Van Jacobson malfrue en 1992 kiam ilia artikolo estis publikigita. BSD Paka Filtrilo: Nova Arkitekturo por Uzantnivela Paka Kaptado, tiu teknologio unue estis prezentita ĉe la Usenix-konferenco en la vintro de 1993.

Ĉar BPF estas virtuala maŝino, ĝi difinas la medion en kiu programoj funkcias. Aldone al la bajtokodo, ĝi ankaŭ difinas la aran memormodelon (ŝarĝinstrukciaĵo estas implicite aplikitaj al la aro), registrojn (A kaj X; akumulilo kaj indeksregistroj), gratmemorstokadon, kaj implican programkalkulilon. Interese, la BPF-bajtokodo estis modeligita post la Motorola 6502 ISA. Kiel Steve McCann memoris en sia plena raporto ĉe Sharkfest '11, li konis konstruon 6502 de sia mezlerneja tempo programado sur la Apple II, kaj tiu scio influis lian laboron dizajnante la BPF-bajtkodon.

BPF-subteno estas efektivigita en la Linukso-kerno en versioj v2.5 kaj pli altaj, aldonitaj ĉefe per la klopodoj de Jay Schullist. La BPF-kodo restis senŝanĝa ĝis 2011, kiam Eric Dumaset restrukturis la BPF-interpretilon por funkcii en JIT-reĝimo (Fonto: JIT por pakaj filtriloj). Post tio, la kerno, anstataŭe de interpretado de BPF-bajtkodo, povis rekte konverti BPF-programojn al la cela arkitekturo: x86, ARM, MIPS, ktp.

Poste, en 2014, Alexey Starovoitov proponis novan JIT-mekanismon por BPF. Fakte, ĉi tiu nova JIT iĝis nova BPF-bazita arkitekturo kaj estis nomita eBPF. Mi pensas, ke ambaŭ VM-oj kunekzistis dum iom da tempo, sed nuntempe paka filtrado estas efektivigita surbaze de eBPF. Fakte, en multaj ekzemploj de moderna dokumentaro, BPF estas komprenita kiel eBPF, kaj klasika BPF hodiaŭ estas konata kiel cBPF.

eBPF etendas la klasikan BPF virtualan maŝinon laŭ pluraj manieroj:

  • Surbaze de modernaj 64-bitaj arkitekturoj. eBPF uzas 64-bitajn registrojn kaj pliigas la nombron da disponeblaj registroj de 2 (akumulilo kaj X) ĝis 10. eBPF ankaŭ disponigas kromajn opkodojn (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Dekroĉite de la retotavola subsistemo. BPF estis ligita al la ardatummodelo. Ĉar ĝi estis uzita por pakaĵeta filtrado, ĝia kodo situis en la subsistemo kiu disponigas retajn komunikadojn. Tamen, la virtuala maŝino eBPF ne plu estas ligita al la datummodelo kaj povas esti uzata por iu ajn celo. Do, nun la programo eBPF povas esti konektita al spurpunkto aŭ kprobe. Ĉi tio malfermas la vojon al eBPF-instrumentado, efikeco-analizo, kaj multaj aliaj uzkazoj en la kunteksto de aliaj kernaj subsistemoj. Nun la eBPF-kodo situas en sia propra vojo: kernel/bpf.
  • Tutmondaj datumbutikoj nomataj Mapoj. Mapoj estas ŝlosilvaloraj vendejoj, kiuj ebligas interŝanĝon de datumoj inter uzantspaco kaj kernspaco. eBPF disponigas plurajn specojn de mapoj.
  • Malĉefaj funkcioj. Aparte, por reverki pakaĵon, kalkuli ĉeksumon aŭ kloni pakaĵon. Ĉi tiuj funkcioj funkcias ene de la kerno kaj ne estas uzantspacaj programoj. Vi ankaŭ povas fari sistemajn vokojn de eBPF-programoj.
  • Finu vokojn. La programgrandeco en eBPF estas limigita al 4096 bajtoj. La trajto de vostovoko permesas al programo eBPF transdoni kontrolon al nova programo eBPF kaj tiel preteriri ĉi tiun limigon (ĝis 32 programoj povas esti ligitaj tiamaniere).

eBPF: ekzemplo

Estas pluraj ekzemploj por eBPF en la Linukso-kernfontoj. Ili haveblas ĉe samples/bpf/. Por kompili ĉi tiujn ekzemplojn, simple enigu:

$ sudo make samples/bpf/

Mi mem ne skribos novan ekzemplon por eBPF, sed uzos unu el la specimenoj disponeblaj en samples/bpf/. Mi rigardos kelkajn partojn de la kodo kaj klarigos kiel ĝi funkcias. Ekzemple, mi elektis la programon tracex4.

Ĝenerale, ĉiu el la ekzemploj en samples/bpf/ konsistas el du dosieroj. Tiuokaze:

  • tracex4_kern.c, enhavas la fontkodon por esti efektivigita en la kerno kiel eBPF-bajtkodo.
  • tracex4_user.c, enhavas programon el uzantspaco.

En ĉi tiu kazo, ni devas kompili tracex4_kern.c al eBPF-bajtokodo. Nuntempe en gcc ne ekzistas backend por eBPF. Feliĉe, clang povas eligi eBPF-bajtokodon. Makefile uzoj clang por kompilo tracex4_kern.c al la objektodosiero.

Mi menciis supre, ke unu el la plej interesaj trajtoj de eBPF estas mapoj. tracex4_kern difinas unu mapon:

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 estas unu el la multaj specoj de kartoj ofertitaj de eBPF. En ĉi tiu kazo, ĝi estas nur haŝiŝo. Vi eble ankaŭ rimarkis reklamon SEC("maps"). SEC estas makroo uzata por krei novan sekcion de binara dosiero. Fakte, en la ekzemplo tracex4_kern du pliaj sekcioj estas difinitaj:

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

Ĉi tiuj du funkcioj permesas forigi enskribon de la mapo (kprobe/kmem_cache_free) kaj aldonu novan eniron al la mapo (kretprobe/kmem_cache_alloc_node). Ĉiuj funkcionomoj skribitaj en majuskloj respondas al makrooj difinitaj en bpf_helpers.h.

Se mi forĵetas la sekciojn de la objektodosiero, mi devus vidi, ke ĉi tiuj novaj sekcioj jam estas difinitaj:

$ 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

Estas ankaŭ tracex4_user.c, ĉefa programo. Esence, ĉi tiu programo aŭskultas eventojn kmem_cache_alloc_node. Kiam tia okazaĵo okazas, la responda eBPF-kodo estas ekzekutita. La kodo konservas la IP-atributon de la objekto en mapon, kaj la objekto tiam estas lopita tra la ĉefprogramo. Ekzemplo:

$ 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

Kiel rilatas uzantspaca programo kaj programo eBPF? Pri inicialigo tracex4_user.c ŝargas objektodosieron tracex4_kern.o uzante la funkcion 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;
}

Dum farante load_bpf_file enketoj difinitaj en la eBPF-dosiero estas aldonitaj al /sys/kernel/debug/tracing/kprobe_events. Nun ni aŭskultas ĉi tiujn eventojn kaj nia programo povas fari ion kiam ili okazas.

$ 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

Ĉiuj aliaj programoj en sample/bpf/ estas strukturitaj simile. Ili ĉiam enhavas du dosierojn:

  • XXX_kern.c: programo eBPF.
  • XXX_user.c: ĉefa programo.

La programo eBPF identigas mapojn kaj funkciojn asociitajn kun sekcio. Kiam la kerno eldonas eventon de certa tipo (ekzemple, tracepoint), la ligitaj funkcioj estas ekzekutitaj. La kartoj disponigas komunikadon inter la kernprogramo kaj la uzantspaca programo.

konkludo

Ĉi tiu artikolo diskutis BPF kaj eBPF ĝenerale. Mi scias, ke ekzistas multaj informoj kaj rimedoj pri eBPF hodiaŭ, do mi rekomendos kelkajn pliajn rimedojn por plua studo.

Mi rekomendas legi:

fonto: www.habr.com

Aldoni komenton