Rozszerzanie jądra 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 ładowany do jądra za pomocą wywołania systemowego bpf(2) i jest dostarczany przez użytkownika jako binarna porcja instrukcji 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 oznacza typ programu i jego nazwę. Program może należeć do dowolnego typu wymienionego w poniższej tabeli. Jeśli dany typ programu nie jest wymieniony, nie obowiązują żadne ścisłe zasady nazewnictwa programu. Nazwa musi być znana tylko procesowi, który łączy program.

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

kprobe Za pomocą infrastruktury kprobe PROGFUNC podpina się pod instrukcję jądra. PROGNAME musi być nazwą funkcji jądra, do której ma być zastosowany kprobe. Więcej informacji o kprobe znajdziesz w dokumentacji dotyczącej jądra kprobe.
punkt śledzenia Elementy przykuwające uwagę PROGFUNC są przyczepiane do punktu śledzenia. PROGNAME musi mieć format SUBSYSTEM/EVENT. Na przykład sekcja punktów przekierowania służąca do dołączania funkcji do zdarzeń przełączania kontekstu przez harmonogram to SEC("tracepoint/sched/sched_switch"), gdzie sched to nazwa podsystemu śledzenia, a sched_switch to nazwa zdarzenia śledzenia. Więcej informacji o punktach pomiaru znajdziesz w dokumentacji kernela zdarzeń pomiarowych.
skfilter Program działa jako filtr gniazdka sieciowego.
schedcls Program działa jako 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 ładowania.

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 przez utworzenie mapy i zdefiniowanie 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ł skompilować program eBPF .c, musisz utworzyć wpis w pliku Android.bp projektu. Na przykład aby skompilować program eBPF C o nazwie bpf_test.c, w pliku Android.bp projektu wykonaj tę czynność:

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 systemu 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 można następnie wykorzystać do dalszej interakcji z programem eBPF lub do odczytu map. W tej sekcji opisano konwencje nazewnictwa tych plików oraz ich lokalizację w sysfs.

Tworzone i przypinane są te pliki:

  • 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 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 funkcje eBPF niskiego poziomu potrzebne 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 śledzenia musisz go aktywować, wykonując te czynności:

  1. Zadzwoń pod numer bpf_obj_get(), aby uzyskać dostęp do programu 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 pokazane):

  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 korzysta ze szablonów C++, aby utworzyć instancję BpfMap na podstawie typu klucza i wartości 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 uzyskać dostęp za pomocą funkcji wygenerowanych niestandardowo, które są świadome typu, co skutkuje czystszym kodem.

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

debugowanie problemów.

Podczas uruchamiania odnotowuje się kilka komunikatów związanych z wczytywaniem BPF. Jeśli proces wczytywania nie powiedzie się z jakiegokolwiek powodu, w pliku logcat znajdziesz szczegółowy komunikat. 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 w 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 gpu_mem program eBPF C śledzi łączne wykorzystanie pamięci GPU przez każdy proces i przez cały system. Ten program służy do profilowania pamięci GPU.