Kernel mit eBPF erweitern

Der erweiterte Berkeley Packet Filter (Extended Berkeley Packet Filter, eBPF) ist eine virtuelle Maschine im Kernel, die vom Nutzer bereitgestellte eBPF-Programme ausführt, um die Kernel-Funktionalität zu erweitern. Diese Programme können an Probes oder Ereignisse im Kernel angehängt werden und zum Erfassen nützlicher Kernel-Statistiken, zur Überwachung und zum Debugging verwendet werden. Ein Programm wird mit dem bpf(2)-Systemaufruf in den Kernel geladen und vom Nutzer als binärer Blob mit eBPF-Maschinenanweisungen bereitgestellt. Das Android-Build-System unterstützt das Kompilieren von C-Programmen in eBPF mit einer einfachen Build-Dateisyntax, die in diesem Dokument beschrieben wird.

Weitere Informationen zu eBPF-Interna und -Architektur finden Sie auf der eBPF-Seite von Brendan Gregg.

Android enthält einen eBPF-Loader und eine eBPF-Bibliothek, die eBPF-Programme beim Booten lädt.

Android BPF-Loader

Beim Android-Booten werden alle eBPF-Programme unter /system/etc/bpf/ geladen. Diese Programme sind binäre Objekte, die vom Android-Build-System aus C-Programmen erstellt wurden und im Android-Quellcodebaum von Android.bp-Dateien begleitet werden. Das Build-System speichert die generierten Objekte unter /system/etc/bpf. Diese Objekte werden Teil des System-Images.

Format eines Android-eBPF-C-Programms

Ein eBPF-C-Programm muss das folgende Format haben:

#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

Wobei:

  • name_of_my_map ist der Name Ihrer Kartenvariablen. Dieser Name informiert den BPF-Loader über den Typ der zu erstellenden Karte und die zu verwendenden Parameter. Diese Strukturdefinition wird durch den enthaltenen bpf_helpers.h-Header bereitgestellt.
  • PROGTYPE/PROGNAME steht für den Typ des Programms und Programmnamen. Der Typ des Programms kann einer der in der folgenden Tabelle aufgeführten Typen sein. Wenn ein Programmtyp nicht aufgeführt ist, gibt es keine strenge Namenskonvention für das Programm. Der Name muss nur dem Prozess bekannt sein, der das Programm anhängt.

  • PROGFUNC ist eine Funktion, die nach dem Kompilieren in einen Abschnitt der resultierenden Datei eingefügt wird.

kprobe Hängt PROGFUNC mit der kprobe-Infrastruktur an eine Kernel-Anweisung an. PROGNAME muss der Name der Kernel-Funktion sein, die mit kprobe untersucht wird. Weitere Informationen zu kprobes finden Sie in der Kernel-Dokumentation zu kprobe.
tracepoint Hängt PROGFUNC an einen Tracepoint an. PROGNAME muss das Format SUBSYSTEM/EVENT haben. Ein Tracepoint-Abschnitt zum Anhängen von Funktionen an Ereignisse zum Kontextwechsel des Schedulers wäre beispielsweise SEC("tracepoint/sched/sched_switch"), wobei sched der Name des Trace-Subsystems und sched_switch der Name des Trace-Ereignisses ist. Weitere Informationen zu Tracepoints finden Sie in der Kernel-Dokumentation zu Trace-Ereignissen.
skfilter Programmiert Funktionen als Netzwerk-Socket-Filter.
schedcls Programm fungiert als Netzwerkverkehr-Klassifikator.
cgroupskb, cgroupsock Das Programm wird ausgeführt, wenn Prozesse in einer CGroup ein AF_INET- oder AF_INET6-Socket erstellen.

Weitere Typen finden Sie im Quellcode des Loaders.

Das folgende myschedtp.c-Programm fügt beispielsweise Informationen zur letzten Aufgaben-PID hinzu, die auf einer bestimmten CPU ausgeführt wurde. Dazu wird eine Karte erstellt und eine tp_sched_switch-Funktion definiert, die an das sched:sched_switch-Trace-Ereignis angehängt werden kann. Weitere Informationen finden Sie unter Programme an Tracepoints anhängen.

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

Das LICENSE-Makro wird verwendet, um zu prüfen, ob das Programm mit der Lizenz des Kernels kompatibel ist, wenn das Programm BPF-Hilfsfunktionen verwendet, die vom Kernel bereitgestellt werden. Geben Sie den Namen der Lizenz Ihres Programms als String an, z. B. LICENSE("GPL") oder LICENSE("Apache 2.0").

Format der Android.bp-Datei

Damit das Android-Build-System ein eBPF-.c-Programm erstellen kann, müssen Sie einen Eintrag in der Android.bp-Datei des Projekts erstellen. Wenn Sie beispielsweise ein eBPF-C-Programm mit dem Namen bpf_test.c erstellen möchten, fügen Sie den folgenden Eintrag in die Android.bp-Datei Ihres Projekts ein:

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

Dieser Eintrag kompiliert das C-Programm und erzeugt das Objekt /system/etc/bpf/bpf_test.o. Beim Booten lädt das Android-System das Programm bpf_test.o automatisch in den Kernel.

In sysfs verfügbare Dateien

