Rozbuduj jądro za pomocą eBPF

Rozszerzony filtr pakietów Berkeley (eBPF) to maszyna wirtualna w jądrze, która wykonuje dostarczane przez użytkownika programy eBPF w celu rozszerzenia funkcjonalności jądra. Te programy można podłączyć do sond lub zdarzeń w rdzeniu, aby zbierać przydatne statystyki jądra, je monitorować i debugować. Program jest wczytywany do jądra przy użyciu wywołania syscall bpf(2) i jest udostępniany przez użytkownika w postaci binarnego obiektu blob instrukcji maszyny eBPF. System kompilacji Androida obsługuje kompilowanie programów C na eBPF za pomocą prostej składni pliku kompilacji opisanej w tym dokumencie.

Więcej informacji o strukturze i elementach wewnętrznych eBPF znajdziesz na stronie Brendana Gregga poświęconej eBPF.

Android zawiera ładowarkę eBPF i bibliotekę, które wczytują programy eBPF podczas uruchamiania.

Ładowarka BPF na Androida

Podczas uruchamiania Androida wczytywane są wszystkie programy eBPF znajdujące się w /system/etc/bpf/. Są to obiekty binarne utworzone przez system kompilacji Androida na podstawie programów C. W drzewie źródeł Androida towarzyszą im pliki Android.bp. System kompilacji przechowuje wygenerowane obiekty w katalogu /system/etc/bpf, a te obiekty stają się częścią obrazu systemu.

Format programu eBPF C na Androida

Program eBPF C musi mieć następujący format:

#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

Gdzie:

  • name_of_my_map to nazwa zmiennej mapy. Ta nazwa informuje program ładujący BPF o tym, jaki typ mapy utworzyć i z jakimi parametrami. Ta definicja struktury jest dostarczana przez dołączony nagłówek bpf_helpers.h.
  • PROGTYPE/PROGNAME reprezentuje typ programu i jego nazwę. Program może należeć do dowolnego typu wymienionego w tabeli poniżej. Gdy program nie jest wymieniony na liście, nie ma ścisłej konwencji nazewnictwa. Nazwa musi być znana tylko dla procesu, który spełnia warunki programu.

  • PROGFUNC to funkcja, która po skompilowaniu jest umieszczana w sekcji pliku wynikowego.

kprobe Za pomocą infrastruktury kprobe podłącza PROGFUNC do instrukcji jądra. PROGNAME musi być nazwą tworzonej funkcji jądra. Więcej informacji o kprobe znajdziesz w dokumentacji dotyczącej jądra kprobe.
punkt kontrolny Pociąga PROGFUNC w punktu śledzenia. PROGNAME musi mieć format SUBSYSTEM/EVENT. Na przykład sekcja punktów przełączania w celu dołączania funkcji do zdarzeń przełączania kontekstu w planiście miałaby postać SEC("tracepoint/sched/sched_switch"), gdzie sched to nazwa podsystemu śledzenia, a sched_switch to nazwa zdarzenia śledzenia. Więcej informacji o punktach przechwytywania znajdziesz w dokumentacji dotyczącej jądra systemu Android.
skfilter Program działa jako filtr gniazdka sieciowego.
schedcls Program działa jak klasyfikator ruchu sieciowego.
cgroupskb, cgroupsock Program jest uruchamiany, gdy procesy w CGroup tworzą gniazdo AF_INET lub AF_INET6.

Dodatkowe typy można znaleźć w kodzie źródłowym komponentu Loader.

Na przykład program myschedtp.c dodaje informacje o najnowszym PID zadania, które było wykonywane na określonym procesorze. Ten program osiąga swój cel poprzez tworzenie mapy i definiowanie funkcji tp_sched_switch, którą można dołączyć do zdarzenia sched:sched_switch śledzenia. Więcej informacji znajdziesz w artykule Dołączanie programów do punktów śledzenia.

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

Makro LICENSE służy do sprawdzania, czy program jest zgodny z licencją jądra, gdy korzysta z funkcji pomocniczych BPF udostępnianych przez jądro. Podaj nazwę licencji programu w postaci ciągu tekstowego, np. LICENSE("GPL") lub LICENSE("Apache 2.0").

Format pliku Android.bp

Aby system kompilacji Androida mógł utworzyć program eBPF .c, musisz utworzyć wpis w pliku Android.bp projektu. Aby na przykład utworzyć program eBPF C o nazwie bpf_test.c, w pliku Android.bp projektu utwórz taką pozycję:

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

Ten wpis kompiluje program C, tworząc obiekt /system/etc/bpf/bpf_test.o. Podczas uruchamiania system Android automatycznie wczytuje program bpf_test.o do jądra.

Pliki dostępne w sysfs

Podczas uruchamiania system Android automatycznie wczytuje wszystkie obiekty eBPF z /system/etc/bpf/, tworzy mapy potrzebne programowi i przypina wczytany program wraz z mapami do systemu plików BPF. Pliki te mogą być następnie wykorzystywane do dalszej interakcji z programem eBPF i odczytywaniem map. W tej sekcji opisujemy konwencje nazewnictwa tych plików oraz ich lokalizację w plikach sysf.

Te pliki zostaną utworzone i przypięte:

  • W przypadku wczytanych programów (przy założeniu, że PROGNAME to nazwa programu, a FILENAME to nazwa pliku eBPF C) ładowarka Androida tworzy i przypina każdy program w miejscu /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME.

    Na przykład w przypadku poprzedniego przykładu punktu śledzenia sched_switch w pliku myschedtp.c tworzy się plik programu i przypina go do pliku /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch.

  • W przypadku utworzonych map (przy założeniu, że MAPNAME to nazwa mapy, a FILENAME to nazwa pliku eBPF C) ładowarka Androida tworzy i przypina każdą mapę do /sys/fs/bpf/map_FILENAME_MAPNAME.

    Na przykład w przypadku poprzedniego przykładu punktu śledzenia sched_switch w pliku myschedtp.c tworzy się plik mapy i przypina się go do pliku /sys/fs/bpf/map_myschedtp_cpu_pid_map.

  • bpf_obj_get() w bibliotece BPF Androida zwraca opis pliku z przypiętego pliku /sys/fs/bpf. Ten deskryptor pliku może być używany do dalszych operacji, takich jak odczytywanie map lub dołączanie programu do punktu śledzenia.

Biblioteka BPF na Androida

Biblioteka BPF na Androida nosi nazwę libbpf_android.so i jest częścią obrazu systemu. Ta biblioteka zapewnia użytkownikowi niskopoziomowe funkcje eBPF niezbędne do tworzenia i odczytywania map, tworzenia sond, punktów śledzenia i buforów wydajności.

Dołączanie programów do punktów śledzenia

Programy Tracepoint są ładowane automatycznie podczas uruchamiania. Po załadowaniu programu punktów kontrolnych należy go aktywować w ten sposób:

  1. Wywołaj bpf_obj_get(), aby uzyskać program fd z lokalizacji przypiętego pliku. Więcej informacji znajdziesz w artykule Pliki dostępne w sysfs.
  2. W bibliotece BPF wywołaj funkcję bpf_attach_tracepoint(), przekazując jej program fd i nazwę punktu śledzenia.

Poniższy przykładowy kod pokazuje, jak dołączyć punkt śledzenia sched_switch zdefiniowany w poprzednim pliku źródłowym myschedtp.c (sprawdzanie błędów nie jest widoczne):

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

Czytanie z map

Mapy BPF obsługują dowolne złożone struktury lub typy kluczy i wartości. Biblioteka BPF Androida zawiera klasę android::BpfMap, która wykorzystuje szablony C++ do tworzenia instancji BpfMap na podstawie klucza i typu wartości danej mapy. Poprzedni przykład kodu pokazuje użycie obiektu BpfMap z kluczem i wartością jako liczbami całkowitymi. Liczby całkowite mogą też być dowolnymi strukturami.

Dzięki temu szablonowa klasa BpfMap umożliwia zdefiniowanie niestandardowego obiektu BpfMap odpowiedniego dla danej mapy. Do mapy można się potem dostać za pomocą funkcji wygenerowanych niestandardowo, które są świadome typu, co powoduje, że kod jest czystszy.

Więcej informacji o BpfMap znajdziesz w źródłach Androida.

Rozwiązywanie problemów

Podczas uruchamiania odnotowuje się kilka komunikatów związanych z wczytywaniem BPF. Jeśli z jakiegoś powodu proces wczytywania nie powiedzie się, w logcat wyświetli się szczegółowy komunikat logu. Filtrowanie logów logcat za pomocą bpf powoduje wyświetlenie wszystkich wiadomości i błędów szczegółowych podczas wczytywania, takich jak błędy weryfikatora eBPF.

Przykłady eBPF na Androidzie

Dodatkowe przykłady korzystania z eBPF znajdziesz w tych programach w AOSP:

  • netd eBPF C program jest używany przez demona sieci (netd) w Androidzie do różnych celów, takich jak filtrowanie gniazd i zbieranie statystyk. Aby sprawdzić, jak ten program jest używany, sprawdź źródła w monitorze ruchu eBPF.

  • Program time_in_state eBPF C oblicza czas, jaki aplikacja na Androida spędza na różnych częstotliwościach procesora, co jest wykorzystywane do obliczania mocy.

  • W Androidzie 12 program eBPF C gpu_mem śledzi całkowite wykorzystanie pamięci GPU w każdym procesie i w całym systemie. Ten program służy do profilowania pamięci GPU.