Kernel-Telemetriedaten mit eBPF erfassen

Auf dieser Seite finden Sie eine Übersicht über das eBPF-Kernel-Telemetrie-Framework (Extended Berkeley Packet Filter) in Android. Es werden die Framework-Architektur, integrierte Messwerte und die Vorgehensweise zum Hinzufügen neuer Kernel-Messwerte beschrieben.

Android enthält ein einheitliches eBPF-Kernel-Telemetrie-Framework zum Erfassen von Kernel-Messwerten mit geringem Overhead und zum Bereitstellen dieser Messwerte für Userspace-Dienste wie statsd. Durch das Anhängen portabler eBPF-Programme an Kernel-Tracepoints und Kprobes aggregiert das Framework Kernel-Ereignisse mit hoher Frequenz direkt und exportiert kompakte Histogramme oder Zähler, ohne das generische Kernel-Image (Generic Kernel Image, GKI) zu ändern oder die Kernel-Modul-Schnittstelle (Kernel Module Interface, KMI) zu unterbrechen.

Bei herkömmlichen Ansätzen für die Kernel-Telemetrie werden benutzerdefinierte procfs- oder sysfs-Knoten, Kernel-Patches außerhalb des Quellcodebaums oder Anbieter-Hooks verwendet, die eine benutzerdefinierte Verknüpfung zwischen Kernel, SELinux, Java Native Interface (JNI) und Userspace-Diensten erfordern. Das eBPF-Kernel-Telemetrie-Framework standardisiert diese Pipeline für alle Android Common Kernels (ACKs) und bietet die folgenden Vorteile:

  • Vermeidung von Kernel-ABI- und KMI-Änderungen: eBPF-Programme werden an vorhandene Kernel-Tracepoints oder Kprobes angehängt, ohne den Kernel-Quellcode zu ändern, die binäre Anwendungsschnittstelle (Application Binary Interface, ABI) des Kernels zu ändern oder Android-Anbieter-Hooks wie TRACE_ANDROID_VH_* hinzuzufügen. Gerätehersteller und System-on-a-Chip-Partner (SoC) können die eBPF-Telemetrie anstelle von neuen Telemetrie-Anbieter-Hooks verwenden oder vorhandene Anbieter-Hooks migrieren.
  • Portabilität über Kernel-Versionen hinweg: BPF Compile Once – Run Everywhere (CO-RE) und BPF Type Format (BTF) werden verwendet, sodass sich ein einzelnes kompiliertes eBPF-Programm automatisch an interne Änderungen des Kernel-Struktur-Layouts in verschiedenen GKI-Versionen anpasst, z. B. android14-6.1 und höher.
  • Minimierung des System-Health-Overheads: Kernel-Ereignisse mit hoher Frequenz werden direkt an der Quelle gefiltert und in kompakte logarithmische Histogramme mit 32 Buckets zur Basis 2 (log2) aggregiert, in der Regel unter 1 KB pro BPF-Map. Der Userspace ruft aggregierte Map-Einträge einmal täglich mit einem einzelnen BPF_MAP_LOOKUP_BATCH-Batch-Systemaufruf ab.
  • Vereinfachung der SELinux- und JNI-Integration: Telemetrie-Maps werden unter /sys/fs/bpf/kernelstats/ angepinnt, um die SELinux-Typvererbung fs_bpf_kernelstats auf Verzeichnisebene anzuwenden. Struktur-Member-Offsets werden zur Laufzeit in JNI mit BTF-Metadaten aufgelöst, um benutzerdefinierten C++-Parsing-Code beim Hinzufügen neuer Messwerte zu vermeiden.

Weitere Informationen zum Laden und Anpinnen von eBPF-Programmen und -Maps in Android finden Sie unter Kernel mit eBPF erweitern.

Architektur

Die eBPF-Kernel-Telemetrie-Pipeline trennt generische Infrastrukturkomponenten von messwertspezifischen Definitionen, sodass zum Hinzufügen eines neuen Kernel-Messwerts nur minimale Codeänderungen erforderlich sind. In der folgenden Tabelle werden die Komponenten der Pipeline beschrieben:

