Rozszerzanie jądra za pomocą eBPF

Extended Berkeley Packet Filter (eBPF) to wbudowana w jądro maszyna wirtualna, która uruchamia dostarczone przez użytkownika programy eBPF w celu rozszerzenia funkcjonalności jądra. Programy te można podłączyć do sond lub zdarzeń w jądrze i używać do zbierania przydatnych statystyk jądra, monitorowania i debugowania. Program jest ładowany do jądra przy użyciu wywołania systemowego bpf(2) i dostarczany przez użytkownika w postaci binarnego obiektu typu blob instrukcji maszynowych eBPF. System kompilacji Androida obsługuje kompilowanie programów w języku C do eBPF przy użyciu prostej składni pliku kompilacji opisanej w tym dokumencie.

Więcej informacji na temat elementów wewnętrznych i architektury eBPF można znaleźć na stronie eBPF Brendana Gregga .

Android zawiera moduł ładujący eBPF i bibliotekę, która ładuje programy eBPF podczas uruchamiania.

Moduł ładujący BPF dla Androida

Podczas uruchamiania Androida ładowane są wszystkie programy eBPF znajdujące się w /system/etc/bpf/ . Programy te są obiektami binarnymi zbudowanymi przez system kompilacji Androida z programów C i towarzyszą im pliki Android.bp w drzewie źródłowym Androida. System kompilacji przechowuje wygenerowane obiekty w /system/etc/bpf , a obiekty te stają się częścią obrazu systemu.

Format programu Android eBPF C

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 will also define 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. Nazwa ta informuje moduł ładujący BPF o rodzaju mapy, którą należy utworzyć i z jakimi parametrami. Ta definicja struktury jest dostarczana przez dołączony nagłówek bpf_helpers.h .
  • PROGTYPE/PROGNAME reprezentuje typ programu i nazwę programu. Typ programu może być dowolnym z wymienionych w poniższej tabeli. Jeśli na liście nie ma typu programu, nie ma dla niego ścisłej konwencji nazewnictwa; nazwa musi być znana procesowi dołączającemu program.

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

sonda Podpina PROGFUNC do instrukcji jądra, korzystając z infrastruktury kprobe. PROGNAME musi być nazwą sprawdzanej funkcji jądra. Więcej informacji na temat kprobe można znaleźć w dokumentacji jądra kprobe .
punkt śledzenia Podłącza PROGFUNC do punktu śledzenia. PROGNAME musi mieć format SUBSYSTEM/EVENT . Na przykład sekcja punktu śledzenia służąca do dołączania funkcji do zdarzeń przełączenia kontekstu programu planującego będzie miała postać SEC("tracepoint/sched/sched_switch") , gdzie sched to nazwa podsystemu śledzenia, a sched_switch to nazwa zdarzenia śledzenia. Więcej informacji na temat punktów śledzenia można znaleźć w dokumentacji jądra zdarzeń śledzenia .
filtr sk Program pełni funkcję filtra gniazd sieciowych.
harmonogramy Program pełni funkcję klasyfikatora ruchu sieciowego.
cgroupskb, cgroupsock Program działa za każdym razem, gdy procesy w grupie C tworzą gniazdo AF_INET lub AF_INET6.

Dodatkowe typy można znaleźć w kodzie źródłowym modułu ładującego .

Na przykład następujący program myschedtp.c dodaje informacje o ostatnim identyfikatorze PID zadania, które zostało uruchomione na określonym procesorze. Program ten osiąga swój cel poprzez utworzenie mapy i zdefiniowanie funkcji tp_sched_switch , którą można dołączyć do zdarzenia śledzenia sched:sched_switch . Aby uzyskać więcej informacji, zobacz 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 sprawdzenia, czy program jest zgodny z licencją jądra, gdy program korzysta z funkcji pomocniczych BPF udostępnianych przez jądro. Określ nazwę licencji programu w formie ciągu znaków, np. LICENSE("GPL") lub LICENSE("Apache 2.0") .

Format pliku Android.bp

