Extension du noyau avec eBPF

Extended Berkeley Packet Filter (eBPF) est une machine virtuelle intégrée au noyau qui exécute les programmes eBPF fournis par l'utilisateur pour étendre les fonctionnalités du noyau. Ces programmes peuvent être connectés à des sondes ou à des événements dans le noyau et utilisés pour collecter des statistiques utiles sur le noyau, surveiller et déboguer. Un programme est chargé dans le noyau à l'aide de l'appel système bpf(2) et est fourni par l'utilisateur sous la forme d'un blob binaire d'instructions machine eBPF. Le système de build Android prend en charge la compilation de programmes C vers eBPF à l'aide de la syntaxe de fichier de build simple décrite dans ce document.

Plus d'informations sur les composants internes et l'architecture de l'eBPF sont disponibles sur la page eBPF de Brendan Gregg .

Android comprend un chargeur et une bibliothèque eBPF qui chargent les programmes eBPF au moment du démarrage.

Chargeur BPF Android

Lors du démarrage d'Android, tous les programmes eBPF situés dans /system/etc/bpf/ sont chargés. Ces programmes sont des objets binaires construits par le système de build Android à partir de programmes C et sont accompagnés de fichiers Android.bp dans l'arborescence des sources Android. Le système de build stocke les objets générés dans /system/etc/bpf , et ces objets font partie de l'image système.

Format d'un programme Android eBPF C

Un programme eBPF C doit avoir le format suivant :

#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

Où:

  • name_of_my_map est le nom de votre variable de carte. Ce nom informe le chargeur BPF du type de carte à créer et avec quels paramètres. Cette définition de structure est fournie par l'en-tête bpf_helpers.h inclus.
  • PROGTYPE/PROGNAME représente le type du programme et le nom du programme. Le type de programme peut être l'un de ceux répertoriés dans le tableau suivant. Lorsqu'un type de programme n'est pas répertorié, il n'existe pas de convention de dénomination stricte pour le programme ; le nom doit simplement être connu du processus qui attache le programme.

  • PROGFUNC est une fonction qui, une fois compilée, est placée dans une section du fichier résultant.

kprobe Accroche PROGFUNC à une instruction du noyau en utilisant l'infrastructure kprobe. PROGNAME doit être le nom de la fonction noyau en cours de vérification. Reportez-vous à la documentation du noyau de kprobe pour plus d'informations sur kprobes.
point de trace Accroche PROGFUNC à un point de trace. PROGNAME doit être au format SUBSYSTEM/EVENT . Par exemple, une section tracepoint permettant d'attacher des fonctions aux événements de changement de contexte du planificateur serait SEC("tracepoint/sched/sched_switch") , où sched est le nom du sous-système de trace et sched_switch est le nom de l'événement de trace. Consultez la documentation du noyau des événements de trace pour plus d'informations sur les points de trace.
filtre sk Le programme fonctionne comme un filtre de prise réseau.
horaires Le programme fonctionne comme un classificateur de trafic réseau.
cgroupskb, cgroupsock Le programme s'exécute chaque fois que les processus d'un CGroup créent un socket AF_INET ou AF_INET6.

Des types supplémentaires peuvent être trouvés dans le code source du Loader .

Par exemple, le programme myschedtp.c suivant ajoute des informations sur le dernier PID de tâche exécuté sur un processeur particulier. Ce programme atteint son objectif en créant une carte et en définissant une fonction tp_sched_switch qui peut être attachée à l'événement de trace sched:sched_switch . Pour plus d'informations, consultez Attachement de programmes aux points de trace .

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

La macro LICENSE est utilisée pour vérifier si le programme est compatible avec la licence du noyau lorsque le programme utilise les fonctions d'assistance BPF fournies par le noyau. Spécifiez le nom de la licence de votre programme sous forme de chaîne, par exemple LICENSE("GPL") ou LICENSE("Apache 2.0") .

Format du fichier Android.bp

Pour que le système de build Android puisse créer un programme eBPF .c , vous devez créer une entrée dans le fichier Android.bp du projet. Par exemple, pour créer un programme eBPF C nommé bpf_test.c , effectuez l'entrée suivante dans le fichier Android.bp de votre projet :

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

Cette entrée compile le programme C résultant en l'objet /system/etc/bpf/bpf_test.o . Au démarrage, le système Android charge automatiquement le programme bpf_test.o dans le noyau.

Fichiers disponibles dans sysfs

