Zbieranie danych telemetrycznych jądra za pomocą eBPF

Ta strona zawiera omówienie platformy telemetrii jądra Extended Berkeley Packet Filter (eBPF) w Androidzie. Opisuje architekturę platformy, wbudowane dane i sposób dodawania nowych danych jądra.

Android zawiera ujednoliconą platformę telemetrii jądra eBPF do zbierania danych jądra z niewielkim narzutem i udostępniania ich usługom w przestrzeni użytkownika, takim jak statsd. Dzięki dołączaniu przenośnych programów eBPF do punktów śledzenia jądra systemu i kprobes platforma agreguje zdarzenia jądra systemu o wysokiej częstotliwości w miejscu i eksportuje kompaktowe histogramy lub liczniki bez modyfikowania ogólnego obrazu jądra (GKI) ani naruszania interfejsu modułu jądra systemu (KMI).

Tradycyjne podejścia do telemetrii jądra opierają się na niestandardowych węzłach procfs lub sysfs, łatkach jądra poza drzewem lub hakach dostawcy, które wymagają niestandardowego połączenia w całym jądrze, SELinux, Java Native Interface (JNI) i usługach w przestrzeni użytkownika. Platforma telemetrii jądra eBPF standaryzuje ten potok w przypadku wspólnych jąder Androida (ACK) i zapewnia te korzyści:

  • Unikanie zmian w ABI jądra i KMI: dołącza programy eBPF do istniejących punktów śledzenia jądra lub kprobes bez modyfikowania kodu źródłowego jądra, zmiany interfejsu binarnego aplikacji jądra (ABI) ani dodawania haków dostawcy Androida, takich jak TRACE_ANDROID_VH_*. Producenci urządzeń i partnerzy systemów na chipie (SoC) mogą używać telemetrii eBPF zamiast dodawać nowe haki dostawcy telemetrii lub migrować z istniejących haków dostawcy.
  • Zachowanie przenośności między wersjami jądra: używa BPF Compile Once – Run Everywhere (CO-RE) i BPF Type Format (BTF), dzięki czemu pojedynczy skompilowany program eBPF automatycznie dostosowuje się do wewnętrznych zmian układu struktury jądra w wersjach GKI, takich jak android14-6.1 i nowsze.
  • Minimalizowanie obciążenia systemu: filtruje i agreguje zdarzenia jądra o wysokiej częstotliwości bezpośrednio u źródła w kompaktowych histogramach logarytmicznych o podstawie 2 (log2) z 32 przedziałami, zwykle poniżej 1 KB na mapę BPF. Przestrzeń użytkownika pobiera zagregowane wpisy mapy raz dziennie za pomocą pojedynczego wywołania systemowego BPF_MAP_LOOKUP_BATCH.
  • Upraszczanie integracji SELinux i JNI: przypina mapy telemetrii w katalogu /sys/fs/bpf/kernelstats/, aby zastosować dziedziczenie typu SELinux fs_bpf_kernelstats na poziomie katalogu, i rozwiązuje przesunięcia elementów struktury w czasie działania w JNI za pomocą metadanych BTF, aby wyeliminować niestandardowy kod analizowania C++ podczas dodawania nowych danych.

Więcej informacji o tym, jak Android wczytuje i przypina programy i mapy eBPF, znajdziesz w artykule Rozszerzanie jądra za pomocą eBPF.

Omówienie architektury

Potok telemetrii jądra eBPF oddziela ogólne komponenty infrastruktury od definicji specyficznych dla danych, dzięki czemu dodanie nowych danych jądra wymaga minimalnych zmian w kodzie. W tabeli poniżej opisujemy komponenty potoku:

KomponentRola w potokuZakres
Wczytywanie BPF, bpfloaderWczytuje skompilowane programy eBPF z katalogu /system/etc/bpf/kernelstats/ podczas uruchamiania, jeśli są zgodne z działającym jądrem, i przypina mapy do katalogu /sys/fs/bpf/kernelstats/. Jeśli jest wyłączony lub nieobsługiwany, pomija wczytywanie.Ogólne
Zasady SELinux, fs_bpf_kernelstatsStosuje etykiety SELinux na poziomie katalogu do katalogu /sys/fs/bpf/kernelstats/, dzięki czemu system_server może odczytywać mapy telemetrii bez aktualizacji zasad SELinux dla poszczególnych danych.Ogólne
Program eBPF w katalogu system/bpfprogs/telemetry/Dołącza się do punktów śledzenia jądra lub kprobes, filtruje zdarzenia i agreguje dane w mapach BPF.Niestandardowe dla poszczególnych danych
Czytnik JNI BTFAnalizuje metadane BTF ze skompilowanego binarnego pliku BPF Executable and Linkable Format (ELF) w czasie działania, buforuje przesunięcia elementów struktury i wykonuje odczyty mapy wsadowej do tablic pierwotnych Java.Ogólne
Pobieranie w przestrzeni użytkownika w statsdPobiera skumulowane mapy histogramów według harmonogramu dziennego, oblicza różnice w stosunku do buforowanej historii i tworzy silnie typizowane atomy pobierania StatsEvent.Ogólna funkcja pomocnicza z pojedynczym wpisem case na atom
Schemat telemetriiDefiniuje silnie typizowany atom pobierania statsd protobuf i agregacje histogramów lub wartości po stronie serwera.Niestandardowe dla poszczególnych danych

Modele wykorzystania w przestrzeni użytkownika

Podczas projektowania danych telemetrii jądra wybierz jeden z 2 modeli wykorzystania w przestrzeni użytkownika w zależności od tego, czy dane reprezentują stan skumulowany, czy uporządkowaną sekwencję zdarzeń. W tabeli poniżej porównujemy te 2 modele:

WłaściwośćAkumulatoryStrumienie
DefinicjaStanowe, monotonicznie rosnące liczniki skumulowane lub histogramyUporządkowana sekwencja zdarzeń jądra z sygnaturami czasowymi
Wzorzec dostępuPrzestrzeń użytkownika odpytuje na żądanie, np. raz na 24 godziny za pomocą statsdPrzestrzeń użytkownika korzysta w sposób ciągły z bufora pierścieniowego, aby uniknąć pominięcia zdarzeń
Typowe typy map BPFBPF_MAP_TYPE_HASH, BPF_MAP_TYPE_LRU_HASH, BPF_MAP_TYPE_PERCPU_HASH, BPF_MAP_TYPE_LRU_PERCPU_HASHBPF_MAP_TYPE_RINGBUF
Model współbieżnościWyszukiwania map jądra są niepodzielne; wielu czytelników może odpytywać bez blokowania przestrzeni użytkownikaSemantyka odczytu destrukcyjnego wymaga pojedynczego wątku czytelnika, który wysyła dane do odbiorców
Przykłady użyciaHistogramy czasu oczekiwania zarządzania pamięcią, rozmiary alokacji bufora bezpośredniego dostępu do pamięci (DMA-BUF), iteratory źródła wybudzeniaFiltrowane zdarzenia śledzenia harmonogramu, ślady zasilania podsystemu o wysokiej częstotliwości

Wbudowane dane telemetrii jądra

Android zawiera wbudowane programy telemetrii jądra eBPF, które działają w jądrach GKI 6.1 i nowszych, w tym android14-6.1, android15-6.6, android16-6.12 i android17-6.18. Każde dane dotyczące czasu oczekiwania i rozmiaru używają histogramu wykładniczego (log2) z 32 przedziałami, agregowanego w ciągu 24 godzin. W tabeli poniżej znajdziesz listę wbudowanych danych telemetrii jądra:

Atom danychPodsystemOpisKlucz wymiaru
KswapdLatencyZarządzanie pamięciąCzas oczekiwania na wykonanie demona wymiany jądra, kswapd, od wybudzenia do uśpienia w mikrosekundachNon-Uniform Memory Access (NUMA) node_id
DirectReclaimLatencyZarządzanie pamięciąCzas oczekiwania na synchroniczne bezpośrednie odzyskiwanie pamięci w mikrosekundachStan procesu braku pamięci (OOM) oom_score_adj
CompactionDurationZarządzanie pamięciąCzas trwania kompresji pamięci w całym systemie w mikrosekundach, zastępujący starsze haki dostawcy kompresjiW całym systemie
CmaAllocationLatencyZarządzanie pamięciąCzas oczekiwania na alokację strony Contiguous Memory Allocator (CMA) w mikrosekundachNazwa sterty CMA
DmabufRequestSizesZarządzanie pamięciąRozmiary żądań alokacji DMA-BUF w bajtachNazwa sterty eksportera
SuspendResumeLatencyZarządzanie zasilaniemCzas oczekiwania między etapami zawieszenia i wznowienia jądra w mikrosekundachEtap działania zawieszenia

