Una breu introducció a BPF i eBPF

Hola Habr! Us informem que estem preparant el llançament d'un llibre "Observabilitat de Linux amb BPF".

Una breu introducció a BPF i eBPF
A mesura que la màquina virtual BPF continua evolucionant i s'utilitza activament a la pràctica, us hem traduït un article que descriu les seves principals característiques i l'estat actual.

En els últims anys, les eines i tècniques de programació han guanyat popularitat per compensar les limitacions del nucli Linux en els casos en què es requereix un processament de paquets d'alt rendiment. Un dels mètodes més populars d'aquest tipus es diu bypass del nucli (bypass del nucli) i permet, saltant la capa de xarxa del nucli, realitzar tot el processament de paquets des de l'espai d'usuari. Eludir el nucli també implica gestionar la targeta de xarxa des espai d'usuari. En altres paraules, quan treballem amb una targeta de xarxa, confiem en el controlador espai d'usuari.

En transferir el control total de la targeta de xarxa a un programa d'espai d'usuari, reduïm la sobrecàrrega causada pel nucli (canvis de context, processament de capa de xarxa, interrupcions, etc.), que és força important quan s'executen a velocitats de 10 Gb/s o més alt. Evitant el nucli més una combinació d'altres característiques (processament per lots) i una acurada sintonització del rendiment (Comptabilitat NUMA, Aïllament de la CPU, etc.) s'ajusten als fonaments bàsics de les xarxes d'espai d'usuari d'alt rendiment. Potser un exemple exemplar d'aquest nou enfocament del processament de paquets és DPDK d'Intel (Kit de desenvolupament del pla de dades), tot i que hi ha altres eines i tècniques conegudes, com ara VPP de Cisco (Vector Packet Processing), Netmap i, per descomptat, enganxada.

L'organització de les interaccions de xarxa a l'espai d'usuari té una sèrie d'inconvenients:

  • Un nucli del sistema operatiu és una capa d'abstracció per als recursos de maquinari. Com que els programes d'espai d'usuari han de gestionar els seus recursos directament, també han de gestionar el seu propi maquinari. Això sovint significa programar els vostres propis controladors.
  • Com que estem renunciant completament a l'espai del nucli, també estem renunciant a tota la funcionalitat de xarxa proporcionada pel nucli. Els programes d'espai d'usuari han de tornar a implementar característiques que ja poden ser proporcionades pel nucli o el sistema operatiu.
  • Els programes funcionen en mode sandbox, la qual cosa limita seriosament la seva interacció i impedeix que s'integrin amb altres parts del sistema operatiu.

En essència, quan es treballa en xarxa a l'espai d'usuari, els guanys de rendiment s'aconsegueixen movent el processament de paquets del nucli a l'espai d'usuari. XDP fa exactament el contrari: mou els programes de xarxa des de l'espai d'usuari (filtres, convertidors, encaminament, etc.) a l'àrea del nucli. XDP ens permet executar la funció de xarxa tan bon punt el paquet arriba a la interfície de xarxa i abans que comenci a viatjar fins al subsistema de xarxa del nucli. Com a resultat, la velocitat de processament de paquets augmenta significativament. Tanmateix, com permet el nucli a l'usuari executar els seus programes a l'espai del nucli? Abans de respondre aquesta pregunta, mirem què és BPF.

BPF i eBPF

Malgrat el nom no del tot clar, BPF (Packet Filtering, Berkeley) és, de fet, un model de màquina virtual. Aquesta màquina virtual va ser dissenyada originalment per gestionar el filtratge de paquets, d'aquí el nom.

Una de les eines més conegudes que utilitzen BPF és tcpdump. En capturar paquets amb tcpdump l'usuari pot especificar una expressió per al filtratge de paquets. Només es capturaran els paquets que coincideixin amb aquesta expressió. Per exemple, l'expressió "tcp dst port 80” es refereix a tots els paquets TCP que arriben al port 80. El compilador pot escurçar aquesta expressió convertint-la a bytecode BPF.

$ 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