Lors du démarrage, le système Android charge automatiquement tous les objets eBPF de /system/etc/bpf/ , crée les cartes dont le programme a besoin et épingle le programme chargé avec ses cartes au système de fichiers BPF. Ces fichiers peuvent ensuite être utilisés pour une interaction ultérieure avec le programme eBPF ou pour lire des cartes. Cette section décrit les conventions utilisées pour nommer ces fichiers et leurs emplacements dans sysfs.

Les fichiers suivants sont créés et épinglés :

  • Pour tous les programmes chargés, en supposant que PROGNAME est le nom du programme et FILENAME est le nom du fichier eBPF C, le chargeur Android crée et épingle chaque programme à /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME .

    Par exemple, pour l'exemple de point de trace sched_switch précédent dans myschedtp.c , un fichier programme est créé et épinglé à /sys/fs/bpf/prog_myschedtp_tracepoint_sched_sched_switch .

  • Pour toutes les cartes créées, en supposant que MAPNAME est le nom de la carte et FILENAME est le nom du fichier eBPF C, le chargeur Android crée et épingle chaque carte à /sys/fs/bpf/map_FILENAME_MAPNAME .

    Par exemple, pour l'exemple de point de trace sched_switch précédent dans myschedtp.c , un fichier de carte est créé et épinglé à /sys/fs/bpf/map_myschedtp_cpu_pid_map .

  • bpf_obj_get() dans la bibliothèque Android BPF renvoie un descripteur de fichier à partir du fichier /sys/fs/bpf épinglé. Ce descripteur de fichier peut être utilisé pour d'autres opérations, telles que la lecture de cartes ou l'attachement d'un programme à un point de trace.

Bibliothèque Android BPF

La bibliothèque Android BPF s'appelle libbpf_android.so et fait partie de l'image système. Cette bibliothèque fournit à l'utilisateur la fonctionnalité eBPF de bas niveau nécessaire à la création et à la lecture de cartes, à la création de sondes, de points de trace et de tampons de performances.

Attacher des programmes aux points de trace

Les programmes Tracepoint sont chargés automatiquement au démarrage. Après le chargement, le programme tracepoint doit être activé en suivant ces étapes :

  1. Appelez bpf_obj_get() pour obtenir le programme fd à partir de l'emplacement du fichier épinglé. Pour plus d'informations, reportez-vous aux fichiers disponibles dans sysfs .
  2. Appelez bpf_attach_tracepoint() dans la bibliothèque BPF, en lui passant le programme fd et le nom du point de trace.

L'exemple de code suivant montre comment attacher le point de trace sched_switch défini dans le fichier source myschedtp.c précédent (la vérification des erreurs n'est pas affichée) :

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

Lecture des cartes

Les cartes BPF prennent en charge des structures ou des types de clés et de valeurs complexes et arbitraires. La bibliothèque Android BPF comprend une classe android::BpfMap qui utilise des modèles C++ pour instancier BpfMap en fonction du type de clé et de valeur de la carte en question. L'exemple de code précédent montre l'utilisation d'un BpfMap avec une clé et une valeur sous forme d'entiers. Les entiers peuvent aussi être des structures arbitraires.

Ainsi, la classe BpfMap modélisée facilite la définition d'un objet BpfMap personnalisé adapté à une carte particulière. La carte est ensuite accessible à l'aide des fonctions générées sur mesure, qui sont sensibles au type, ce qui permet d'obtenir un code plus propre.

Pour plus d'informations sur BpfMap , reportez-vous aux sources Android .

Problèmes de débogage

Pendant le démarrage, plusieurs messages liés au chargement de BPF sont enregistrés. Si le processus de chargement échoue pour une raison quelconque, un message de journal détaillé est fourni dans logcat. Le filtrage des journaux logcat par « bpf » imprime tous les messages et toutes les erreurs détaillées pendant le temps de chargement, telles que les erreurs du vérificateur eBPF.

Exemples d'eBPF dans Android

Les programmes suivants dans AOSP fournissent des exemples supplémentaires d’utilisation d’eBPF :

  • Le programme netd eBPF C est utilisé par le démon réseau (netd) sous Android à diverses fins telles que le filtrage des sockets et la collecte de statistiques. Pour voir comment ce programme est utilisé, consultez les sources du moniteur de trafic eBPF .

  • Le programme time_in_state eBPF C calcule le temps qu'une application Android passe à différentes fréquences de processeur, qui est utilisé pour calculer la puissance.

  • Dans Android 12, le programme gpu_mem eBPF C suit l'utilisation totale de la mémoire GPU pour chaque processus et pour l'ensemble du système. Ce programme est utilisé pour le profilage de la mémoire GPU.