É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ées dans le noyau à l'aide de l'appel système bpf(2) et fournies par l'utilisateur en tant que blob binaire d'instructions machine eBPF. Le système de compilation Android prend en charge la compilation de programmes C en eBPF à l'aide d'une syntaxe de fichier de compilation simple décrite dans ce document.

Pour en savoir plus sur l'architecture et les composants internes 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, puis ces objets font partie de l'image système.

Format d'un programme C eBPF Android

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 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 informe le chargeur BPF du type de carte à créer et des paramètres à utiliser. Cette définition de structure est fournie par l'élément bpf_helpers.h inclus en-tête.
  • 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 listé, il n'existe aucune convention d'attribution de noms stricte pour le programme. 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 Hooks PROGFUNC sur une instruction du kernel à l'aide de l'infrastructure kprobe. PROGNAME doit être le nom de la fonction du kernel qui est kprobed. Reportez-vous à la documentation sur le noyau kprobe pour en savoir plus sur kprobes.
point de trace Connecte PROGFUNC à un point de trace. PROGNAME doit être au format SUBSYSTEM/EVENT. Par exemple, une section tracepoint permettant d'associer des fonctions aux événements de changement de contexte du programmeur serait SEC("tracepoint/sched/sched_switch"), où sched correspond à le nom du sous-système de trace, et sched_switch est le nom de l'événement de trace. Vérifiez le noyau des événements de trace documentation pour en savoir plus sur les tracepoints.
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 chargeur.

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 licence du noyau lorsque le programme utilise les fonctions d'assistance BPF fournies par le noyau. Indiquez 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 compilation Android crée un programme .c eBPF, vous devez Créez une entrée dans le fichier Android.bp du projet. Par exemple, pour créer un programme C eBPF 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, 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 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 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 eBPF C, le chargeur Android crée et épingle chaque carte sur /sys/fs/bpf/map_FILENAME_MAPNAME.

    Par exemple, pour l'exemple de tracepoint 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 de /sys/fs/bpf fichier épinglé. Ce descripteur de fichier peut être utilisé pour 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, et créer des vérifications, des points de trace et des tampons de performances.

Associer des programmes à des tracepoints

Les programmes de point de trace sont chargés automatiquement au démarrage. Après le chargement, Pour activer le programme tracepoint, procédez comme suit:

  1. Appelez bpf_obj_get() pour obtenir le programme fd à partir de l'emplacement du fichier épinglé. Pour en savoir plus, consultez Fichiers disponibles en 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 les cartes

Les mappages BPF acceptent des structures ou types de clés et de valeurs complexes arbitraires. La La bibliothèque Android BPF inclut une classe android::BpfMap qui utilise C++ des modèles pour instancier BpfMap en fonction du type de clé et de valeur du 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 en utilisant des fonctions personnalisées générées, qui prennent en compte le type, pour un code plus propre.

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

Problèmes de débogage

Au démarrage, plusieurs messages concernant le 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. Le filtrage des journaux Logcat par bpf affiche tous les messages et les erreurs détaillées pendant le 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 C eBPF netd est utilisé par le daemon 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.

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