KomponenteRolle in der PipelineUmfang
BPF-Loader, bpfloaderLädt kompilierte eBPF-Programme beim Booten aus /system/etc/bpf/kernelstats/, wenn sie mit dem ausgeführten Kernel kompatibel sind, und pinnt Maps an /sys/fs/bpf/kernelstats/. Das Laden wird sauber übersprungen, wenn es deaktiviert oder nicht unterstützt wird.Allgemein
SELinux-Richtlinie, fs_bpf_kernelstatsWendet SELinux-Labels auf Verzeichnisebene auf /sys/fs/bpf/kernelstats/ an, sodass system_server Telemetrie-Maps ohne SELinux-Richtlinienupdates pro Messwert lesen kann.Allgemein
eBPF-Programm in system/bpfprogs/telemetry/Wird an Kernel-Tracepoints oder Kprobes angehängt, filtert Ereignisse und aggregiert Messwerte in BPF-Maps.Benutzerdefiniert pro Messwert
JNI-BTF-ReaderParst BTF-Metadaten zur Laufzeit aus der kompilierten binären BPF-Datei im ELF-Format (Executable and Linkable Format), speichert Offsets von Struktur-Membern im Cache und führt Batch-Map-Lesevorgänge in primitive Java-Arrays aus.Allgemein
Userspace-Puller in statsdRuft kumulative Histogramm-Maps nach einem täglichen Zeitplan ab, berechnet Deltas im Vergleich zum im Cache gespeicherten Verlauf und erstellt stark typisierte StatsEvent-Pull-Atome.Allgemeiner Helfer mit einem einzelnen case-Eintrag pro Atom
TelemetrieschemaDefiniert das stark typisierte statsd-Pull-Atom-Protobuf und serverseitige Histogramm- oder Wertmesswertaggregationen.Benutzerdefiniert pro Messwert

Userspace-Nutzungsmodelle

Beim Entwerfen eines Kernel-Telemetrie-Messwerts haben Sie die Wahl zwischen zwei Userspace-Nutzungsmodellen, je nachdem, ob der Messwert einen kumulativen Zustand oder eine geordnete Ereignissequenz darstellt. In der folgenden Tabelle werden die beiden Modelle verglichen:

AttributAkkumulatorenStreams
DefinitionZustandsorientierte, monoton steigende kumulative Zähler oder HistogrammeGeordnete Sequenz von Kernel-Ereignissen mit Zeitstempel
ZugriffsmusterUserspace-Abfragen nach Bedarf, z. B. einmal alle 24 Stunden über statsdUserspace-Nutzung kontinuierlich aus einem Ringpuffer, um verworfene Ereignisse zu vermeiden
Typische BPF-Map-TypenBPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_RINGBUF
Concurrency-ModellKernel-Map-Lookups sind atomar. Mehrere Reader können ohne Userspace-Sperrung abfragen.Destruktive Lesesemantik erfordert einen einzelnen Reader-Thread, der an die Nutzer weiterleitet.
BeispielanwendungsfälleLatenzhistogramme für die Speicherverwaltung, Zuordnungsgrößen für Direct Memory Access Buffer (DMA-BUF), Iteratoren für die WeckquelleGefilterte Scheduler-Trace-Ereignisse, hochfrequente Subsystem-Power-Traces

Integrierte Kernel-Telemetrie-Messwerte

Android enthält integrierte eBPF-Kernel-Telemetrieprogramme, die für GKI 6.1 und höhere Kernel funktionieren, einschließlich android14-6.1, android15-6.6, android16-6.12 und android17-6.18. Für jeden Latenz- und Größenmesswert wird ein exponentielles Histogramm mit 32 Buckets (log2) verwendet, das über einen Zeitraum von 24 Stunden aggregiert wird. In der folgenden Tabelle sind die integrierten Kernel-Telemetrie-Messwerte aufgeführt:

