Hallo, Habr! We melden dat we werken aan het uitgeven van het boek "".

Aangezien de BPF-virtuele machine blijft evolueren en actief in de praktijk wordt toegepast, hebben we een artikel voor jullie vertaald dat de belangrijkste mogelijkheden en de huidige staat beschrijft.
In de afgelopen jaren zijn er tools en technieken populair geworden die de beperkingen van de Linux-kernel willen compenseren wanneer er een hoge doorvoersnelheid van pakketten vereist is. Een van de meest populaire technieken van dit soort wordt genoemd kernelomzeiling (kernel bypass) en stelt in staat om de netwerklaag van de kernel over te slaan en alle pakketverwerking vanuit de gebruikersruimte uit te voeren. Kernomzeiling houdt ook in dat de netwerkinterface vanuit de gebruikersruimtewordt beheerd. Met andere woorden, bij het werken met de netwerkinterface vertrouwen we op de driver. gebruikersruimte.
Door de volledige controle over de netwerkinterface aan een programma in de gebruikersruimte te geven, minimaliseren we de overhead die wordt veroorzaakt door het werken van de kernel (contextwisseling, netwerklaagverwerking, onderbrekingen, enz.), wat bijzonder belangrijk is bij snelheden van 10 Gb/s of meer. Kernomzeiling, gecombineerd met andere mogelijkheden (pakketverwerking) en zorgvuldige prestatie-instellingen (NUMA-overweging, CPU-isolatie, enz.) vormen de basis voor hoge prestatienetwerkverwerking in de gebruikersruimte. Een treffend voorbeeld van deze nieuwe benadering van pakketverwerking is de van Intel (Data Plane Development Kit), hoewel er ook andere bekende tools en technieken zijn, waaronder VPP van Cisco (Vector Packet Processing), Netmap en natuurlijk .
Het organiseren van netwerkinteracties in de gebruikersruimte heeft enkele nadelen:
- De kernel van het besturingssysteem is een abstractielaag voor hardwarebronnen. Aangezien programma's in de gebruikersruimte hun bronnen direct moeten beheren, moeten ze ook hun eigen hardware beheren. Dit betekent vaak dat ze hun eigen drivers moeten programmeren.
- Aangezien we volledig afzien van de kernelruimte, doen we ook afstand van alle netwerkmogelijkheden die door de kernel worden geboden. Programma's in de gebruikersruimte moeten de functies opnieuw implementeren die mogelijk al door de kernel of het besturingssysteem worden aangeboden.
- Programma's werken in een sandbox-modus, wat het vermogen tot interactie aanzienlijk beperkt en de integratie met andere delen van het besturingssysteem bemoeilijkt.
In wezen, bij het organiseren van netwerkinteracties in de gebruikersruimte, wordt de performanceverbetering bereikt door de verwerking van pakketten van de kernel naar de gebruikersruimte te verplaatsen. XDP doet het tegenovergestelde: het verplaatst netwerkprogramma's van de gebruikersruimte (filters, converters, routering, enz.) naar het gebied van de kernel. XDP stelt ons in staat om een netwerkfunctie uit te voeren zodra een pakket het netwerkinterface bereikt en voordat het omhoog beweegt in de netwerksubsystemen van de kernel. Het resultaat is een aanzienlijke toename van de verwerkingssnelheid van pakketten. Maar hoe staat de kernel gebruikers toe om hun programma's in de kernelruimte uit te voeren? Voordat we deze vraag beantwoorden, laten we eerst bekijken wat BPF is.
BPF en eBPF
Ondanks de niet helemaal duidelijke naam BPF (Berkeley Packet Filter) is het feitelijk een model voor een virtuele machine. Deze virtuele machine werd oorspronkelijk ontworpen voor het verwerken van pakketfiltering, vandaar de naam.
Een van de bekendste tools die BPF gebruiken, is tcpdump. Bij het vastleggen van pakketten met behulp van tcpdump kan de gebruiker een expressie opgeven voor het filteren van pakketten. Alleen de pakketten die aan deze expressie voldoen, worden vastgelegd. Bijvoorbeeld, de expressie ātcp dst port 80ā betreft alle TCP-pakketten die binnenkomen op poort 80. De compiler kan deze expressie verkorten door deze om te zetten in 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
Dit is in principe wat het bovenstaande programma doet:
- Instructie (000): laadt een pakket met een offset van 12, als een 16-bits woord in de accumulator. Offset 12 komt overeen met het ethertype van het pakket.
- Instructie (001): vergelijkt de waarde in de accumulator met 0x86dd, dat wil zeggen, de ethertype-waarde voor IPv6. Als het resultaat waar is, gaat de programmastap naar instructie (002), en als dat niet het geval is, naar (006).
- Instructie (006): vergelijkt de waarde met 0x800 (ethertype-waarde voor IPv4). Als het antwoord waar is, gaat het programma naar (007), zo niet, dan naar (015).
En zo verder, totdat het pakketfilterprogramma een resultaat teruggeeft. Gewoonlijk is dit een boolean. Een niet-nul resultaat (instructie (014)) betekent dat het pakket geschikt is, terwijl een nulresultaat (instructie (015)) betekent dat het pakket niet geschikt is.
De virtuele machine BPF en zijn bytecode werden eind 1992 voorgesteld door Steve McCanne en Van Jacobson, toen hun artikel verscheen. , voor het eerst gepresenteerd op de Usenix-conferentie in de winter van 1993.
Aangezien BPF een virtuele machine is, definieert het de omgeving waarin programma's worden uitgevoerd. Naast de bytecode definieert het ook het pakket-geheugenmodel (laadinstructies worden impliciet op het pakket toegepast), registers (A en X; de accumulator- en indexregisters), opslag van scratch-geheugen en een impliciete programmacounter. Interessant is dat de bytecode van BPF is gemodelleerd naar het voorbeeld van de Motorola 6502 ISA. Zoals Steve McCanne zich herinnert in zijn op Sharkfest '11, was hij al bekend met de 6502-assemblage sinds de middelbare school, toen hij programmeerde op de Apple II, en deze kennis heeft zijn werk aan het ontwerp van de BPF-bytecode beĆÆnvloed.
De ondersteuning voor BPF is geĆÆmplementeerd in de Linux-kernel in versie v2.5 en hoger, voornamelijk dankzij de inspanningen van Jay Sullivan. De BPF-code onderging tot 2011 geen grote wijzigingen, toen Eric Dumazet de BPF-interpreter herontwierp om in JIT-modus te werken (Bron: ). Na deze wijziging kon de kernel de BPF-bytecode in plaats van te interpreteren rechtstreeks omzetten naar programma's voor de doelarchitectuur: x86, ARM, MIPS, enz.
Later, in 2014, stelde Alexey Starovoitov een nieuw JIT-mechanisme voor BPF voor. In feite werd deze nieuwe JIT een nieuwe architectuur gebaseerd op BPF en kreeg de naam eBPF. Ik denk dat beide virtuele machines een tijdje naast elkaar hebben bestaan, maar tegenwoordig wordt pakketfiltering voornamelijk op basis van eBPF uitgevoerd. In feite wordt in veel moderne documentatiesamples met BPF vaak eBPF bedoeld, terwijl de klassieke BPF tegenwoordig bekend staat als cBPF.
eBPF breidt op verschillende manieren de klassieke virtuele machine BPF uit:
- Het steunt op moderne 64-bits architecturen. eBPF maakt gebruik van 64-bits registers en vergroot het aantal beschikbare registers van 2 (de accumulator en X) tot 10. In eBPF worden ook extra opcodes aangeboden (BPF_MOV, BPF_JNE, BPF_CALL...).
- Afgekoppeld van de netwerklaag. BPF was gekoppeld aan het pakketdatamodel. Omdat het werd gebruikt voor het filteren van pakketten, bevond de code zich in de subsystemen die netwerkinteracties mogelijk maakten. De virtuele machine eBPF is echter niet langer gebonden aan het datamodel en kan voor elke doeleinden worden gebruikt. Zo kan eBPF-programma nu worden aangesloten op tracepoint of kprobe. Dit opent de weg voor eBPF-instrumentatie, prestatieanalyse en vele andere toepassingen in de context van andere kernel-subsystemen. Nu bevindt de eBPF-code zich op zijn eigen pad: kernel/bpf.
- Wereldwijde dataopslagplaatsen genaamd Kaarten. Kaarten zijn opslagplaatsen van het type 'sleutel-waarde', die gegevensuitwisseling tussen gebruikersruimte en kernelruimte mogelijk maken. eBPF biedt kaarten van verschillende typen.
- Hulpfuncties. In het bijzonder voor het herschrijven van pakketten, het berekenen van checksums of het klonen van pakketten. Deze functies worden binnen de kernel uitgevoerd en behoren niet tot de programma's in de gebruikersruimte. Bovendien kunnen vanuit eBPF-programma's systeemaanroepen worden gedaan.
- Eindoproepen. De grootte van een programma in eBPF is beperkt tot 4096 bytes. De mogelijkheid van een eindoproep stelt een eBPF-programma in staat de controle over te dragen aan een nieuw eBPF-programma en op deze manier deze beperking te omzeilen (tot 32 programma's kunnen op deze manier worden gekoppeld).
eBPF: voorbeeld
In de Linux-kernelbronnen zijn er verschillende voorbeelden voor eBPF. Deze zijn beschikbaar op het adres samples/bpf/. Om deze voorbeelden te compileren, voert u gewoon in:
$ sudo make samples/bpf/
Ik ga geen nieuw voorbeeld voor eBPF zelf schrijven, maar gebruik een van de monsters die beschikbaar zijn in samples/bpf/. Ik zal enkele secties van de code bekijken en uitleggen hoe het werkt. Als voorbeeld heb ik het programma gekozen tracex4.
Over het algemeen bestaat elk voorbeeld in samples/bpf/ uit twee bestanden. In dit geval:
tracex4_kern.c, bevat de broncode die in de kernel als eBPF-bayencode moet draaien.tracex4_gebruiker.c, bevat het programma uit de gebruikersruimte.
In dat geval moeten we compileren tracex4_kern.c in bytecode eBPF. Currently, there is gcc no server-side component for eBPF. Fortunately, clang it can output eBPF bytecode. gebruikt clang for compilation tracex4_kern.c into an object file.
As I mentioned earlier, one of the most interesting features of eBPF is maps. tracex4_kern defines one map:
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 ā one of many types of maps offered by eBPF. In this case, it is simply a hash. You may also notice the declaration of SEC("maps"). SEC is a macro used to create a new section in the binary file. Actually, in the example, tracex4_kern defines two more sections:
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;
// get IP address of the caller of 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;
} These two functions allow you to delete an entry from the map (kprobe/kmem_cache_free) and add a new entry to the map (kretprobe/kmem_cache_alloc_node). All function names written in capital letters correspond to macros defined in .
If I output the section dump of the object file, I should see that these new sections are already defined:
$ 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
Ook is er , the main program. In principle, this program listens for events kmem_cache_alloc_node. When such an event occurs, the corresponding eBPF code runs. The code saves the IP attribute of the object to the map, and then this object is cyclically displayed in the main program. For example:
$ sudo .\/tracex4
obj 0xffff8d6430f60a00 is 2 seconden oud, werd toegewezen aan ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 is 23 seconden oud, werd toegewezen aan ip ffffffff98090e8f
obj 0xffff8d5f80161780 is 6 seconden oud, werd toegewezen aan ip ffffffff98090e8f
How are the user-space program and the eBPF program related? During initialization, tracex4_gebruiker.c it loads the object file tracex4_kern.o using a function laad_bpf_bestand.
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;
} Tijdens uitvoering de sondes, gedefinieerd in het eBPF-bestand, worden toegevoegd aan /sys/kernel/debug/tracing/kprobe_events. Nu luisteren we naar deze gebeurtenissen, en ons programma kan iets doen wanneer ze zich voordoen.
$ 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 andere programma's in sample/bpf/ zijn vergelijkbaar opgebouwd. Ze bevatten altijd twee bestanden:
XXX_kern.c: eBPF-programma.XXX_user.c: hoofdprogramma.
Het eBPF-programma definieert kaarten en functies die aan Sectie zijn gekoppeld. Wanneer de kernel een evenement van een bepaald type genereert (bijvoorbeeld, tracepoint), worden de gekoppelde functies uitgevoerd. Kaarten zorgen voor gegevensuitwisseling tussen het kernelprogramma en het gebruikersruimteprogramma.
Conclusie
In dit artikel zijn BPF en eBPF in grote lijnen besproken. Ik weet dat er vandaag de dag veel informatie en bronnen over eBPF zijn, dus ik raad aan nog enkele materialen te bekijken voor verdere studie.
Aanbevolen lectuur:
- van Jonathan Corbet. Een introductie tot BPF en een verhaal over hoe het is geƫvolueerd naar eBPF.
- van Brendan Gregg. Artikel van de site LWN.net. Brendan tweet vaak over eBPF en houdt een lijst met bronnen over dit onderwerp bij op zijn .
- van Julia Evans. Opmerkingen bij de presentatie van Suchakra Sharma āThe BSD Packet Filter: A New Architecture for User-level Packet Captureā. De opmerkingen zijn goed en helpen echt om de dia's te begrijpen.
- van Ferris Ellis. Longread met , maar het is de moeite waard om te lezen. Een van de beste artikelen over eBPF die ik ben tegengekomen.
Bron: habr.com
