Hej, Habr! Vi meddelar att vi förbereder en bok "".

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 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 .
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. , 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 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: ). 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. 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 .
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Ä , 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 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:
- Jonathan Corbet. En introduktion till BPF och en berÀttelse om hur den har utvecklats till eBPF.
- Brendan Gregg. En artikel frÄn LWN.net. Brendan twittrar ofta om eBPF och har en resurslista om Àmnet. .
- Julia Evans. Kommentarer till presentationen av Suchakra Sharma "The BSD Packet Filter: A New Architecture for User-level Packet Capture". Kommentarerna Àr bra och hjÀlper verkligen till att förstÄ bilderna.
- Ferry Ellis. En lÄng artikel med , men den Àr vÀrd att lÀsa. En av de bÀsta artiklarna om eBPF som jag har stött pÄ.
KĂ€lla: habr.com
