Étendre le noyau avec eBPF

Extended Berkeley Packet Filter (eBPF) est une machine virtuelle dans le noyau qui exécute des 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, le surveiller et le 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 compilation Android permet de compiler des programmes C au format eBPF à l'aide d'une syntaxe de fichier de compilation simple décrite dans ce document.

Pour en savoir plus sur les composants internes et l'architecture d'eBPF, consultez la page eBPF de Brendan Gregg.

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

Chargeur Android BPF

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 créés par le système de compilation Android à partir de programmes C et sont accompagnés de fichiers Android.bp dans l'arborescence source Android. Le système de compilation 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 C eBPF Android

Un programme C eBPF doit respecter 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 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

Où :

  • name_of_my_map est le nom de votre variable de carte. Ce nom indique au chargeur BPF le type de carte à créer et les paramètres à utiliser. Cette définition de struct est fournie par l'en-tête bpf_helpers.h inclus.
  • PROGTYPE/PROGNAME représente le type et le nom du programme. Le type du programme peut être l'un de ceux listés dans le tableau suivant. Lorsqu'un type de programme n'est pas répertorié, il n'existe aucune convention d'attribution de noms stricte. Le nom doit simplement être connu du processus qui associe le programme.

  • PROGFUNC est une fonction qui, lors de la compilation, est placée dans une section du fichier généré.

kprobe Relie PROGFUNC à une instruction de noyau à l'aide de l'infrastructure kprobe. PROGNAME doit être le nom de la fonction du kernel qui est kprobed. Reportez-vous à la documentation du noyau kprobe pour en savoir plus sur kprobes.
point de trace Accroche PROGFUNC à un point de trace. PROGNAME doit être au format SUBSYSTEM/EVENT. Par exemple, une section de point de trace pour associer 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 suivi et sched_switch est le nom de l'événement de suivi. Pour en savoir plus sur les points de trace, consultez la documentation du kernel sur les événements de trace.
skfilter Le programme fonctionne comme un filtre de socket réseau.
schedcls Le programme fonctionne comme un classificateur de trafic réseau.
cgroupskb, cgroupsock Le programme s'exécute chaque fois que des processus d'un CGroup créent un socket AF_INET ou AF_INET6.

Vous trouverez d'autres types dans le code source du composant 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 pouvant être associée à l'événement de traçage sched:sched_switch. Pour en savoir plus, consultez la section Associer des programmes à des 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 permet de vérifier si le programme est compatible avec la licence du kernel lorsque le programme utilise des fonctions d'assistance BPF fournies par le kernel. Spécifiez le nom de la licence de votre programme sous la forme d'une chaîne, par exemple LICENSE("GPL") ou LICENSE("Apache 2.0").

Format du fichier Android.bp

Pour que le système de compilation Android crée un programme .c eBPF, 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, ajoutez 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, ce qui génère 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

Au démarrage, le système Android charge automatiquement tous les objets eBPF à partir 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 interagir davantage avec le programme eBPF ou 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 soit le nom du programme et que FILENAME soit le nom du fichier C eBPF, le chargeur Android crée et épingle chaque programme à /sys/fs/bpf/prog_FILENAME_PROGTYPE_PROGNAME.

    Par exemple, pour l'exemple de tracepoint sched_switch précédent dans myschedtp.c, un fichier de 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 C eBPF, 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 BPF Android 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'association d'un programme à un point de trace.

Bibliothèque Android BPF

La bibliothèque BPF Android est nommée libbpf_android.so et fait partie de l'image système. Cette bibliothèque fournit à l'utilisateur les fonctionnalités eBPF de bas niveau nécessaires pour créer et lire des cartes, créer des sondes, des tracepoints et des tampons de performances.

Associer des programmes à des points de trace

Les programmes Tracepoint sont chargés automatiquement au démarrage. Une fois le programme de tracepoint chargé, vous devez l'activer en procédant comme suit:

  1. Appelez bpf_obj_get() pour obtenir le programme fd à partir de l'emplacement du fichier épinglé. Pour en savoir plus, consultez la section Fichiers disponibles dans sysfs.
  2. Appelez bpf_attach_tracepoint() dans la bibliothèque BPF, en lui transmettant le programme fd et le nom du point de trace.

L'exemple de code suivant montre comment associer 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));

Lire à partir des cartes

Les mappages BPF acceptent des structures ou types de clés et de valeurs complexes arbitraires. La bibliothèque BPF Android inclut 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 comment utiliser une BpfMap avec une clé et une valeur sous forme d'entiers. Les entiers peuvent également être des structures arbitraires.

Ainsi, la classe BpfMap modélisée vous permet de définir un objet BpfMap personnalisé adapté à la carte en question. Vous pouvez ensuite accéder à la carte à l'aide des fonctions générées par le biais de la génération personnalisée, qui sont sensibles au type, ce qui permet d'obtenir un code plus propre.

Pour en savoir plus sur BpfMap, consultez les sources Android.

Problèmes de débogage

Au démarrage, plusieurs messages liés au chargement de BPF sont consignés. Si le processus de chargement échoue pour une raison quelconque, un message de journal détaillé est fourni dans logcat. Filtrer les journaux logcat par bpf affiche tous les messages et toutes les erreurs détaillées au moment du chargement, telles que les erreurs de vérificateur eBPF.

Exemples d'eBPF sur Android

Les programmes suivants d'AOSP fournissent d'autres exemples d'utilisation d'eBPF:

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

  • Le programme C eBPF time_in_state 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 eBPF C gpu_mem suit l'utilisation totale de la mémoire GPU pour chaque processus et l'ensemble du système. Ce programme est utilisé pour le profilage de la mémoire GPU.