Estensione del kernel con eBPF

Extended Berkeley Packet Filter (eBPF) è una macchina virtuale interna al kernel che esegue programmi eBPF forniti dall'utente per estendere le funzionalità del kernel. Questi programmi possono essere collegati a sondaggi o eventi nel kernel e utilizzati per raccogliere utili statistiche del kernel, monitorare ed eseguire il debug. Un programma viene caricato nel kernel utilizzando la chiamata di sistema bpf(2) ed è fornito dall'utente come un blob binario di istruzioni macchina eBPF. Il sistema di build Android supporta la compilazione di programmi C in eBPF utilizzando la semplice sintassi del file di build descritta in questo documento.

Maggiori informazioni sugli interni e sull'architettura di eBPF possono essere trovate nella pagina eBPF di Brendan Gregg .

Android include un caricatore eBPF e una libreria che carica i programmi eBPF all'avvio.

Caricatore BPF Android

Durante l'avvio di Android, vengono caricati tutti i programmi eBPF che si trovano in /system/etc/bpf/ . Questi programmi sono oggetti binari creati dal sistema di build Android da programmi C e sono accompagnati da file Android.bp nell'albero dei sorgenti Android. Il sistema di compilazione memorizza gli oggetti generati in /system/etc/bpf e tali oggetti diventano parte dell'immagine del sistema.

Formato di un programma Android eBPF C

Un programma C eBPF deve avere il seguente 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

Dove:

  • name_of_my_map è il nome della variabile della mappa. Questo nome informa il caricatore BPF del tipo di mappa da creare e con quali parametri. Questa definizione di struttura è fornita dall'intestazione bpf_helpers.h inclusa.
  • PROGTYPE/PROGNAME rappresenta il tipo di programma e il nome del programma. Il tipo di programma può essere uno qualsiasi di quelli elencati nella tabella seguente. Quando un tipo di programma non è elencato, non esiste una convenzione di denominazione rigida per il programma; è sufficiente che il nome sia noto al processo che associa il programma.

  • PROGFUNC è una funzione che, una volta compilata, viene inserita in una sezione del file risultante.

kprobe Collega PROGFUNC a un'istruzione del kernel utilizzando l'infrastruttura kprobe. PROGNAME deve essere il nome della funzione del kernel da kprobed. Fare riferimento alla documentazione del kernel kprobe per ulteriori informazioni su kprobes.
tracepoint Aggancia PROGFUNC a un tracepoint. PROGNAME deve essere nel formato SUBSYSTEM/EVENT . Ad esempio, una sezione tracepoint per allegare funzioni agli eventi di cambio di contesto dello scheduler sarebbe SEC("tracepoint/sched/sched_switch") , dove sched è il nome del sottosistema di traccia e sched_switch è il nome dell'evento di traccia. Controlla la documentazione del kernel degli eventi di traccia per ulteriori informazioni sui tracepoint.
skfilter Il programma funziona come un filtro socket di rete.
schedcls Il programma funziona come un classificatore del traffico di rete.
cgroupskb, cgroupsock Il programma viene eseguito ogni volta che i processi in un CGroup creano un socket AF_INET o AF_INET6.

Ulteriori tipi possono essere trovati nel codice sorgente del Loader .

Ad esempio, il seguente programma myschedtp.c aggiunge informazioni sull'ultimo PID dell'attività eseguita su una particolare CPU. Questo programma raggiunge il suo obiettivo creando una mappa e definendo una funzione tp_sched_switch che può essere allegata all'evento di traccia sched:sched_switch . Per ulteriori informazioni, vedere Collegamento di programmi ai tracepoint .

#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 viene utilizzata per verificare se il programma è compatibile con la licenza del kernel quando il programma fa uso delle funzioni di supporto BPF fornite dal kernel. Specifica il nome della licenza del tuo programma sotto forma di stringa, come LICENSE("GPL") o LICENSE("Apache 2.0") .

Formato del file Android.bp

Affinché il sistema di build Android possa creare un programma eBPF .c , è necessario creare una voce nel file Android.bp del progetto. Ad esempio, per creare un programma C eBPF denominato bpf_test.c , inserisci la seguente voce nel file Android.bp del tuo progetto:

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

Questa voce compila il programma C risultante nell'oggetto /system/etc/bpf/bpf_test.o . All'avvio, il sistema Android carica automaticamente il programma bpf_test.o nel kernel.

File disponibili in sysfs

