Ampliando el kernel con eBPF

Extended Berkeley Packet Filter (eBPF) es una máquina virtual integrada en el kernel que ejecuta programas eBPF proporcionados por el usuario para ampliar la funcionalidad del kernel. Estos programas pueden conectarse a sondas o eventos en el kernel y usarse para recopilar estadísticas útiles del kernel, monitorear y depurar. Un programa se carga en el kernel utilizando la llamada al sistema bpf(2) y el usuario lo proporciona como un blob binario de instrucciones de máquina eBPF. El sistema de compilación de Android admite la compilación de programas C en eBPF utilizando la sintaxis simple del archivo de compilación que se describe en este documento.

Puede encontrar más información sobre la arquitectura y los componentes internos de eBPF en la página de eBPF de Brendan Gregg .

Android incluye un cargador y una biblioteca eBPF que carga programas eBPF en el momento del arranque.

Cargador BPF de Android

Durante el arranque de Android, se cargan todos los programas eBPF ubicados en /system/etc/bpf/ . Estos programas son objetos binarios creados por el sistema de compilación de Android a partir de programas C y van acompañados de archivos Android.bp en el árbol de código fuente de Android. El sistema de compilación almacena los objetos generados en /system/etc/bpf y esos objetos pasan a formar parte de la imagen del sistema.

Formato de un programa Android eBPF C

Un programa eBPF C debe tener el siguiente formato:

#include <bpf_helpers.h>

/* Define one or more maps in the maps section, for example
 * define a map of type array int -> uint32_t, with 10 entries
 */
DEFINE_BPF_MAP(name_of_my_map, ARRAY, int, uint32_t, 10);

/* this will also define type-safe accessors:
 *   value * bpf_name_of_my_map_lookup_elem(&key);
 *   int bpf_name_of_my_map_update_elem(&key, &value, flags);
 *   int bpf_name_of_my_map_delete_elem(&key);
 * as such it is heavily suggested to use lowercase *_map names.
 * Also note that due to compiler deficiencies you cannot use a type
 * of 'struct foo' but must instead use just 'foo'.  As such structs
 * must not be defined as 'struct foo {}' and must instead be
 * 'typedef struct {} foo'.
 */

DEFINE_BPF_PROG("PROGTYPE/PROGNAME", AID_*, AID_*, PROGFUNC)(..args..) {
   <body-of-code
    ... read or write to MY_MAPNAME
    ... do other things
   >
}

LICENSE("GPL"); // or other license

Dónde:

  • name_of_my_map es el nombre de la variable de su mapa. Este nombre informa al cargador BPF del tipo de mapa a crear y con qué parámetros. Esta definición de estructura la proporciona el encabezado bpf_helpers.h incluido.
  • PROGTYPE/PROGNAME representa el tipo de programa y el nombre del programa. El tipo de programa puede ser cualquiera de los enumerados en la siguiente tabla. Cuando un tipo de programa no aparece en la lista, no existe una convención de nomenclatura estricta para el programa; el nombre sólo necesita ser conocido por el proceso que adjunta el programa.

  • PROGFUNC es una función que, cuando se compila, se coloca en una sección del archivo resultante.

sonda k Conecta PROGFUNC a una instrucción del kernel utilizando la infraestructura kprobe. PROGNAME debe ser el nombre de la función del núcleo que se está probando. Consulte la documentación del kernel de kprobe para obtener más información sobre kprobes.
punto de seguimiento Engancha PROGFUNC a un punto de seguimiento. PROGNAME debe tener el formato SUBSYSTEM/EVENT . Por ejemplo, una sección de punto de seguimiento para adjuntar funciones a eventos de cambio de contexto del programador sería SEC("tracepoint/sched/sched_switch") , donde sched es el nombre del subsistema de seguimiento y sched_switch es el nombre del evento de seguimiento. Consulte la documentación del kernel de eventos de seguimiento para obtener más información sobre los puntos de seguimiento.
filtrosk El programa funciona como un filtro de socket de red.
horarios El programa funciona como clasificador de tráfico de red.
cgroupskb, cgroupsock El programa se ejecuta cada vez que los procesos en un CGroup crean un socket AF_INET o AF_INET6.

