Breve introducción a BPF y eBPF

Hola Habr! Le informamos que nos estamos preparando para lanzar el libro " Observabilidad de Linux con BPF ".


A medida que la máquina virtual BPF continúa evolucionando y se aplica activamente en la práctica, hemos traducido un artículo para usted que describe sus características principales y su estado actual.

En los últimos años, las herramientas y técnicas de programación han comenzado a ganar popularidad, diseñadas para compensar las limitaciones del kernel de Linux en los casos en que se requiere un procesamiento de paquetes de alto rendimiento. Uno de los métodos más populares de este tipo se llama omitir el kernel (omisión de kernel) y permite que los núcleos de capa de red que pasan realicen todo el procesamiento de paquetes desde el espacio del usuario. Omitir el núcleo también implica administrar la tarjeta de red desde el espacio del usuario . En otras palabras, cuando trabajamos con una tarjeta de red, confiamos en el controlador de espacio del usuario .

Al transferir el control total sobre la tarjeta de red al programa desde el espacio del usuario, reducimos los costos asociados con el kernel (cambio de contexto, procesamiento del nivel de red, interrupciones, etc.), lo cual es bastante importante cuando se trabaja a velocidades de 10 Gb / so más. El bypass del kernel más una combinación de otras características ( procesamiento por lotes ) y un ajuste preciso del rendimiento ( contabilidad NUMA , aislamiento de la CPU , etc.) corresponden a los principios básicos del procesamiento de red de alto rendimiento en el espacio del usuario. Quizás un ejemplo modelo de este nuevo enfoque para el procesamiento de paquetes es el DPDK de Intel ( Kit de desarrollo de plano de datos), aunque existen otras herramientas y técnicas bien conocidas, como VPP de Cisco (Vector Packet Processing), Netmap y, por supuesto, Snabb .

La organización de las interacciones de red en el espacio del usuario tiene una serie de desventajas:

  • El núcleo del sistema operativo es el nivel de abstracción para los recursos de hardware. Debido a que los programas de espacio de usuario deben administrar sus recursos directamente, también deben administrar su propio hardware. Esto a menudo significa que necesita programar sus propios controladores.
  • , , . , , , .
  • , .

En esencia, cuando se organizan interacciones de red en el espacio del usuario, se logra aumentar la productividad al transferir el procesamiento de paquetes desde el núcleo al espacio del usuario. XDP hace exactamente lo contrario: mueve los programas de red desde el espacio del usuario (filtros, convertidores, enrutamiento, etc.) al área del kernel. XDP nos permite realizar una función de red tan pronto como el paquete llega a la interfaz de red y antes de que comience a subir al subsistema de red del núcleo. Como resultado, la velocidad de procesamiento de paquetes aumenta significativamente. Sin embargo, ¿cómo permite el núcleo al usuario ejecutar sus programas en el espacio del núcleo? Antes de responder esta pregunta, veamos qué es BPF.

BPF y eBPF

A pesar del nombre no tan claro BPF (filtrado de paquetes, Berkeley), es, de hecho, un modelo de máquina virtual. Esta máquina virtual fue diseñada originalmente para manejar el filtrado de paquetes, de ahí el nombre.

Una de las herramientas más conocidas que utilizan BPF es tcpdump. Al capturar paquetes con, el tcpdumpusuario puede especificar una expresión para filtrar paquetes. Solo se capturarán los paquetes que coincidan con esta expresión. Por ejemplo, la expresión " tcp dst port 80" se aplica a todos los paquetes TCP que llegan al puerto 80. El compilador puede acortar esta expresión al convertirla en código de bytes BPF. Esto es lo que básicamente hace el programa anterior:

$ 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




  • Instrucción (000): descarga un paquete en el desplazamiento 12 como una palabra de 16 bits en la batería. Un desplazamiento de 12 corresponde a un paquete ethertype.
  • (001): 0x86dd, , ethertype- IPv6. true, (002), – (006).
  • (006): 0x800 (ethertype- IPv4). true, (007), – (015).

Y así sucesivamente, hasta que el programa de filtrado de paquetes devuelva un resultado. Esto suele ser un bulean. Devolver un valor distinto de cero (instrucción (014)) significa que el paquete se ha acercado, y devolver cero (instrucción (015)) significa que el paquete no ha llegado.

La máquina virtual BPF y su código de bytes fueron propuestos por Steve McCann y Van Jacobson a fines de 1992 cuando su artículo BSD Packet Filter: A New Architecture for Packet Capture at User Level , se presentó por primera vez en una conferencia de Usenix en el invierno de 1993.

Como BPF es una máquina virtual, define el entorno en el que se ejecutan los programas. Además del código de bytes, también define un modelo de memoria por lotes (las instrucciones de arranque se aplican implícitamente al paquete), los registros (A y X; registros de batería e índice), almacenamiento de memoria temporal y un contador de programa implícito. Curiosamente, el código de bytes BPF fue modelado después del Motorola 6502 ISA. Como Steve McCann recordó en su charla plenaria en Sharkfest '11, había estado familiarizado con la construcción 6502 desde la escuela secundaria cuando programó en Apple II, y este conocimiento influyó en su trabajo en el diseño del código de bytes BPF.

