En kort introduktion till BPF och eBPF

Hej, Habr! Vi meddelar att vi förbereder en bok "Linux Observability with BPF".

En kort introduktion till BPF och eBPF
Eftersom BPF-virtuella maskinen fortsÀtter att utvecklas och aktivt anvÀnds i praktiken, har vi översatt en artikel för dig som beskriver dess grundlÀggande funktioner och aktuella tillstÄnd.

Under de senaste Ären har verktyg för programmering och tekniker som syftar till att kompensera för begrÀnsningarna i Linux-kÀrnan vid högpresterande paketbehandling fÄtt ökad popularitet. En av de mest populÀra metoderna av detta slag kallas kÀrnöverskridande (kernel bypass) och gör det möjligt att utföra all paketbehandling frÄn anvÀndarrymden genom att hoppa över nÀtverkslagret i kÀrnan. KÀrnöverskridning innebÀr ocksÄ att nÀtverkskortet hanteras frÄn anvÀndarrymden. Med andra ord, nÀr vi arbetar med nÀtverkskortet förlitar vi oss pÄ drivrutinen. anvÀndarrymden.

Genom att överlÀmna fullstÀndig kontroll över nÀtverkskortet till ett program i anvÀndarrymden minskar vi kostnaderna som orsakas av kÀrnans arbete (kontextbyten, hantering av nÀtverkslagret, avbrott etc.), vilket Àr sÀrskilt viktigt nÀr man arbetar med hastigheter pÄ 10 Gb/s eller mer. KÀrnöverskridning plus en kombination av andra funktioner (paketbearbetning) och noggrant prestandaoptimering (NUMA-berÀkning, CPU-isolering, etc.) Àr grundlÀggande för högpresterande nÀtverksbehandling i anvÀndarrymden. Ett framstÄende exempel pÄ ett nytt tillvÀgagÄngssÀtt för paketbehandling Àr DPDK frÄn Intel (Data Plane Development Kit), Àven om det finns andra vÀlkÀnda verktyg och tekniker, inklusive VPP frÄn Cisco (Vector Packet Processing), Netmap och sjÀlvklart Snabb.

Organisationen av nÀtverksinteraktioner i anvÀndarrymden har flera nackdelar:

  • KĂ€rnan i operativsystemet Ă€r en abstraheringsnivĂ„ för hĂ„rdvaruresurser. Eftersom program i anvĂ€ndarrymden mĂ„ste hantera sina resurser direkt, mĂ„ste de ocksĂ„ hantera sin egen hĂ„rdvara. Detta innebĂ€r ofta att man mĂ„ste programmera egna drivrutiner.
  • Eftersom vi helt avstĂ„r frĂ„n kĂ€rnans utrymme avstĂ„r vi ocksĂ„ frĂ„n all nĂ€tverksfunktionalitet som tillhandahĂ„lls av kĂ€rnan. Program i anvĂ€ndarutrymmet mĂ„ste Ă„terskapa de funktioner som kanske redan tillhandahĂ„lls av kĂ€rnan eller operativsystemet.
  • Program körs i en sandbox-miljö, vilket allvarligt begrĂ€nsar deras samverkan och hindrar dem frĂ„n att integrera med andra delar av operativsystemet.

I sjÀlva verket, nÀr man organiserar nÀtverksinteraktioner i anvÀndarutrymmet, uppnÄs prestandaförbÀttring genom att flytta paketbearbetningen frÄn kÀrnan till anvÀndarutrymmet. XDP gör det precis motsatta: det flyttar nÀtverksprogram frÄn anvÀndarutrymmet (filter, omvandlare, routing, osv.) till kÀrnomrÄdet. XDP gör det möjligt för oss att utföra nÀtverksfunktioner sÄ snart paketet nÄr nÀtverksgrÀnssnittet och innan det börjar röra sig uppÄt i kÀrnans nÀtverksdel. Som ett resultat ökar hastigheten för paketbearbetning avsevÀrt. Men hur tillÄter kÀrnan anvÀndaren att köra sina program i kÀrnans utrymme? Innan vi svarar pÄ den frÄgan, lÄt oss titta pÄ vad BPF Àr.

