Hej, Habr! Vi meddeler, at vi har en bog på vej "".

Efterhånden som BPF-VM'en fortsætter med at udvikle sig og anvendes aktivt i praksis, har vi oversat en artikel for dig, der beskriver dens grundlæggende funktioner og nuværende tilstand.
I de seneste år er værktøjer til programmering og teknikker, der kompenserer for begrænsningerne i Linux-kernen, blevet populære, når der kræves højtydende pakkehåndtering. En af de mest populære teknikker af denne art kaldes kerneomgåelse og gør det muligt, ved at omgå netværkslaget i kernen, at udføre al pakkehåndtering fra brugerrummet. Kerneomgåelse indebærer også at styre netværkskortet fra brugerrummet. Med andre ord, når vi arbejder med netværkskortet, stoler vi på driveren brugerrummet.
Ved at give fuld kontrol over netværkskortet til et program fra brugerrummet reducerer vi omkostningerne forbundet med kernens arbejde (konteksskift, håndtering af netværkslaget, afbrydelser osv.), hvilket er væsentligt ved hastigheder på 10 Gb/s eller derover. Kerneomgåelse plus kombinationen af andre funktioner (pakkehåndtering) og finjustering af ydeevnen (NUMA-hensyntagen, CPU-isolering, osv.) udgør grundlaget for højtydende netværkshåndtering i brugerrummet. Et fremragende eksempel på denne nye tilgang til pakkehåndtering er fra Intel (Data Plane Development Kit), selvom der også findes andre velkendte værktøjer og teknikker, herunder VPP fra Cisco (Vector Packet Processing), Netmap og selvfølgelig, .
At organisere netværksinteraktioner i brugerrummet har en række ulemper:
- Kernen i OS er et abstraheringslag for hardware ressourcer. Da programmer i brugerrummet skal håndtere deres ressourcer direkte, skal de også styre deres egen hardware. Ofte betyder det, at der er behov for at programmere egne drivere.
- Da vi fuldstændigt fraskriver os kernerummet, afviser vi også al netværksfunktionalitet, der leveres af kernen. Programmer i brugerrummet bliver nødt til at implementere de funktioner, der muligvis allerede tilbydes af kernen eller operativsystemet, fra bunden.
- Programmer kører i en sandkasse, hvilket alvorligt begrænser deres interaktionsevner og forhindrer dem i at integrere med andre dele af operativsystemet.
I bund og grund opnås ydeevneforbedringer i netværksinteraktioner i brugerrummet ved at flytte pakkehåndtering fra kernen til brugerrummet. XDP gør lige det modsatte: det flytter netværksprogrammer fra brugerrummet (filtre, transformatorer, routing osv.) ind i kerneområdet. XDP giver os mulighed for at udføre en netværksfunktion, så snart en pakke rammer netværksinterfacet, og inden den begynder at bevæge sig op i kernens netværkssubsystem. Resultatet er en betydelig stigning i pakkehåndteringshastigheden. Men hvordan tillader kernen brugeren at køre sine programmer i kernerummet? Før vi svarer på dette spørgsmål, lad os se nærmere på, hvad BPF er.
BPF og eBPF
På trods af det ikke helt forståelige navn BPF (Berkeley Packet Filter) er det i virkeligheden en model for en virtuel maskine. Denne virtuelle maskine blev oprindeligt designet til at håndtere pakke filtrering, og det er derfor navnet.
Et af de mest kendte værktøjer, der bruger BPF, er tcpdump. Når pakker fanges ved hjælp af tcpdump kan brugeren angive et udtryk til filtrering af pakker. Kun de pakker, der stemmer overens med dette udtryk, vil blive fanget. For eksempel vedrører udtrykket “tcp dst port 80” alle TCP-pakker, der ankommer til port 80. Kompilatoren kan forkorte dette udtryk ved at konvertere det til BPF byte-kode.
$ 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
Dette er i princippet, hvad det ovennævnte program gør:
- Instruktion (000): indlæser pakken med en offset på 12, i form af et 16-bit ord i akkumulatoren. Offset 12 svarer til packetens ethertype.
- Instruktion (001): sammenligner værdien i akkumulatoren med 0x86dd, altså med ethertype-værdien for IPv6. Hvis resultatet er sandt, går programmets tæller til instruktion (002), og hvis ikke - til (006).
- Instruktion (006): sammenligner værdien med 0x800 (ethertype-værdi for IPv4). Hvis svaret er true, går programmet til (007), ellers til (015).
Og så videre, indtil paketfiltreringsprogrammet returnerer et resultat. Normalt er dette en boolesk værdi. En ikke-nul værdi (instruktion (014)) betyder, at pakken passer, mens en nul værdi (instruktion (015)) betyder, at pakken ikke passer.
Den virtuelle maskine BPF og dens bytekode blev foreslået af Steve McCanne og Van Jacobson i slutningen af 1992, da deres artikel blev publiceret. , for første gang blev denne teknologi præsenteret på Usenix-konferencen om vinteren 1993.
Da BPF er en virtuel maskine, definerer den det miljø, hvor programmerne køres. Udover bytekoden definerer den også pakkemodel for hukommelsen (indlæsning af instruktioner gælder implicit for pakken), registre (A og X; akkumulator- og indeksregistre), lager af skratchhukommelse og en implicit programtæller. Det er interessant, at bytekoden for BPF var modelleret efter Motorola 6502 ISA. Som Steve McCanne mindedes i sin på Sharkfest ‘11, var han bekendt med 6502-samlingen fra gymnasiet, da han programmerede på Apple II, og denne viden havde indflydelse på hans arbejde med at designe bytekoden for BPF.
BPF-support er implementeret i Linux-kernen fra version v2.5 og frem, tilføjet hovedsageligt af Jay Schullist. Koden for BPF forblev uden væsentlige ændringer indtil 2011, hvor Eric Dumazet omarbejdede BPF-fortolkeren til at køre i JIT-tilstand (Kilde: ). Efter dette kunne kernen, i stedet for at fortolke bytekoden for BPF, direkte konvertere BPF-programmer til målarkitektur: x86, ARM, MIPS, osv.
Senere, i 2014, foreslog Alexey Starovoitov en ny JIT-mekanisme for BPF. Faktisk blev denne nye JIT en ny arkitektur baseret på BPF og fik navnet eBPF. Jeg tror, at i en periode eksisterede begge virtuelle maskiner side om side, men i dag implementeres pakke filtrering baseret på eBPF. Faktisk forstås eBPF i mange moderne dokumentationer som BPF, mens den klassiske BPF i dag er kendt som cBPF.
eBPF udvider på flere måder den klassiske virtuelle maskine BPF:
- Basert på moderne 64-biters arkitekturer. eBPF bruker 64-biters registre og øker antallet tilgjengelige registre fra 2 (akkumulator og X) til 10. I eBPF er det også flere operasjonskoder tilgjengelige (BPF_MOV, BPF_JNE, BPF_CALL…).
- Fritt fra nettverkslag. BPF var knyttet til en pakke datamodell. Siden den ble brukt til pakke-filtrering, var koden dens i delsystemet som håndterer nettverksinteraksjoner. Imidlertid er den virtuelle maskinen eBPF ikke lenger knyttet til datamodellen og kan brukes til ethvert formål. Nå kan eBPF-programmet kobles til tracepoint eller kprobe. Dette åpner for eBPF-instrumentering, ytelsesanalyse og mange andre bruksområder i konteksten av andre kjernedelsystemer. Nå ligger eBPF-koden i sin egen bane: kernel/bpf.
- Globale datalagre kalt Kart. Kart er lagre med "nøkkel-verdi"-type som muliggjør datadeling mellom bruker- og kjernrom. I eBPF er det flere typer kart tilgjengelige.
- Hjelpefunksjoner. Spesielt for omskriving av pakker, beregning av sjekksum eller kloning av pakker. Disse funksjonene utføres inne i kjernen og er ikke relatert til programmer i brukerom. I tillegg kan systemkall utføres fra eBPF-programmer.
- Avslutningskall. Størrelsen på et program i eBPF er begrenset til 4096 byte. Muligheten til å gjøre avslutningskall lar et eBPF-program overføre kontrollen til et nytt eBPF-program og dermed omgå denne begrensningen (opptil 32 programmer kan kobles sammen).
eBPF: eksempel
I kildekoden til Linux-kjernen er det flere eksempler for eBPF. De er tilgjengelige på adressen samples/bpf/. For å kompilere disse eksemplene, skriv enkelt inn:
$ sudo make samples/bpf/
Jeg vil ikke skrive et nytt eksempel for eBPF, men bruke en av prøvene som er tilgjengelige i samples/bpf/. Jeg vil se på noen deler av koden og forklare hvordan den fungerer. Som et eksempel har jeg valgt programmet tracex4.
Generelt består hver av prøvene i samples/bpf/ av to filer. I dette tilfellet:
tracex4_kern.c, inneholder kildekoden som skal kjøres i kjernen som eBPF bytekode.tracex4_user.c, inneholder programmet fra brukerrommet.
I så fall må vi kompilere tracex4_kern.c til bytecode eBPF. I øjeblikket mangler gcc backend for eBPF. Heldigvis kan clang generere bytecode eBPF. bruger clang til kompilering tracex4_kern.c til objektfilen.
Som nævnt før er en af de mest interessante funktioner ved eBPF kort. tracex4_kern definerer et kort:
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 af mange korttyper, der tilbydes af eBPF. I dette tilfælde er det blot en hash. Du har måske også lagt mærke til erklæringen SEC("maps"). SEC er en makro, der bruges til at oprette en ny sektion i den binære fil. I eksemplet defineres der faktisk to yderligere sektioner: tracex4_kern definerer yderligere to 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 til kalderen 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;
} Disse to funktioner muliggør at fjerne en post fra kortet (kprobe/kmem_cache_free) og tilføje en ny post til kortet (kretprobe/kmem_cache_alloc_node). Alle funktionsnavne, der er skrevet med store bogstaver, svarer til makroer, der er defineret i .
Hvis jeg udskriver rækken af sektioner i objektfilen, skal jeg kunne se, at disse nye sektioner allerede er defineret:
$ objdump -h tracex4_kern.o
tracex4_kern.o: filformat elf64-lille
Sektioner:
Idx Navn Størrelse VMA LMA Fil off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
INDHOLD, AFTAL, LAST, LÆSNING, KODE
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
INDHOLD, AFTAL, LAST, RELOC, LÆSNING, KODE
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
INDHOLD, AFTAL, LAST, RELOC, LÆSNING, KODE
3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2
INDHOLD, AFTAL, LAST, DATA
4 license 00000004 0000000000000000 0000000000000000 00000164 2**0
INDHOLD, AFTAL, LAST, DATA
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
INDHOLD, AFTAL, LAST, DATA
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
INDHOLD, AFTAL, LAST, RELOC, LÆSNING, DATA
Der er også , hovedprogrammet. I princippet lytter dette program til begivenheder kmem_cache_alloc_node. Når en sådan begivenhed opstår, udføres den tilsvarende eBPF-kode. Koden gemmer IP-attributten for objektet i kortet, og derefter bliver dette objekt cyklisk udskrevet i hovedprogrammet. Eksempel:
$ sudo .\/tracex4
obj 0xffff8d6430f60a00 er 2 sek gammel og blev tildelt ved ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 er 23 sek gammel og blev tildelt ved ip ffffffff98090e8f
obj 0xffff8d5f80161780 er 6 sek gammel og blev tildelt ved ip ffffffff98090e8f
Hvordan er programmet i brugerfladen forbundet med eBPF-programmet? Ved initialisering tracex4_user.c indlæser filen tracex4_kern.o ved hjælp af funktionen indlæs_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;
} Under udførelsen sonder, der er defineret i eBPF-filen, tilføjes til /sys/kernel/debug/tracing/kprobe_events. Nu lytter vi til disse begivenheder, og vores program kan gøre noget, når de sker.
$ 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
Alle andre programmer i sample/bpf/ er struktureret på samme måde. Der er altid to filer:
XXX_kern.c: eBPF-programmet.XXX_user.c: hovedprogrammet.
eBPF-programmet definerer kort og funktioner, der er bundet til sektionen. Når kernen udsteder en begivenhed af en bestemt type (for eksempel, tracepoint), udføres de bundne funktioner. Kortene muliggør datadeling mellem kerneprogrammet og brugerprogrammet.
Konklusion
Denne artikel har kort gennemgået BPF og eBPF. Jeg er klar over, at der i dag er meget information og ressourcer om eBPF, så jeg vil anbefale nogle yderligere materialer til at fordybe sig i emnet.
Jeg anbefaler at læse:
- Jonathan Corbet. En introduktion til BPF og en fortælling om, hvordan den har udviklet sig til eBPF.
- Brendan Gregg. Artikel fra LWN.net. Brendan skriver ofte tweets om eBPF og holder sin liste over ressourcer til emnet. .
- Julia Evans. Kommentering af præsentationen af Suchakra Sharma “The BSD Packet Filter: A New Architecture for User-level Packet Capture”. Kommentarerne er gode og hjælper virkelig med at forstå lysbilledene.
- Ferris Ellis. Longread med , men det er værd at læse. En af de bedste artikler om eBPF, jeg er stødt på.
Kilde: habr.com