Messwert-AtomSubsystemBeschreibungDimensionsschlüssel
KswapdLatencySpeicherverwaltungAusführungslatenz des Kernel-Swap-Daemons kswapd vom Aufwachen bis zum Ruhezustand in MikrosekundenNon-Uniform Memory Access (NUMA) node_id
DirectReclaimLatencySpeicherverwaltungLatenz der synchronen direkten Speicherfreigabe in MikrosekundenStatus des Prozesses bei Arbeitsspeichermangel (Out of Memory, OOM) oom_score_adj
CompactionDurationSpeicherverwaltungAusführungsdauer der systemweiten Speicherkomprimierung in Mikrosekunden, ersetzt Legacy-Anbieter-Hooks für die KomprimierungSystemweit
CmaAllocationLatencySpeicherverwaltungLatenz der Seitenzuordnung des Contiguous Memory Allocator (CMA) in MikrosekundenCMA-Heap-Name
DmabufRequestSizesSpeicherverwaltungGrößen der DMA-BUF-Zuordnungsanfragen in ByteName des Export-Heaps
SuspendResumeLatencyEnergiespareinstellungenLatenz über die Phasen des Kernel-Ruhezustands und der Wiederaufnahme hinweg in MikrosekundenPhase der Ruhezustandsaktion

Kernel-Telemetrie-Messwert hinzufügen

Wenn Sie der eBPF-Kernel-Telemetrie-Pipeline einen neuen Akkumulator-Messwert hinzufügen möchten, führen Sie die folgenden Schritte in der angegebenen Reihenfolge aus:

  1. Schreiben Sie das eBPF-Programm unter system/bpfprogs/telemetry/ und erstellen Sie die Binärdatei in /system/etc/bpf/kernelstats/.
  2. Prüfen Sie die Kompatibilität des BTF-Struktur-Layouts für den generischen JNI-BTF-Reader.
  3. Registrieren Sie das Pull-Atom in statsd.
  4. Definieren Sie das Protobuf-Schema für das Pull-Atom.

In den folgenden Abschnitten wird DirectReclaimLatency (directReclaim.c) als Beispiel verwendet.

eBPF-Programm schreiben

Fügen Sie in system/bpfprogs/telemetry/ <bpf_telemetry.h> ein und definieren Sie den Map Schlüssel und die Ausgabemap für den Akkumulator. Verwenden Sie in BPF-Map-Schlüsseln Ganzzahl-IDs oder Enums anstelle von Rohstrings, um den Speicherbedarf der Map zu minimieren und die Privatsphäre zu schützen:

#include <android_bpf_defs.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf_telemetry.h>
#include <stdint.h>

struct direct_reclaim_key {
    uint32_t process_state;
};

#define DIRECT_RECLAIM_START_MAP_ENTRIES 128
#define DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES 8

// Temporary map to store start time. Key: pid (thread_id), Value: start_time_ns
DEFINE_BPF_MAP_GRO(direct_reclaim_start_map, HASH, uint32_t, uint64_t,
                   DIRECT_RECLAIM_START_MAP_ENTRIES, AID_SYSTEM);

// Output map. Key: direct_reclaim_key, Value: accumulator_value (32 log2 buckets)
DEFINE_BPF_MAP_GRO(direct_reclaim_latency_map, HASH, struct direct_reclaim_key,
                   struct accumulator_value, DIRECT_RECLAIM_OUTPUT_MAP_ENTRIES,
                   AID_SYSTEM);

Erfassen Sie in den Tracepoint-Handlern beim Eintritt den Startzeitstempel, berechnen Sie beim Verlassen den log2-Latenz-Bucket mit GET_LATENCY_BUCKET und erhöhen Sie den Eintrag in der Akkumulator-Map mit INCREMENT_ACCUMULATOR:

DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_begin", AID_ROOT, AID_SYSTEM,
                tracepoint_vmscan_mm_vmscan_direct_reclaim_begin)(
        uint64_t* args) {
    (void)args;
    uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
    SAVE_FIRST_START_TIME(direct_reclaim_start_map, &pid);
    return 0;
}

DEFINE_BPF_PROG("tp_btf/mm_vmscan_direct_reclaim_end", AID_ROOT, AID_SYSTEM,
                tracepoint_vmscan_mm_vmscan_direct_reclaim_end)(
        uint64_t* args) {
    (void)args;
    uint32_t pid = (uint32_t)bpf_get_current_pid_tgid();
    uint32_t bucket = GET_LATENCY_BUCKET(direct_reclaim_start_map, &pid);
    if (bucket == BPF_TELEMETRY_INVALID_BUCKET) return 0;

    struct task_struct* task = (struct task_struct*)bpf_get_current_task();
    int oom_score_adj = BPF_CORE_READ(task, signal, oom_score_adj);
    enum process_state state = get_process_state(oom_score_adj);

    struct direct_reclaim_key key = {.process_state = (uint32_t)state};
    INCREMENT_ACCUMULATOR(direct_reclaim_latency_map, &key, bucket);
    return 0;
}