BPF och eBPF

Trots dess nÄgot oklara namn BPF (Berkeley Packet Filter) Àr det i sjÀlva verket en modell för en virtuell maskin. Denna virtuella maskin designades ursprungligen för paketfiltrering, dÀrav namnet.

Ett av de mest kĂ€nda verktygen som anvĂ€nder BPF Ă€r Fungerar libdns korrekt? => Kör. NĂ€r paket fĂ„ngas med hjĂ€lp av Fungerar libdns korrekt? => Kör kan anvĂ€ndaren ange ett uttryck för att filtrera paket. Endast de paket som matchar detta uttryck kommer att fĂ„ngas. Till exempel avser uttrycket “tcp dst port 80” alla TCP-paket som anlĂ€nder till port 80. Kompilatorn kan förkorta detta uttryck genom att omvandla det till 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

Detta Àr i princip vad det ovan nÀmnda programmet gör:

  • Instruktion (000): laddar paketet med en förskjutning pĂ„ 12, som ett 16-bitars ord i ackumulatorn. Förskjutningen 12 motsvarar paketets ethertype.
  • Instruktion (001): jĂ€mför vĂ€rdet i ackumulatorn med 0x86dd, det vill sĂ€ga, med ethertype-vĂ€rdet för IPv6. Om resultatet Ă€r sant gĂ„r programmet vidare till instruktion (002), annars till (006).
  • Instruktion (006): jĂ€mför vĂ€rdet med 0x800 (ethertype-vĂ€rde för IPv4). Om svaret Ă€r sant, gĂ„r programmet vidare till (007), om inte – till (015).

Och sÄ vidare, tills paketfilterprogrammet returnerar ett resultat. Vanligtvis Àr detta en boolean. En icke-noll Äterkomst (instruktion (014)) betyder att paketet passade, medan en noll Äterkomst (instruktion (015)) betyder att paketet inte passade.

BPF virtuell maskin och dess bytekod föreslogs av Steve McCanne och Van Jacobson i slutet av 1992, nÀr deras artikel publicerades. BSD paketfilter: En ny arkitektur för paketfÄngst pÄ anvÀndarnivÄ., den hÀr teknologin presenterades först pÄ Usenix-konferensen vintern 1993.

Eftersom BPF Ă€r en virtuell maskin definierar den miljön dĂ€r program körs. Förutom bytekoden definierar den ocksĂ„ paketmodellen för minnet (inladdningsinstruktioner tillĂ€mpas implicit pĂ„ paketet), register (A och X; ackumulator och indexregister), skrapminne och en implicit programrĂ€knare. Intressant nog modellerades BPF-bytecode efter Motorola 6502 ISA. Som Steve McCanne pĂ„minde om i sin plenar förelĂ€sning pĂ„ Sharkfest ’11, han hade varit bekant med 6502 assembler sedan högstadiet nĂ€r han programmerade pĂ„ Apple II, och dessa kunskaper pĂ„verkade hans arbete med att designa BPF-bytecode.

BPF-stöd implementerades i Linux-kÀrnan i version v2.5 och senare, tillagd frÀmst genom Jay Shullists insatser. BPF-koden förblev utan större förÀndringar fram till 2011 nÀr Eric Dumazet omarbetade BPF-tolken för att fungera i JIT-lÀge (KÀlla: JIT för paketfilter). Efter detta kunde kÀrnan istÀllet för att tolka BPF-bytecode direkt översÀtta BPF-program till den mÄlarkitektur: x86, ARM, MIPS, etc.