Això és bàsicament el que fa el programa anterior:

  • Instrucció (000): Carrega el paquet al desplaçament 12, com a paraula de 16 bits, a l'acumulador. L'offset 12 correspon a l'ethertype del paquet.
  • Instrucció (001): compara el valor de l'acumulador amb 0x86dd, és a dir, amb el valor ethertype per a IPv6. Si el resultat és cert, aleshores el comptador del programa passa a la instrucció (002), i si no, llavors a (006).
  • Instrucció (006): compara el valor amb 0x800 (valor ethertype per a IPv4). Si la resposta és certa, el programa passa a (007), si no, llavors a (015).

I així successivament, fins que el programa de filtratge de paquets retorni un resultat. Normalment és booleà. Si retorna un valor diferent de zero (instrucció (014)) significa que el paquet coincideix, i retornar zero (instrucció (015)) significa que el paquet no coincideix.

La màquina virtual BPF i el seu codi de bytes van ser proposats per Steve McCann i Van Jacobson a finals de 1992 quan va sortir el seu article. Filtre de paquets BSD: nova arquitectura per a la captura de paquets a nivell d'usuari, per primera vegada aquesta tecnologia es va presentar a la conferència Usenix a l'hivern de 1993.

Com que BPF és una màquina virtual, defineix l'entorn en què s'executen els programes. A més del bytecode, també defineix un model de memòria de paquets (les instruccions de càrrega s'apliquen implícitament a un paquet), registres (A i X; registres d'índex i acumulador), emmagatzematge de memòria temporal i un comptador de programa implícit. Curiosament, el bytecode BPF es va modelar després del Motorola 6502 ISA. Com recordava Steve McCann en el seu informe del ple al Sharkfest '11, estava familiaritzat amb la compilació 6502 de l'escola secundària quan programava a l'Apple II, i aquest coneixement va influir en el seu treball de disseny del bytecode BPF.

El suport de BPF s'implementa al nucli Linux a la versió v2.5 i posteriors, afegit principalment per Jay Schullist. El codi BPF es va mantenir sense canvis fins al 2011, quan Eric Dumaset va redissenyar l'intèrpret BPF perquè funcionés en mode JIT (Font: JIT per a filtres de paquets). Després d'això, en lloc d'interpretar el bytecode BPF, el nucli podria convertir directament els programes BPF a l'arquitectura de destinació: x86, ARM, MIPS, etc.

Més tard, el 2014, Alexei Starovoitov va proposar un nou mecanisme JIT per a BPF. De fet, aquest nou JIT es va convertir en una nova arquitectura basada en BPF i es va anomenar eBPF. Crec que les dues màquines virtuals van coexistir durant algun temps, però actualment el filtratge de paquets s'implementa a sobre de l'eBPF. De fet, en molts exemples de documentació moderna, el BPF es coneix com a eBPF, i el BPF clàssic es coneix avui com a cBPF.

eBPF amplia la màquina virtual clàssica de BPF de diverses maneres:

  • Es basa en arquitectures modernes de 64 bits. eBPF utilitza registres de 64 bits i augmenta el nombre de registres disponibles de 2 (acumulador i X) a 10. eBPF també proporciona codis operatius addicionals (BPF_MOV, BPF_JNE, BPF_CALL...).
  • Desconnectat del subsistema de la capa de xarxa. BPF estava lligat al model de dades per lots. Com que s'utilitzava per filtrar paquets, el seu codi estava al subsistema que proporcionava les interaccions de xarxa. Tanmateix, la màquina virtual eBPF ja no està lligada a un model de dades i es pot utilitzar per a qualsevol propòsit. Per tant, ara el programa eBPF es pot connectar a tracepoint o a kprobe. Això obre la porta a la instrumentació eBPF, l'anàlisi de rendiment i molts altres casos d'ús en el context d'altres subsistemes del nucli. Ara el codi eBPF es troba al seu propi camí: kernel/bpf.
  • Magatzems de dades globals anomenats Maps. Els mapes són magatzems de valor-clau que proporcionen intercanvi de dades entre l'espai d'usuari i l'espai del nucli. eBPF ofereix diversos tipus de targetes.
  • Funcions secundàries. En particular, per sobreescriure un paquet, calcular una suma de verificació o clonar un paquet. Aquestes funcions s'executen dins del nucli i no pertanyen als programes d'espai d'usuari. A més, es poden fer trucades al sistema des dels programes eBPF.
  • Finalitzar les trucades. La mida del programa a eBPF està limitada a 4096 bytes. La funció de finalització de trucada permet que un programa eBPF transfereixi el control a un nou programa eBPF i, per tant, salti aquesta limitació (es poden encadenar fins a 32 programes d'aquesta manera).

Exemple d'eBPF

Hi ha diversos exemples d'eBPF a les fonts del nucli de Linux. Estan disponibles a samples/bpf/. Per compilar aquests exemples, només cal que escriviu:

$ sudo make samples/bpf/

Jo mateix no escriuré un exemple nou per a eBPF, però utilitzaré una de les mostres disponibles a samples/bpf/. Miraré algunes parts del codi i explicaré com funciona. Com a exemple, vaig triar el programa tracex4.

En general, cadascun dels exemples de samples/bpf/ consta de dos fitxers. En aquest cas:

  • tracex4_kern.c, conté el codi font que s'executarà al nucli com a bytecode eBPF.
  • tracex4_user.c, conté un programa des de l'espai d'usuari.

En aquest cas, hem de compilar tracex4_kern.c al codi de bytes eBPF. De moment en gcc no hi ha cap part del servidor per a eBPF. Afortunadament, clang pot produir codi de bytes eBPF. Makefile usos clang per compilar tracex4_kern.c al fitxer objecte.

He esmentat anteriorment que una de les característiques més interessants d'eBPF són els mapes. tracex4_kern defineix un mapa:

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 és un dels molts tipus de targetes que ofereix eBPF. En aquest cas, només és un hash. Potser també us heu adonat de l'anunci SEC("maps"). SEC és una macro que s'utilitza per crear una nova secció d'un fitxer binari. De fet, a l'exemple tracex4_kern es defineixen dos apartats més:

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

Aquestes dues funcions us permeten eliminar una entrada del mapa (kprobe/kmem_cache_free) i afegeix una nova entrada al mapa (kretprobe/kmem_cache_alloc_node). Tots els noms de funcions escrits en majúscules corresponen a macros definides a bpf_helpers.h.

Si bolco les seccions del fitxer objecte, hauria de veure que aquestes noves seccions ja estan definides:

$ 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

També hi ha tracex4_user.c, programa principal. Bàsicament, aquest programa escolta esdeveniments kmem_cache_alloc_node. Quan es produeix un esdeveniment d'aquest tipus, s'executa el codi eBPF corresponent. El codi desa l'atribut IP de l'objecte en un mapa i, a continuació, l'objecte es passa en bucle al programa principal. Exemple:

$ 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

Com es relacionen el programa d'espai d'usuari i el programa eBPF? A la inicialització tracex4_user.c carrega el fitxer d'objectes tracex4_kern.o utilitzant la funció 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;
}

Mentre feia load_bpf_file s'afegeixen les sondes definides al fitxer eBPF /sys/kernel/debug/tracing/kprobe_events. Ara escoltem aquests esdeveniments i el nostre programa pot fer alguna cosa quan succeeixin.

$ 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

Tots els altres programes de sample/bpf/ estan estructurats de manera similar. Sempre contenen dos fitxers:

  • XXX_kern.c: programa eBPF.
  • XXX_user.c: programa principal.

El programa eBPF defineix els mapes i les funcions associades a una secció. Quan el nucli emet un esdeveniment d'un tipus determinat (per exemple, tracepoint), s'executen les funcions lligades. Els mapes proporcionen comunicació entre un programa del nucli i un programa d'espai d'usuari.

Conclusió

En aquest article, es parla de BPF i eBPF en termes generals. Sé que avui hi ha molta informació i recursos sobre eBPF, així que recomanaré alguns materials més per a un estudi posterior.

Recomano llegir:

Font: www.habr.com

Afegeix comentari