Aby system kompilacji Android mógł zbudować program eBPF .c , należy utworzyć wpis w pliku Android.bp projektu. Na przykład, aby zbudować program eBPF C o nazwie bpf_test.c , wykonaj następujący wpis w pliku Android.bp swojego projektu:

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

Ten wpis kompiluje program C, w wyniku czego powstaje obiekt /system/etc/bpf/bpf_test.o . Podczas uruchamiania system Android automatycznie ładuje program bpf_test.o do jądra.

Pliki dostępne w sysfs

Podczas startu system Android automatycznie ładuje wszystkie obiekty eBPF z /system/etc/bpf/ , tworzy mapy potrzebne programowi i przypina załadowany program wraz z jego mapami do systemu plików BPF. Pliki te można następnie wykorzystać do dalszej interakcji z programem eBPF lub czytania map. W tej sekcji opisano konwencje używane do nazewnictwa tych plików i ich lokalizacji w sysfs.

Tworzone i przypinane są następujące pliki:

  • Dla wszystkich załadowanych programów, zakładając, że PROGNAME to nazwa programu, a FILENAME to nazwa pliku eBPF C, moduł ładujący Androida tworzy i przypina każdy program do /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME .

    Na przykład w przypadku poprzedniego przykładu punktu śledzenia sched_switch w myschedtp.c tworzony jest plik programu i przypinany do /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch .

  • W przypadku dowolnych utworzonych map, zakładając, że MAPNAME to nazwa mapy, a FILENAME to nazwa pliku eBPF C, moduł ładujący 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 myschedtp.c tworzony jest plik mapy i przypinany do /sys/fs/bpf/map_myschedtp_cpu_pid_map .

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

Biblioteka BPF dla Androida

Biblioteka Android BPF nosi nazwę libbpf_android.so i jest częścią obrazu systemu. Ta biblioteka zapewnia użytkownikowi niskopoziomową funkcjonalność eBPF potrzebną 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 rozruchu. Po załadowaniu program Tracepoint należy aktywować, wykonując następujące czynności:

  1. Wywołaj funkcję bpf_obj_get() , aby uzyskać program fd z lokalizacji przypiętego pliku. Aby uzyskać więcej informacji, zobacz Pliki dostępne w sysfs .
  2. Wywołaj funkcję bpf_attach_tracepoint() w bibliotece BPF, 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 (nie jest pokazywane sprawdzanie błędów):

  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 Android BPF zawiera klasę android::BpfMap , która korzysta z szablonów C++ w celu utworzenia instancji BpfMap na podstawie typu klucza i wartości dla danej mapy. Poprzedni przykładowy kod demonstruje użycie BpfMap z kluczem i wartością jako liczbami całkowitymi. Liczby całkowite mogą być również dowolnymi strukturami.

Zatem szablonowa klasa BpfMap ułatwia zdefiniowanie niestandardowego obiektu BpfMap odpowiedniego dla konkretnej mapy. Dostęp do mapy można następnie uzyskać za pomocą niestandardowych funkcji, które rozpoznają typ, co zapewnia czystszy kod.

Więcej informacji na temat BpfMap można znaleźć w źródłach Androida .

Problemy z debugowaniem

Podczas uruchamiania rejestrowanych jest kilka komunikatów związanych z ładowaniem BPF. Jeśli z jakiegoś powodu proces ładowania nie powiedzie się, w logcat zostanie wyświetlony szczegółowy komunikat dziennika. Filtrowanie dzienników logcat według „bpf” powoduje wyświetlenie wszystkich komunikatów i wszelkich szczegółowych błędów występujących w czasie ładowania, takich jak błędy weryfikatora eBPF.

Przykłady eBPF w Androidzie

Następujące programy w AOSP dostarczają dodatkowych przykładów użycia eBPF:

  • Program netd eBPF C jest używany przez demona sieciowego (netd) w systemie Android do różnych celów, takich jak filtrowanie gniazd i zbieranie statystyk. Aby zobaczyć, jak używany jest ten program, sprawdź źródła monitora ruchu eBPF .

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

  • W systemie Android 12 program gpu_mem eBPF C śledzi całkowite wykorzystanie pamięci GPU dla każdego procesu i całego systemu. Ten program służy do profilowania pamięci GPU.