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.1und 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 einzelnenBPF_MAP_LOOKUP_BATCH-Batch-Systemaufruf ab. - Vereinfachung der SELinux- und JNI-Integration: Telemetrie-Maps werden unter
/sys/fs/bpf/kernelstats/angepinnt, um die SELinux-Typvererbungfs_bpf_kernelstatsauf 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:
| Komponente | Rolle in der Pipeline | Umfang |
|---|---|---|
BPF-Loader, bpfloader | Lä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_kernelstats | Wendet 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-Reader | Parst 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 statsd | Ruft 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 |
| Telemetrieschema | Definiert 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:
| Attribut | Akkumulatoren | Streams |
|---|---|---|
| Definition | Zustandsorientierte, monoton steigende kumulative Zähler oder Histogramme | Geordnete Sequenz von Kernel-Ereignissen mit Zeitstempel |
| Zugriffsmuster | Userspace-Abfragen nach Bedarf, z. B. einmal alle 24 Stunden über statsd | Userspace-Nutzung kontinuierlich aus einem Ringpuffer, um verworfene Ereignisse zu vermeiden |
| Typische BPF-Map-Typen | BPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASH | BPF_MAP_TYPE_RINGBUF |
| Concurrency-Modell | Kernel-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älle | Latenzhistogramme für die Speicherverwaltung, Zuordnungsgrößen für Direct Memory Access Buffer (DMA-BUF), Iteratoren für die Weckquelle | Gefilterte 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-Atom | Subsystem | Beschreibung | Dimensionsschlüssel |
|---|---|---|---|
KswapdLatency | Speicherverwaltung | Ausführungslatenz des Kernel-Swap-Daemons kswapd vom Aufwachen bis zum Ruhezustand in Mikrosekunden | Non-Uniform Memory Access (NUMA) node_id |
DirectReclaimLatency | Speicherverwaltung | Latenz der synchronen direkten Speicherfreigabe in Mikrosekunden | Status des Prozesses bei Arbeitsspeichermangel (Out of Memory, OOM) oom_score_adj |
CompactionDuration | Speicherverwaltung | Ausführungsdauer der systemweiten Speicherkomprimierung in Mikrosekunden, ersetzt Legacy-Anbieter-Hooks für die Komprimierung | Systemweit |
CmaAllocationLatency | Speicherverwaltung | Latenz der Seitenzuordnung des Contiguous Memory Allocator (CMA) in Mikrosekunden | CMA-Heap-Name |
DmabufRequestSizes | Speicherverwaltung | Größen der DMA-BUF-Zuordnungsanfragen in Byte | Name des Export-Heaps |
SuspendResumeLatency | Energiespareinstellungen | Latenz über die Phasen des Kernel-Ruhezustands und der Wiederaufnahme hinweg in Mikrosekunden | Phase 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:
- Schreiben Sie das eBPF-Programm unter
system/bpfprogs/telemetry/und erstellen Sie die Binärdatei in/system/etc/bpf/kernelstats/. - Prüfen Sie die Kompatibilität des BTF-Struktur-Layouts für den generischen JNI-BTF-Reader.
- Registrieren Sie das Pull-Atom in
statsd. - 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 derlibbpf-Funktionbtf__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
statsddie Map abruft, gibt die JNI-Ebene einen einzelnenBPF_MAP_LOOKUP_BATCH-Befehl an densys_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-FunktionSetLongArrayRegiondirekt 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;
}