Senare, 2014, föreslog Alexey Starovoitov en ny JIT-mekanism för BPF. Faktiskt blev denna nya JIT en ny arkitektur baserad pÄ BPF och fick namnet eBPF. Jag tror att under en tid samexisterade bÄda virtuella maskiner, men för nÀrvarande sker paketfiltering baserat pÄ eBPF. Faktiskt, i mÄnga moderna dokumentationsexempel förstÄs BPF som eBPF, medan klassisk BPF idag Àr kÀnd som cBPF.

eBPF utökar klassisk BPF pÄ flera sÀtt:

  • Baserat pĂ„ moderna 64-bitarsarkitekturer. eBPF anvĂ€nder 64-bitarsregister och ökar antalet tillgĂ€ngliga register frĂ„n 2 (ackumulator och X) till 10. eBPF tillhandahĂ„ller ocksĂ„ ytterligare operation koder (BPF_MOV, BPF_JNE, BPF_CALL
).
  • Frikopplad frĂ„n nĂ€tverkslagret. BPF var kopplad till pakets data modell. Eftersom det anvĂ€ndes för paketfiltering var dess kod i underrubriken som möjliggör nĂ€tverksinteraktioner. Men den virtuella maskinen eBPF Ă€r inte lĂ€ngre kopplad till datamodellen och kan anvĂ€ndas för alla Ă€ndamĂ„l. Nu kan ett eBPF-program kopplas till en tracepoint eller en kprobe. Detta öppnar vĂ€gen för eBPF-instrumentering, prestandanalys och mĂ„nga andra anvĂ€ndningsomrĂ„den i kontexten av andra kĂ€rnundersystem. Nu finns eBPF-koden pĂ„ sin egen vĂ€g: kernel/bpf.
  • Globala datalager kallade Kartor. Kartor Ă€r nyckel-vĂ€rde-lager som möjliggör datadelning mellan anvĂ€ndarrummet och kĂ€rnans rum. eBPF tillhandahĂ„ller kartor av flera typer.
  • HjĂ€lpfunktioner. I synnerhet för omskrivning av paket, berĂ€kning av kontrollsummor eller kloning av paket. Dessa funktioner utförs inom kĂ€rnan och relaterar inte till anvĂ€ndarprogram. Dessutom kan systemanrop göras frĂ„n eBPF-program.
  • Slut-anrop. Störrelsen pĂ„ programmet i eBPF Ă€r begrĂ€nsad till 4096 byte. Möjligheten till slut-anrop tillĂ„ter eBPF-programmet att överlĂ€mna kontrollen till ett nytt eBPF-program och dĂ€rmed kringgĂ„ denna begrĂ€nsning (upp till 32 program kan kopplas samman).

eBPF: exempel

I kÀllkoden till Linux-kÀrnan finns flera exempel för eBPF. De Àr tillgÀngliga pÄ adressen samples/bpf/. För att kompilera dessa exempel, skriv helt enkelt:

$ sudo make samples/bpf/

Jag tÀnker inte skriva ett nytt exempel för eBPF sjÀlv, utan anvÀnda ett av de exempel som finns tillgÀngliga i samples/bpf/. Jag kommer att titta pÄ vissa kodavsnitt och förklara hur det fungerar. Jag har valt programmet tracex4.

AllmÀnt bestÄr varje exempel i samples/bpf/ av tvÄ filer. I detta fall:

  • tracex4_kern.c, innehĂ„ller koden som ska köras i kĂ€rnan som eBPF bytekod.
  • tracex4_user.c, innehĂ„ller programmet frĂ„n anvĂ€ndarutrymmet.

I sÄ fall behöver vi kompilera tracex4_kern.c i eBPF bytecode. För nÀrvarande finns gcc ingen serverdel för eBPF. Lyckligtvis clang kan generera eBPF bytecode. Makefile anvÀnder clang för kompilering tracex4_kern.c till ett objektfil.