Beim Booten lädt das Android-System automatisch alle eBPF-Objekte aus /system/etc/bpf/, erstellt die für das Programm erforderlichen Karten und pinnt das geladene Programm mit seinen Karten an das BPF-Dateisystem an. Diese Dateien können dann für die weitere Interaktion mit dem eBPF-Programm oder zum Lesen von Karten verwendet werden. In diesem Abschnitt werden die Konventionen beschrieben, die für die Benennung dieser Dateien und ihre Speicherorte in sysfs verwendet werden.

Die folgenden Dateien werden erstellt und angepinnt:

  • Für alle geladenen Programme, wobei PROGNAME der Name des Programms und FILENAME der Name der eBPF-C-Datei ist, erstellt und pinnt der Android-Loader jedes Programm unter /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME.

    Für das vorherige sched_switch-Tracepoint-Beispiel in myschedtp.c wird beispielsweise eine Programmdatei erstellt und an /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch angepinnt.

  • Für alle erstellten Karten, wobei MAPNAME der Name der Karte und FILENAME der Name der eBPF-C-Datei ist, erstellt und pinnt der Android-Loader jede Karte unter /sys/fs/bpf/map_FILENAME_MAPNAME.

    Für das vorherige sched_switch-Tracepoint-Beispiel in myschedtp.c wird beispielsweise eine Kartendatei erstellt und an /sys/fs/bpf/map_myschedtp_cpu_pid_map angepinnt.

  • bpf_obj_get() in der Android BPF-Bibliothek gibt einen Dateideskriptor aus der angepinnten /sys/fs/bpf-Datei zurück. Dieser Dateideskriptor kann für weitere Vorgänge verwendet werden, z. B. zum Lesen von Karten oder zum Anhängen eines Programms an einen Tracepoint.

Android BPF-Bibliothek

Die Android BPF-Bibliothek heißt libbpf_android.so und ist Teil des System-Images. Diese Bibliothek bietet dem Nutzer die eBPF-Funktionen auf niedriger Ebene, die zum Erstellen und Lesen von Karten, zum Erstellen von Probes, Tracepoints und Perf-Puffern erforderlich sind.

Programme an Tracepoints anhängen

Tracepoint-Programme werden beim Booten automatisch geladen. Nach dem Laden muss das Tracepoint-Programm mit den folgenden Schritten aktiviert werden:

  1. Rufen Sie bpf_obj_get() auf, um den Programm-fd aus dem Speicherort der angepinnten Datei abzurufen. Weitere Informationen finden Sie unter den In sysfs verfügbaren Dateien.
  2. Rufen Sie bpf_attach_tracepoint() in der BPF-Bibliothek auf und übergeben Sie den Programm-fd und den Tracepoint-Namen.

Das folgende Codebeispiel zeigt, wie der sched_switch-Tracepoint angehängt wird, der in der vorherigen myschedtp.c-Quelldatei definiert wurde (die Fehlerprüfung wird nicht gezeigt):

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

Aus den Karten lesen

BPF-Karten unterstützen beliebige komplexe Schlüssel- und Wertstrukturen oder -typen. Die Android BPF-Bibliothek enthält eine android::BpfMap-Klasse, die C++ -Vorlagen verwendet, um BpfMap basierend auf dem Schlüssel- und Werttyp für die betreffende Karte zu instanziieren. Das vorherige Codebeispiel zeigt die Verwendung von BpfMap mit Schlüssel und Wert als Ganzzahlen. Die Ganzzahlen können auch beliebige Strukturen sein.

Mit der Vorlagenklasse BpfMap können Sie also ein benutzerdefiniertes BpfMap-Objekt definieren, das für die jeweilige Karte geeignet ist. Auf die Karte kann dann mit den benutzerdefinierten Funktionen zugegriffen werden, die typbezogen sind, was zu saubererem Code führt.

Weitere Informationen zu BpfMap finden Sie in den Android-Quellen.

Probleme beheben

Während des Bootens werden mehrere Meldungen im Zusammenhang mit dem Laden von BPF protokolliert. Wenn der Ladevorgang aus irgendeinem Grund fehlschlägt, wird in logcat ein detaillierter Logeintrag bereitgestellt. Wenn Sie die logcat-Logs nach bpf filtern, werden alle Meldungen und detaillierten Fehler während der Ladezeit ausgegeben, z. B. eBPF-Verifier-Fehler.

Beispiele für eBPF in Android

Die folgenden Programme in AOSP enthalten weitere Beispiele für die Verwendung von eBPF:

  • Das netd eBPF-C Programm wird vom Netzwerk-Daemon (netd) in Android für verschiedene Zwecke verwendet, z. B. für die Socket-Filterung und die Statistikerfassung. Wie dieses Programm verwendet wird, erfahren Sie in den Quellen des eBPF-Traffic Monitors.

  • Das time_in_state eBPF-C Programm berechnet die Zeit, die eine Android-App bei verschiedenen CPU-Frequenzen verbringt. Diese wird zur Berechnung des Stromverbrauchs verwendet.

  • In Android 12 erfasst das gpu_mem eBPF-C -Programm die gesamte GPU-Arbeitsspeichernutzung für jeden Prozess und für das gesamte System. Dieses Programm wird für das GPU-Arbeitsspeicher-Profiling verwendet.

Weitere Informationen zur Verwendung von eBPF zum Erfassen von Histogrammen zur Kernel-Arbeitsspeicherverwaltung und zur Latenz des Stromverbrauchs mit geringem Overhead für statsd finden Sie unter Kernel-Telemetrie mit eBPF erfassen.