El soporte para BPF se implementa en el kernel de Linux en la versión v2.5 y superior, agregado principalmente por los esfuerzos de Jay Schullist. El código BPF permaneció sin cambios hasta 2011, cuando Eric Dumazett rehizo el intérprete BPF para que funcione en modo JIT (Fuente: JIT para filtros de paquetes ). Después de eso, el núcleo, en lugar de interpretar el código de bytes BPF, podría convertir directamente los programas BPF a la arquitectura de destino: x86, ARM, MIPS, etc.

Más tarde, en 2014, Alexey Starovoitov propuso un nuevo mecanismo JIT para BPF. De hecho, este nuevo JIT se ha convertido en la nueva arquitectura basada en BPF y se llama eBPF. Creo que durante algún tiempo ambas máquinas virtuales coexistieron, pero actualmente el filtrado de paquetes se implementa en base a eBPF. De hecho, en muchos ejemplos de documentación moderna, se entiende que BPF significa eBPF, y el BPF clásico ahora se conoce como cBPF.

eBPF extiende la clásica máquina virtual BPF de varias maneras:

  • Basado en arquitecturas modernas de 64 bits. eBPF utiliza registros de 64 bits y aumenta el número de registros disponibles de 2 (batería y X) a 10. eBPF también proporciona códigos de operación adicionales (BPF_MOV, BPF_JNE, BPF_CALL ...).
  • . BPF . , , . , eBPF . , eBPF tracepoint kprobe. eBPF, . eBPF : kernel/bpf.
  • . – «-», . eBPF .
  • . , , . . , eBPF .
  • . eBPF 4096 . eBPF eBPF- ( 32 ).

eBPF: un ejemplo

Hay varios ejemplos para eBPF en las fuentes del kernel de Linux. Están disponibles en samples / bpf /. Para compilar estos ejemplos, simplemente ingrese:

$ sudo make samples/bpf/

No escribiré un nuevo ejemplo para eBPF, pero usaré una de las muestras disponibles en samples / bpf /. Veré algunas secciones del código y explicaré cómo funciona. Como ejemplo, elegí un programa tracex4.

En general, cada uno de los ejemplos en samples / bpf / consta de dos archivos. En este caso:

  • tracex4_kern.c, contiene el código fuente que debe ejecutarse en el núcleo como código de bytes eBPF.
  • tracex4_user.c, contiene un programa desde el espacio del usuario.

En este caso, necesitamos compilar el tracex4_kern.ceBPF en bytecode. Actualmente gccno hay ninguna parte del servidor para eBPF. Afortunadamente, clangpuede emitir bytecode eBPF. Makefileutiliza clangpara compilar tracex4_kern.cun archivo objeto.

Mencioné anteriormente que una de las características más interesantes de eBPF son las tarjetas. tracex4_kern define 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- Uno de los muchos tipos de tarjetas que ofrece eBPF. En este caso, es solo un hash. También puede haber notado un anuncio SEC("maps"). SEC es una macro utilizada para crear una nueva sección de un archivo binario. En realidad, el ejemplo tracex4_kerndefine dos secciones 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;
}   

Estas dos funciones le permiten eliminar una entrada de la tarjeta ( kprobe/kmem_cache_free) y agregar un nuevo registro ( kretprobe/kmem_cache_alloc_node) a la tarjeta . Todos los nombres de funciones escritos en mayúsculas corresponden a las macros definidas en bpf_helpers.h.

Si produzco un volcado de secciones del archivo de objeto, debería ver que estas nuevas secciones ya están definidas: Todavía allí , el programa principal. Básicamente, este programa escucha eventos . Cuando ocurre tal evento, se ejecuta el código eBPF correspondiente. El código almacena el atributo IP del objeto en el mapa, y luego este objeto se muestra cíclicamente en el programa principal. Ejemplo: ¿Cómo se relacionan el programa de espacio de usuario y el programa eBPF? En la inicialización, carga un archivo de objeto usando una función .

$ 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


tracex4_user.ckmem_cache_alloc_node

$ 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


tracex4_user.ctracex4_kern.oload_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;
}

Cuando se ejecuta, load_bpf_filese agregan las sondas definidas en el archivo eBPF /sys/kernel/debug/tracing/kprobe_events. Ahora estamos escuchando estos eventos, y nuestro programa puede hacer algo cuando suceden. Todos los demás programas en sample / bpf / están estructurados de manera similar. Siempre tienen dos archivos:

$ 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




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

El programa eBPF define las tarjetas y funciones asociadas con la sección. Cuando el kernel lanza un evento de cierto tipo (por ejemplo, tracepoint), se ejecutan las funciones enlazadas. Los mapas proporcionan intercambio de datos entre el programa del núcleo y el programa de espacio de usuario.

Conclusión

Este artículo describió BPF y eBPF. Sé que hoy hay mucha información y recursos sobre eBPF, por lo que recomiendo algunos materiales más para estudiar más.

Recomiendo leer:


All Articles