Tidigare nÀmnde jag att en av de mest intressanta funktionerna i eBPF Àr kartor. tracex4_kern definierar en karta:

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 – en av mĂ„nga typer av kartor som erbjuds av eBPF. I detta fall Ă€r det bara en hash. Du kanske ocksĂ„ har lagt mĂ€rke till deklarationen SEC("maps"). SEC Ă€r en makro som anvĂ€nds för att skapa en ny sektion av den binĂ€ra filen. I exemplet tracex4_kern definierar tvĂ„ andra sektioner:

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;

    \\ fÄr ip-adressen frÄn den anropande sidan 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;
}   

Dessa tvÄ funktioner gör det möjligt att ta bort ett post frÄn kartan (kprobe/kmem_cache_free) och lÀgga till en ny post i kartan (kretprobe/kmem_cache_alloc_node). Alla funktionsnamn som skrivs med stora bokstÀver motsvarar makron definierade i bpf_helpers.h.

Om jag skriver ut dumpen av sektionerna i objektfilen bör jag se att dessa nya sektioner redan Àr definierade:

$ objdump -h tracex4_kern.o

tracex4_kern.o: filformat elf64-little

Sektioner:
Idx Namn Storlek VMA LMA Fil off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
INNEHÅLL, ALLOKERA, LÄS, ÄNDRING, KOD
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
INNEHÅLL, ALLOKERA, LÄS, RELOC, READONLY, KOD
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
INNEHÅLL, ALLOKERA, LÄS, RELOC, READONLY, KOD
3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2
INNEHÅLL, ALLOKERA, LÄS, DATA
4 license 00000004 0000000000000000 0000000000000000 00000164 2**0
INNEHÅLL, ALLOKERA, LÄS, DATA
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
INNEHÅLL, ALLOKERA, LÄS, DATA
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
INNEHÅLL, ALLOKERA, LÄS, RELOC, READONLY, DATA

Det finns ocksÄ tracex4_user.c, huvudprogrammet. I princip lyssnar detta program pÄ hÀndelser kmem_cache_alloc_node. NÀr en sÄdan hÀndelse intrÀffar, körs motsvarande eBPF-kod. Koden sparar IP-attributet av objektet i kartan, och sedan skrivs detta objekt cykliskt ut i huvudprogrammet. Exempel:

$ sudo ./tracex4
obj 0xffff8d6430f60a00 Àr 2 sekunder gammal, allokerad vid ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 Àr 23 sekunder gammal, allokerad vid ip ffffffff98090e8f
obj 0xffff8d5f80161780 Àr 6 sekunder gammal, allokerad vid ip ffffffff98090e8f

Hur hÀnger programmet i anvÀndarutrymme ihop med eBPF-programmet? Vid initialisering tracex4_user.c laddar filen tracex4_kern.o med hjÀlp av funktionen ladda_bpf_fil.

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

Vid körning ladda_bpf_fil prober som definieras i eBPF-filen lÀggs till i /sys/kernel/debug/tracing/kprobe_events. Nu lyssnar vi pÄ dessa hÀndelser och vÄrt program kan utföra nÄgot nÀr de intrÀffar.

$ 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

Alla andra program i sample/bpf/ Àr strukturerade pÄ samma sÀtt. De har alltid tvÄ filer:

  • XXX_kern.c: eBPF-programmet.
  • XXX_user.c: huvudprogrammet.

eBPF-programmet definierar kartor och funktioner som Àr kopplade till sektionen. NÀr kÀrnan genererar en hÀndelse av viss typ (t.ex. tracepoint), körs de kopplade funktionerna. Kartorna möjliggör datadelning mellan kÀrnprogrammet och anvÀndarprogrammet.

Slutsats

I den hÀr artikeln har vi i allmÀnhet diskuterat BPF och eBPF. Jag vet att det idag finns mycket information och resurser om eBPF, sÄ jag rekommenderar ytterligare nÄgra material för vidare studier.

Jag rekommenderar att lÀsa:

KĂ€lla: habr.com

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster