Erweiterung des Kernels mit eBPF

Extended Berkeley Packet Filter (eBPF) ist eine virtuelle Maschine im Kernel, die vom Benutzer bereitgestellte eBPF-Programme ausführt, um die Kernel-Funktionalität zu erweitern. Diese Programme können mit Sonden oder Ereignissen im Kernel verknüpft und zum Sammeln nützlicher Kernel-Statistiken, zur Überwachung und zum Debuggen verwendet werden. Ein Programm wird mithilfe des Systemaufrufs bpf(2) in den Kernel geladen und vom Benutzer als binärer Blob von eBPF-Maschinenanweisungen bereitgestellt. Das Android-Build-System unterstützt das Kompilieren von C-Programmen zu eBPF mithilfe der in diesem Dokument beschriebenen einfachen Build-Dateisyntax.

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

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

Android BPF-Loader

Beim Android-Boot werden alle eBPF-Programme geladen, die sich unter /system/etc/bpf/ befinden. Diese Programme sind binäre Objekte, die vom Android-Build-System aus C-Programmen erstellt werden und von Android.bp Dateien im Android-Quellbaum begleitet werden. Das Build-System speichert die generierten Objekte unter /system/etc/bpf und 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 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

Wo:

  • name_of_my_map ist der Name Ihrer Kartenvariablen. Dieser Name informiert den BPF-Loader über den Typ der zu erstellenden Karte und mit welchen Parametern. Diese Strukturdefinition wird durch den enthaltenen Header bpf_helpers.h bereitgestellt.
  • PROGTYPE/PROGNAME repräsentiert den Typ des Programms 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 strenge 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 einem Abschnitt der resultierenden Datei platziert wird.

kprobe Hängt PROGFUNC mithilfe der kprobe-Infrastruktur an eine Kernel-Anweisung an. PROGNAME muss der Name der Kernel-Funktion sein, die geprüft wird. Weitere Informationen zu kprobes finden Sie in der kprobe-Kernel-Dokumentation .
Tracepoint Hängt PROGFUNC an einen Tracepoint. PROGNAME muss das Format SUBSYSTEM/EVENT haben. Ein Tracepoint-Abschnitt zum Anhängen von Funktionen an Scheduler-Kontextwechselereignisse 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 Das Programm fungiert als Netzwerk-Socket-Filter.
Termine Das Programm fungiert als Netzwerk-Verkehrsklassifikator.
cgroupskb, cgroupsock Das Programm wird immer dann 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 über die letzte Task-PID hinzu, die auf einer bestimmten CPU ausgeführt wurde. Dieses Programm erreicht sein Ziel, indem es eine Karte erstellt und eine tp_sched_switch Funktion definiert, die an das Trace-Ereignis sched:sched_switch 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 überprüfen, ob das Programm mit der Kernel-Lizenz kompatibel ist, wenn das Programm die vom Kernel bereitgestellten BPF-Hilfsfunktionen nutzt. Geben Sie den Namen der Lizenz Ihres Programms in Zeichenfolgenform 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. Um beispielsweise ein eBPF-C-Programm mit dem Namen bpf_test.c zu erstellen, nehmen Sie den folgenden Eintrag in Android.bp Datei Ihres Projekts vor:

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

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

In sysfs verfügbare Dateien

Während des Startvorgangs lädt das Android-System automatisch alle eBPF-Objekte aus /system/etc/bpf/ , erstellt die vom Programm benötigten Karten und heftet das geladene Programm mit seinen Karten an das BPF-Dateisystem. 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 angeheftet:

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

    Beispielsweise wird für das vorherige sched_switch Tracepoint-Beispiel in myschedtp.c eine Programmdatei erstellt und an /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch angeheftet.

  • Für alle erstellten Karten wird davon ausgegangen, dass MAPNAME der Name der Karte und FILENAME der Name der eBPF-C-Datei ist. Der Android-Loader erstellt jede Karte und pinnen sie an /sys/fs/bpf/map_FILENAME_MAPNAME .

    Beispielsweise wird für das vorherige sched_switch Tracepoint-Beispiel in myschedtp.c eine Zuordnungsdatei erstellt und an /sys/fs/bpf/map_myschedtp_cpu_pid_map angeheftet.

  • bpf_obj_get() in der Android-BPF-Bibliothek gibt einen Dateideskriptor aus der angehefteten Datei /sys/fs/bpf zurück. Dieser Dateideskriptor kann für weitere Vorgänge verwendet werden, beispielsweise 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 stellt dem Benutzer die Low-Level-eBPF-Funktionalität zur Verfügung, die er zum Erstellen und Lesen von Karten, zum Erstellen von Sonden, Tracepoints und Leistungspuffern benötigt.

Anhängen von Programmen an Tracepoints

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

  1. Rufen Sie bpf_obj_get() auf, um das Programm fd vom Speicherort der angehefteten 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 ihr den 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 (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-Maps 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 demonstriert die Verwendung einer BpfMap mit Schlüssel und Wert als Ganzzahlen. Die ganzen Zahlen können auch beliebige Strukturen sein.

Somit erleichtert die mit Vorlagen versehene BpfMap Klasse die Definition eines benutzerdefinierten BpfMap Objekts, das für die jeweilige Karte geeignet ist. Auf die Karte kann dann mithilfe der benutzerdefinierten, typbewussten Funktionen zugegriffen werden, was zu einem saubereren Code führt.

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

Debugging-Probleme

Während des Startvorgangs werden mehrere Meldungen im Zusammenhang mit dem BPF-Laden protokolliert. Sollte der Ladevorgang aus irgendeinem Grund fehlschlagen, wird in logcat eine detaillierte Protokollmeldung bereitgestellt. Durch das Filtern der Logcat-Protokolle nach „bpf“ werden alle Meldungen und alle detaillierten Fehler während der Ladezeit gedruckt, z. B. eBPF-Verifizierungsfehler.

Beispiele für eBPF in Android

Die folgenden Programme in AOSP bieten zusätzliche 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 Statistikerfassung verwendet. Um zu sehen, wie dieses Programm verwendet wird, überprüfen Sie die Quellen des eBPF-Verkehrsmonitors .

  • Das eBPF-C-Programm „ time_in_state berechnet die Zeit, die eine Android-App bei verschiedenen CPU-Frequenzen verbringt, was zur Berechnung der Leistung verwendet wird.

  • In Android 12 verfolgt das gpu_mem eBPF C-Programm die gesamte GPU-Speichernutzung für jeden Prozess und für das gesamte System. Dieses Programm wird für die GPU-Speicherprofilierung verwendet.