Cómo extender el kernel con eBPF

El filtro de paquetes de Berkeley extendido (eBPF) es una máquina virtual de kernel que ejecuta programas de eBPF proporcionados por el usuario para extender la funcionalidad del kernel. Estos programas se pueden conectar a sondas o eventos en el kernel y se pueden usar para recopilar estadísticas útiles del kernel, supervisarlo y depurarlo. Un programa es Se cargan en el kernel mediante la llamada de sistema bpf(2) y la proporciona el usuario. como un BLOB binario de instrucciones de la máquina de eBPF. El sistema de compilación de Android admite la compilación de programas C en eBPF con la sintaxis simple de archivos de compilación que se describe en este documento.

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

Android incluye un cargador y una biblioteca de eBPF que cargan programas de eBPF en el momento del inicio.

Cargador de BPF de Android

Durante el inicio de Android, todos los programas de eBPF ubicados en /system/etc/bpf/ se cargado. Estos programas son objetos binarios compilados por el sistema de compilación de Android. de programas de C y se acompañan de archivos Android.bp en el código fuente de Android de imágenes. El sistema de compilación almacena los objetos generados en /system/etc/bpf. para que esos objetos formen parte de la imagen del sistema.

Formato de un programa de eBPF C de Android

Un programa C de eBPF 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 also defines 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

donde:

  • name_of_my_map es el nombre de tu variable de mapa. Este nombre le informa al cargador de BPF el tipo de mapa que se debe crear y con qué parámetros. El encabezado bpf_helpers.h incluido proporciona esta definición de struct.
  • PROGTYPE/PROGNAME representa el tipo de programa. y el nombre del programa. El tipo de programa puede ser cualquiera de los que se indican en la siguiente tabla. Cuando no se incluye un tipo de programa, no hay una convención de nombres estricta para el programa. El proceso que lo adjunta solo debe conocer el nombre.

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

kprobe Engancha PROGFUNC en una instrucción de kernel mediante el kprobe. PROGNAME debe ser el nombre del kernel función kprobada. Consulta 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"), en la que sched es el nombre del subsistema de seguimiento y sched_switch es el nombre del evento de seguimiento. Verifica el kernel de eventos de seguimiento documentaciónpara obtener más información sobre los puntos de seguimiento.
filtro sk El programa funciona como un filtro de socket de red.
schedcls Programar funciones como clasificador de tráfico de red
cgroupskb, cgroupsock El programa se ejecuta cada vez que los procesos de un CGroup crean un socket AF_INET o AF_INET6.

Puedes 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 pueda conectado al evento de registro sched:sched_switch. Para obtener más información, consulta Cómo 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 LICENSE se usa para verificar si el programa es compatible con la licencia del kernel cuando usa las funciones de ayuda de BPF que proporciona el kernel. Especifica el nombre de la licencia de tu programa en formato de cadena, como LICENSE("GPL") o LICENSE("Apache 2.0").

Formato del archivo Android.bp

Para que el sistema de compilación de Android compile un programa .c de eBPF, debes Crea una entrada en el archivo Android.bp del proyecto. Por ejemplo, para compilar un programa C de eBPF llamado bpf_test.c, realiza la siguiente entrada en el archivo Android.bp de tu proyecto:

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

Esta entrada compila el programa C, lo que genera el objeto /system/etc/bpf/bpf_test.o. Durante el inicio, el sistema Android carga automáticamente el programa bpf_test.o en el kernel.

Archivos disponibles en sysfs

Durante el inicio, el sistema Android carga automáticamente todos los objetos eBPF de /system/etc/bpf/, crea los mapas que necesita el programa y fija el programa cargado con sus mapas en el sistema de archivos BPF. Luego, estos archivos se pueden usar para una interacción más detallada 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, suponiendo que PROGNAME es el nombre del programa y FILENAME es el nombre del archivo C de eBPF, el cargador de Android crea y fija cada programa en /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME.

    Por ejemplo, en el ejemplo anterior de punto de seguimiento sched_switch, en myschedtp.c, se crea un archivo del 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 C de eBPF, el cargador de Android crea y fija cada mapa a /sys/fs/bpf/map_FILENAME_MAPNAME.

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

  • bpf_obj_get() de la biblioteca BPF de Android muestra un descriptor de archivo del archivo /sys/fs/bpf fijado. Este descriptor de archivo se puede usar para otras operaciones, como leer mapas o adjuntar un programa a un punto de seguimiento.

Biblioteca BPF de Android

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

Cómo adjuntar programas a puntos de seguimiento

Los programas de punto de seguimiento se cargan automáticamente durante el inicio. Después de la carga, El programa de punto de seguimiento debe activarse con estos pasos:

  1. Llama a bpf_obj_get() para obtener el programa fd de la ubicación del archivo fijado. Para obtener más información, consulta los archivos disponibles en sysfs.
  2. Llama a bpf_attach_tracepoint() en la biblioteca de BPF y pásale el programa fd y el nombre del punto de seguimiento.

En la siguiente muestra de código, se 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));

Leer desde los mapas

Los mapas de BPF admiten estructuras o tipos de claves y valores complejos arbitrarios. El La biblioteca BPF de Android incluye una clase android::BpfMap que usa C++ para crear una instancia de BpfMap según el tipo de clave y valor del mapa en cuestión. En la muestra de código anterior, se demuestra cómo usar un BpfMap con la clave. y el valor como números enteros. Los números enteros también pueden ser estructuras arbitrarias.

Por lo tanto, la clase BpfMap con plantillas te permite definir un objeto BpfMap personalizado adecuado para el mapa en particular. A continuación, se puede acceder al mapa a través del funciones generadas de manera personalizada, que reconocen tipos, lo que permite un código más limpio.

Para obtener más información sobre BpfMap, consulta la Fuentes de Android.

Cómo depurar problemas

Durante el tiempo de inicio, 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. Si filtras 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 de eBPF.

Ejemplos de eBPF en Android

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

  • La netd eBPF C programa El daemon de red (netd) en Android lo usa para varios fines, como como el filtrado de socket y la recopilación de estadísticas. Para ver cómo se usa este programa, consulta las fuentes del monitor de tráfico de eBPF.

  • La time_in_state eBPF C programa calcula la cantidad de tiempo que una app de Android pasa en diferentes las frecuencias de la CPU, que se usa para calcular la potencia.

  • En Android 12, la gpu_mem eBPF C programa realiza un seguimiento del uso total de memoria de GPU para cada proceso y para todo el sistema. Este programa se usa para generar perfiles de memoria de GPU.