Se pueden encontrar tipos adicionales en el código fuente del cargador .

Por ejemplo, el siguiente programa myschedtp.c agrega información sobre el PID de tarea más reciente que se ejecutó en una CPU en particular. Este programa logra su objetivo creando un mapa y definiendo una función tp_sched_switch que se puede adjuntar al evento de seguimiento sched:sched_switch . Para obtener más información, consulte Adjuntar programas a puntos de seguimiento .

#include <linux/bpf.h>
#include <stdbool.h>
#include <stdint.h>
#include <bpf_helpers.h>

DEFINE_BPF_MAP(cpu_pid_map, ARRAY, int, uint32_t, 1024);

struct switch_args {
    unsigned long long ignore;
    char prev_comm[16];
    int prev_pid;
    int prev_prio;
    long long prev_state;
    char next_comm[16];
    int next_pid;
    int next_prio;
};

DEFINE_BPF_PROG("tracepoint/sched/sched_switch", AID_ROOT, AID_SYSTEM, tp_sched_switch)
(struct switch_args *args) {
    int key;
    uint32_t val;

    key = bpf_get_smp_processor_id();
    val = args->next_pid;

    bpf_cpu_pid_map_update_elem(&key, &val, BPF_ANY);
    return 1; // return 1 to avoid blocking simpleperf from receiving events
}

LICENSE("GPL");

La macro LICENCIA se utiliza para verificar si el programa es compatible con la licencia del kernel cuando el programa hace uso de las funciones auxiliares de BPF proporcionadas por el kernel. Especifique el nombre de la licencia de su programa en forma de cadena, como LICENSE("GPL") o LICENSE("Apache 2.0") .

Formato del archivo Android.bp

Para que el sistema de compilación de Android cree un programa eBPF .c , debe crear una entrada en el archivo Android.bp del proyecto. Por ejemplo, para crear un programa eBPF C llamado bpf_test.c , realice la siguiente entrada en el archivo Android.bp de su proyecto:

bpf {
    name: "bpf_test.o",
    srcs: ["bpf_test.c"],
    cflags: [
        "-Wall",
        "-Werror",
    ],
}

Esta entrada compila el programa C dando como resultado el objeto /system/etc/bpf/bpf_test.o . Al arrancar, el sistema Android carga automáticamente el programa bpf_test.o en el kernel.

Archivos disponibles en sysfs

Durante el arranque, el sistema Android carga automáticamente todos los objetos eBPF desde /system/etc/bpf/ , crea los mapas que el programa necesita y fija el programa cargado con sus mapas al sistema de archivos BPF. Estos archivos se pueden utilizar para una mayor interacción con el programa eBPF o para leer mapas. Esta sección describe las convenciones utilizadas para nombrar estos archivos y sus ubicaciones en sysfs.

Se crean y fijan los siguientes archivos:

  • Para cualquier programa cargado, asumiendo que PROGNAME es el nombre del programa y FILENAME es el nombre del archivo eBPF C, el cargador de Android crea y fija cada programa en /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME .

    Por ejemplo, para el ejemplo anterior del punto de seguimiento sched_switch en myschedtp.c , se crea un archivo de programa y se fija en /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch .

  • Para cualquier mapa creado, suponiendo que MAPNAME es el nombre del mapa y FILENAME es el nombre del archivo eBPF C, el cargador de Android crea y fija cada mapa en /sys/fs/bpf/map_FILENAME_MAPNAME .

    Por ejemplo, para el ejemplo anterior del punto de seguimiento sched_switch en myschedtp.c , se crea un archivo de mapa y se fija en /sys/fs/bpf/map_myschedtp_cpu_pid_map .

  • bpf_obj_get() en la biblioteca BPF de Android devuelve un descriptor de archivo del archivo /sys/fs/bpf anclado. Este descriptor de archivo se puede utilizar para operaciones adicionales, como leer mapas o adjuntar un programa a un punto de seguimiento.