Durante l'avvio, il sistema Android carica automaticamente tutti gli oggetti eBPF da /system/etc/bpf/ , crea le mappe necessarie al programma e aggiunge il programma caricato con le sue mappe al file system BPF. Questi file possono quindi essere utilizzati per ulteriori interazioni con il programma eBPF o per leggere le mappe. Questa sezione descrive le convenzioni utilizzate per denominare questi file e le loro posizioni in sysfs.

Vengono creati e bloccati i seguenti file:

  • Per tutti i programmi caricati, presupponendo che PROGNAME sia il nome del programma e FILENAME sia il nome del file eBPF C, il caricatore Android crea e blocca ciascun programma in /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME .

    Ad esempio, per il precedente esempio di tracepoint sched_switch in myschedtp.c , un file di programma viene creato e aggiunto a /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch .

  • Per tutte le mappe create, presupponendo che MAPNAME sia il nome della mappa e FILENAME sia il nome del file eBPF C, il caricatore Android crea e aggiunge ciascuna mappa a /sys/fs/bpf/map_FILENAME_MAPNAME .

    Ad esempio, per il precedente esempio di tracepoint sched_switch in myschedtp.c , un file di mappa viene creato e aggiunto a /sys/fs/bpf/map_myschedtp_cpu_pid_map .

  • bpf_obj_get() nella libreria BPF di Android restituisce un descrittore di file dal file /sys/fs/bpf bloccato. Questo descrittore di file può essere utilizzato per ulteriori operazioni, come leggere mappe o allegare un programma a un tracepoint.

Libreria BPF Android

La libreria BPF di Android si chiama libbpf_android.so e fa parte dell'immagine di sistema. Questa libreria fornisce all'utente le funzionalità eBPF di basso livello necessarie per creare e leggere mappe, creare sonde, tracepoint e buffer di perf.

Collegamento di programmi ai tracepoint

I programmi Tracepoint vengono caricati automaticamente all'avvio. Dopo il caricamento, il programma Tracepoint deve essere attivato seguendo questi passaggi:

  1. Chiama bpf_obj_get() per ottenere il programma fd dalla posizione del file bloccato. Per ulteriori informazioni, fare riferimento ai file disponibili in sysfs .
  2. Chiama bpf_attach_tracepoint() nella libreria BPF, passandogli il programma fd e il nome del tracepoint.

Il seguente esempio di codice mostra come allegare il tracepoint sched_switch definito nel precedente file sorgente myschedtp.c (il controllo degli errori non viene mostrato):

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

Lettura dalle mappe

Le mappe BPF supportano strutture o tipi di chiavi e valori complessi arbitrari. La libreria Android BPF include una classe android::BpfMap che utilizza modelli C++ per creare un'istanza BpfMap in base al tipo di chiave e valore per la mappa in questione. L'esempio di codice precedente illustra l'utilizzo di un BpfMap con chiave e valore come numeri interi. Gli interi possono anche essere strutture arbitrarie.

Pertanto la classe BpfMap basata su modello semplifica la definizione di un oggetto BpfMap personalizzato adatto alla mappa particolare. È quindi possibile accedere alla mappa utilizzando le funzioni generate su misura, che riconoscono il tipo, risultando in un codice più pulito.

Per ulteriori informazioni su BpfMap , fare riferimento alle fonti Android .

Problemi di debug

Durante la fase di avvio, vengono registrati diversi messaggi relativi al caricamento di BPF. Se per qualsiasi motivo il processo di caricamento fallisce, in logcat viene fornito un messaggio di registro dettagliato. Filtrare i log logcat tramite "bpf" stampa tutti i messaggi ed eventuali errori dettagliati durante il tempo di caricamento, come gli errori del verificatore eBPF.

Esempi di eBPF in Android

I seguenti programmi in AOSP forniscono ulteriori esempi di utilizzo di eBPF:

  • Il programma netd eBPF C viene utilizzato dal demone di rete (netd) in Android per vari scopi come il filtraggio dei socket e la raccolta di statistiche. Per vedere come viene utilizzato questo programma, controlla le fonti del monitoraggio del traffico eBPF .

  • Il programma C time_in_state eBPF calcola la quantità di tempo che un'app Android trascorre a diverse frequenze della CPU, che viene utilizzata per calcolare la potenza.

  • In Android 12, il programma gpu_mem eBPF C tiene traccia dell'utilizzo totale della memoria della GPU per ciascun processo e per l'intero sistema. Questo programma viene utilizzato per la profilazione della memoria GPU.