Dodawanie danych telemetrii jądra

Aby dodać nowe dane akumulatora do potoku telemetrii jądra eBPF, wykonaj te czynności w podanej kolejności:

  1. Napisz program eBPF w katalogu system/bpfprogs/telemetry/ i skompiluj plik binarny do katalogu /system/etc/bpf/kernelstats/.
  2. Sprawdź zgodność układu struktury BTF z ogólnym czytnikiem JNI BTF.
  3. Zarejestruj atom pobierania w statsd.
  4. Zdefiniuj schemat protobuf atomu pobierania.

W sekcjach poniżej jako przykład używamy DirectReclaimLatency (directReclaim.c).

Tworzenie programu eBPF

W katalogu system/bpfprogs/telemetry/ dodaj <bpf_telemetry.h> i zdefiniuj klucz mapy oraz mapę akumulatora wyjściowego. Aby zminimalizować wykorzystanie pamięci mapy i zachować prywatność, używaj w kluczach map BPF identyfikatorów całkowitych lub wyliczeń zamiast surowych ciągów znaków:

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

W procedurach obsługi punktów śledzenia zapisz sygnaturę czasową rozpoczęcia przy wejściu, oblicz przedział czasu oczekiwania log2 przy wyjściu za pomocą GET_LATENCY_BUCKET i zwiększ wpis mapy akumulatora za pomocą 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;
}

Sprawdzanie zgodności układu struktury BTF

Podczas dodawania nowej mapy histogramu nie musisz pisać niestandardowego kodu JNI C++, o ile klucz mapy i struktury wartości używają standardowych typów całkowitych, wyliczeniowych i tablic o stałym rozmiarze. Zamiast rzutować wpisy map BPF na zakodowane na stałe struktury C w czasie kompilacji, ogólny czytnik JNI rozwiązuje układy struktur dynamicznie w czasie działania w 3 etapach:

  • Powiązanie ELF z mapą: warstwa JNI analizuje metadane BTF bezpośrednio ze skompilowanego pliku BPF ELF na dysku, np. /system/etc/bpf/kernelstats/directReclaim.bpf, za pomocą funkcji libbpf btf__parse.
  • Rozpoznawanie typu BTF: przy pierwszym dostępie czytnik tworzy i buforuje reprezentację strukturalną klucza mapy i wartości struktur C, obliczając skumulowane przesunięcia bajtów dla elementów struktury, szerokości bajtów liczb całkowitych i wyliczeń oraz rozmiary elementów tablicy na podstawie wskaźnika bazowego.
  • Wyszukiwanie i serializacja wsadowa: gdy statsd pobiera mapę, warstwa JNI wysyła pojedyncze polecenie BPF_MAP_LOOKUP_BATCH do wywołania systemowego sys_bpf, aby odczytać wszystkie wpisy skrótów zbiorczo, i kopiuje rozpoznane identyfikatory kluczy oraz tablice przedziałów bezpośrednio do tablic pierwotnych Java long[] za pomocą funkcji JNI SetLongArrayRegion.

Jeśli struktura BPF C zmieni się w przyszłej wersji, ponownie skompilowana sekcja BTF zostanie automatycznie zaktualizowana, a czytnik JNI dostosuje się w czasie działania bez konieczności ponownej kompilacji JNI.

Rejestrowanie atomu pobierania w statsd

W StatsPullAtomService.java ogólna funkcja pomocnicza pullKernelTelemetry odczytuje przypiętą mapę BPF, oblicza różnice przedziałów w stosunku do buforowanych liczników z poprzedniego pobrania i emituje wpisy StatsEvent. Dodanie nowych danych telemetrii jądra wymaga tylko 3 niewielkich zmian:

// 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";

Definiowanie atomu pobierania protobuf

Zdefiniuj silnie typizowany komunikat atomu pobierania w frameworks/proto_logging/, aby pasował do klucza wymiaru i tablicy histogramu log2 z 32 przedziałami wygenerowanej przez mapę BPF:

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