Kompatibilität des BTF-Struktur-Layouts prüfen

Sie müssen keinen benutzerdefinierten JNI-C++-Code schreiben, wenn Sie eine neue Histogramm-Map hinzufügen, solange Ihre Map-Schlüssel- und Wertstrukturen Standardtypen für Ganzzahlen, Enums und feste Arrays verwenden. Anstatt BPF-Map-Einträge zur Kompilierzeit in hartcodierte C-Strukturen umzuwandeln, löst der generische JNI-Reader Struktur-Layouts zur Laufzeit in drei Phasen auf:

  • Zuordnung von ELF zu Map: Die JNI-Ebene parst BTF-Metadaten direkt aus der kompilierten BPF-ELF-Datei auf der Festplatte, z. B. /system/etc/bpf/kernelstats/directReclaim.bpf, mit der libbpf-Funktion btf__parse.
  • Auflösung des BTF-Typs: Beim ersten Zugriff erstellt und speichert der Reader eine strukturelle Darstellung der C-Strukturen für Schlüssel und Wert der Map im Cache. Dabei werden kumulative Byte-Offsets für Struktur-Member, Byte-Breiten für Ganzzahlen und Enums sowie Array-Elementgrößen vom Basiszeiger berechnet.
  • Batch-Lookup und -Serialisierung: Wenn statsd die Map abruft, gibt die JNI-Ebene einen einzelnen BPF_MAP_LOOKUP_BATCH-Befehl an den sys_bpf-Systemaufruf aus, um alle Hash-Einträge im Batch-Verfahren zu lesen. Die aufgelösten Schlüssel-IDs und Bucket-Arrays werden mit der JNI-Funktion SetLongArrayRegion direkt in primitive Java-long[]-Arrays kopiert.

Wenn sich eine BPF-C-Struktur in einer zukünftigen Version ändert, wird der neu kompilierte BTF-Abschnitt automatisch aktualisiert und der JNI-Reader passt sich zur Laufzeit an, ohne dass eine Neukompilierung von JNI erforderlich ist.

Pull-Atom in statsd registrieren

In StatsPullAtomService.java liest der generische Helfer pullKernelTelemetry die angepinnte BPF-Map, berechnet Bucket-Deltas im Vergleich zu den im Cache gespeicherten Zählern des vorherigen Pulls und gibt StatsEvent-Einträge aus. Zum Hinzufügen eines neuen Kernel-Telemetrie-Messwerts sind nur drei kleine Ergänzungen erforderlich:

// 1. Dispatch the atom tag to pullKernelTelemetry() in onPullAtomLocked():
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY:
    return pullKernelTelemetry(atomTag, data);

// 2. Add the atom tag to the registered atoms list in registerKernelTelemetry():
int[] atoms = {
    FrameworkStatsLog.DIRECT_RECLAIM_LATENCY,
    // ...
};

// 3. Map the atom tag to its pinned BPF map path:
case FrameworkStatsLog.DIRECT_RECLAIM_LATENCY ->
    "/sys/fs/bpf/kernelstats/map_directReclaim_direct_reclaim_latency_map";

Protobuf für Pull-Atom definieren

Definieren Sie die stark typisierte Pull-Atom-Nachricht in frameworks/proto_logging/, um sie an den Dimensionsschlüssel und das log2-Histogramm-Array mit 32 Buckets anzupassen, das von der BPF-Map erzeugt wird:

message DirectReclaimLatency {
    enum ProcessState {
        PROCESS_STATE_UNSPECIFIED = 0;
        STATE_NATIVE = 1;
        STATE_SYSTEM = 2;
        STATE_PERSISTENT = 3;
        STATE_FOREGROUND = 4;
        STATE_VISIBLE = 5;
        STATE_PERCEPTIBLE = 6;
        STATE_SERVICE_BACKGROUND = 7;
        STATE_CACHED = 8;
    }
    optional ProcessState process_state = 1;
    repeated int32 duration_log2_buckets_micros = 2;
}