Kernel mit eBPF erweitern

Der erweiterte Berkeley-Paketfilter (eBPF) ist eine virtuelle Maschine im Kernel, die von Nutzern bereitgestellte eBPF-Programme ausführt, um die Kernelfunktionen zu erweitern. Diese Programme können an Probes oder Ereignisse im Kernel angehängt und verwendet werden, um nützliche Kernelstatistiken zu erfassen, zu überwachen und zu debuggen. Ein Programm wird mit dem Systemaufruf bpf(2) in den Kernel geladen und vom Nutzer als binäres Blob mit eBPF-Maschinenanweisungen bereitgestellt. Das Android-Build-System unterstützt die Kompilierung von C-Programmen in eBPF mithilfe der einfachen Build-Dateisyntax, die in diesem Dokument beschrieben wird.

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

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

Android BPF-Ladeprogramm

Beim Starten von Android werden alle eBPF-Programme unter /system/etc/bpf/ geladen. Diese Programme sind Binärobjekte, die vom Android-Build-System aus C-Programmen erstellt wurden. Sie werden im Android-Quellbaum von Android.bp-Dateien begleitet. 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

Dabei gilt:

  • name_of_my_map ist der Name der Zuordnungsvariablen. Anhand dieses Namens wird der BPF-Ladeprogramm mitgeteilt, welche Art von Karte mit welchen Parametern erstellt werden soll. Diese Strukturdefinition wird durch den enthaltenen bpf_helpers.h-Header bereitgestellt.
  • PROGTYPE/PROGNAME steht für den Programmtyp und den Programmnamen. Der Typ des Programms kann einer der in der folgenden Tabelle aufgeführten sein. Wenn ein Programmtyp nicht aufgeführt ist, gibt es keine strikte Namenskonvention für das Programm. Der Name muss nur dem Prozess bekannt sein, der das Programm anhängt.

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

kprobe Hooks PROGFUNC an einer Kernelanweisung mithilfe der kprobe-Infrastruktur an. PROGNAME muss der Name der Kernel-Funktion sein, die geprüft 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 des Kontextwechsels 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 Kerneldokumentation zu Trace-Ereignissen.
skfilter Das Programm dient als Netzwerk-Socket-Filter.
schedcls Das Programm dient als Netzwerk-Traffic-Klassifikator.
cgroupskb, cgroupsock Das Programm wird ausgeführt, wenn Prozesse in einer CGroup einen AF_INET- oder AF_INET6-Socket erstellen.

Weitere Typen finden Sie im Loader-Quellcode.

Das folgende myschedtp.c-Programm fügt beispielsweise Informationen zur letzten Aufgabe-PID hinzu, die auf einer bestimmten CPU ausgeführt wurde. Dazu wird eine Map erstellt und eine tp_sched_switch-Funktion definiert, die an das sched:sched_switch-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");

Mit dem LICENSE-Makro wird geprüft, ob das Programm mit der Lizenz des Kernels kompatibel ist, wenn es BPF-Hilfsfunktionen des Kernels verwendet. Geben Sie den Namen der Lizenz Ihres Programms in Stringform 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 Datei Android.bp 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 Datei Android.bp Ihres Projekts ein:

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

Dieser Eintrag kompiliert das C-Programm, das zum Objekt /system/etc/bpf/bpf_test.o führt. Beim Starten lädt das Android-System das bpf_test.o-Programm automatisch in den Kernel.

In Sysfs verfügbare Dateien

Beim Starten lädt das Android-System automatisch alle eBPF-Objekte aus /system/etc/bpf/, erstellt die vom Programm benötigten Maps und ordnet das geladene Programm mit seinen Maps dem BPF-Dateisystem zu. 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 für die Benennung dieser Dateien und ihre Speicherorte in sysfs beschrieben.

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 das Android-Ladeprogramm jedes Programm und legt es unter /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME fest.

    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.

  • Wenn MAPNAME der Name der Karte und FILENAME der Name der eBPF-C-Datei ist, wird für alle erstellten Karten vom Android-Ladeprogramm jede Karte erstellt und an /sys/fs/bpf/map_FILENAME_MAPNAME angepinnt.

    Für das vorherige Tracepoint-Beispiel sched_switch in myschedtp.c wird beispielsweise eine Zuordnungsdatei 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 Datei /sys/fs/bpf 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 Low-Level-eBPF-Funktionen, die zum Erstellen und Lesen von Maps, zum Erstellen von Probes, Tracepoints und Perf-Buffers erforderlich sind.

Programme an Tracepoints anhängen

Tracepoint-Programme werden beim Starten automatisch geladen. Nach dem Laden muss das Tracepoint-Programm so aktiviert werden:

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

Das folgende Codebeispiel zeigt, wie der in der vorherigen myschedtp.c-Quelldatei definierte sched_switch-Tracepoint angehängt wird. Die Fehlerprüfung wird nicht angezeigt:

  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 Map zu instanziieren. Im vorherigen Codebeispiel wird die Verwendung eines BpfMap mit Schlüssel und Wert als Ganzzahlen veranschaulicht. Die Ganzzahlen können auch beliebige Strukturen sein.

So können Sie mit der BpfMap-Klasse mit Vorlage ein benutzerdefiniertes BpfMap-Objekt für die jeweilige Karte definieren. Auf die Karte kann dann über die benutzerdefinierten generierten Funktionen zugegriffen werden, die typspezifisch sind und zu einem saubereren Code führen.

Weitere Informationen zu BpfMap findest du in den Android-Quellen.

Probleme beheben

Während des Bootens werden mehrere Meldungen zur BPF-Ladezeit protokolliert. Wenn der Ladevorgang aus irgendeinem Grund fehlschlägt, wird in logcat eine detaillierte Protokollmeldung ausgegeben. Wenn Sie die Logcat-Logs nach bpf filtern, werden alle Meldungen und alle detaillierten Fehler während der Ladezeit ausgegeben, z. B. eBPF-Prüffehler.

Beispiele für eBPF in Android

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

  • Das netd-eBPF-C-Programm wird vom Netzwerk-Daemon (netd) in Android für verschiedene Zwecke wie Socket-Filterung und Statistikerhebung verwendet. Informationen zur Verwendung dieses Programms finden Sie in den eBPF-Traffic-Monitor-Quellen.

  • Das time_in_state eBPF-C-Programm berechnet die Zeit, die eine Android-App mit verschiedenen CPU-Frequenzen verbringt. Dieser Wert wird zur Berechnung des Energieverbrauchs verwendet.

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