Biblioteca BPF de Android

La biblioteca BPF de Android se llama libbpf_android.so y es parte de la imagen del sistema. Esta biblioteca proporciona al usuario la funcionalidad eBPF de bajo nivel necesaria para crear y leer mapas, crear sondas, puntos de seguimiento y búferes de rendimiento.

Adjuntar programas a puntos de seguimiento

Los programas de Tracepoint se cargan automáticamente al arrancar. Después de la carga, el programa tracepoint debe activarse siguiendo estos pasos:

  1. Llame bpf_obj_get() para obtener el programa fd de la ubicación del archivo anclado. Para obtener más información, consulte Archivos disponibles en sysfs .
  2. Llame bpf_attach_tracepoint() en la biblioteca BPF, pasándole el programa fd y el nombre del punto de seguimiento.

El siguiente ejemplo de código muestra cómo adjuntar el punto de seguimiento sched_switch definido en el archivo fuente myschedtp.c anterior (no se muestra la verificación de errores):

  char *tp_prog_path = "/sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch";
  char *tp_map_path = "/sys/fs/bpf/map_myschedtp_cpu_pid";

  // Attach tracepoint and wait for 4 seconds
  int mProgFd = bpf_obj_get(tp_prog_path);
  int mMapFd = bpf_obj_get(tp_map_path);
  int ret = bpf_attach_tracepoint(mProgFd, "sched", "sched_switch");
  sleep(4);

  // Read the map to find the last PID that ran on CPU 0
  android::bpf::BpfMap<int, int> myMap(mMapFd);
  printf("last PID running on CPU %d is %d\n", 0, myMap.readValue(0));

Leyendo de los mapas

Los mapas BPF admiten estructuras o tipos de claves y valores complejos y arbitrarios. La biblioteca BPF de Android incluye una clase android::BpfMap que utiliza plantillas de C++ para crear instancias BpfMap según la clave y el tipo de valor del mapa en cuestión. El ejemplo de código anterior demuestra el uso de BpfMap con clave y valor como números enteros. Los números enteros también pueden ser estructuras arbitrarias.

Por lo tanto, la clase BpfMap con plantilla facilita la definición de un objeto BpfMap personalizado adecuado para el mapa en particular. Luego se puede acceder al mapa mediante funciones generadas personalizadas, que reconocen el tipo, lo que da como resultado un código más limpio.

Para obtener más información sobre BpfMap , consulte las fuentes de Android .

Problemas de depuración

Durante el arranque, se registran varios mensajes relacionados con la carga de BPF. Si el proceso de carga falla por algún motivo, se proporciona un mensaje de registro detallado en logcat. Al filtrar los registros de logcat por "bpf", se imprimen todos los mensajes y cualquier error detallado durante el tiempo de carga, como los errores del verificador eBPF.

Ejemplos de eBPF en Android

Los siguientes programas en AOSP proporcionan ejemplos adicionales del uso de eBPF:

  • El demonio de red (netd) en Android utiliza el programa netd eBPF C para diversos fines, como filtrado de sockets y recopilación de estadísticas. Para ver cómo se utiliza este programa, consulte las fuentes del monitor de tráfico eBPF .

  • El programa time_in_state eBPF C calcula la cantidad de tiempo que pasa una aplicación de Android en diferentes frecuencias de CPU, que se utiliza para calcular la potencia.

  • En Android 12, el programa gpu_mem eBPF C rastrea el uso total de memoria de la GPU para cada proceso y para todo el sistema. Este programa se utiliza para crear perfiles de memoria GPU.