Une map eBPF est une structure de données du noyau, partagée avec l'espace utilisateur : c'est par elle que votre programme mémorise un état et transmet ce qu'il observe. Sans les maps, un programme eBPF serait muet et sans mémoire. Cette page explique ce qu'elles sont, les grands types (table de hachage, tableau, ring buffer), et montre sur un exemple réel le noyau qui écrit et l'espace utilisateur qui lit la même map. Public : toute personne qui écrit des programmes eBPF au-delà du simple affichage. Sorties capturées sur une VM Ubuntu 24.04 (noyau 6.8).
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre pourquoi les maps sont indispensables
- Distinguer les grands types de maps et leur usage
- Lire et écrire une même map depuis le noyau et depuis l'espace utilisateur
- Choisir entre stocker un état et diffuser un flux d'événements
- Éviter les pièges classiques (test
NULL, contention, taille)
Le problème que les maps résolvent
Section intitulée « Le problème que les maps résolvent »Un programme eBPF a deux limites fortes. D'abord, il ne garde aucun souvenir d'un déclenchement à l'autre : ses variables locales disparaissent à chaque fin d'exécution. Ensuite, il ne peut pas parler directement à un programme en espace utilisateur pour lui livrer un résultat. Le helper (une fonction que le noyau met à disposition des programmes eBPF) bpf_trace_printk, vu ailleurs, dépanne pour déboguer, mais il écrit dans un canal unique et partagé, inadapté au vrai travail.
Les maps lèvent ces deux limites d'un coup. Ce sont des zones de mémoire gérées par le noyau, persistantes (elles survivent entre les déclenchements) et partagées : le programme noyau y accède par des helpers, l'espace utilisateur par des appels système. La map est le point de rendez-vous des deux mondes.
Qu'est-ce qu'une map, concrètement
Section intitulée « Qu'est-ce qu'une map, concrètement »Une map associe une clé à une valeur, toutes deux de type fixe défini à la création. Vous choisissez à la déclaration : le type de map (qui détermine son comportement), la taille de la clé, la taille de la valeur, et le nombre maximal d'entrées. À partir de là, le programme lit, écrit et met à jour des entrées ; l'espace utilisateur fait de même de son côté.
Le catalogue du noyau compte de nombreux types de maps. Voici les plus courants et ce à quoi ils servent.
| Type de map | À quoi elle sert |
|---|---|
| Hash | Association clé -> valeur quelconque (compteurs par processus, par IP...) |
| Array | Tableau indexé par entier, taille fixe, accès très rapide |
| Per-CPU hash / array | Une copie par cœur, pour compter sans contention entre processeurs |
| LRU hash | Table de hachage qui évince automatiquement les entrées les moins récentes |
| Ring buffer | File d'attente pour diffuser un flux d'événements vers l'espace utilisateur |
| Stack trace | Stockage des piles d'appel, base du profilage |
Un exemple : le noyau écrit, l'espace utilisateur lit
Section intitulée « Un exemple : le noyau écrit, l'espace utilisateur lit »Rien ne vaut de voir les deux côtés de la map. Le programme suivant compte les appels système par processus. Le noyau accumule dans une hash map ; le script Python la lit ensuite. On utilise BCC, où le programme C est embarqué dans Python.
from bcc import BPFfrom time import sleep
programme = r"""struct cle_t { char nom[16]; };BPF_HASH(compteur, struct cle_t, u64); // la map : nom de processus -> nombre
TRACEPOINT_PROBE(raw_syscalls, sys_enter) { // a chaque appel systeme struct cle_t cle = {}; bpf_get_current_comm(&cle.nom, sizeof(cle.nom)); u64 zero = 0, *valeur; valeur = compteur.lookup_or_try_init(&cle, &zero); // recupere ou cree l'entree if (valeur) // test NULL exige par le verificateur (*valeur)++; return 0;}"""b = BPF(text=programme)
print("Comptage des appels systeme pendant 5 secondes...\n")sleep(5)
print(f"{'PROCESSUS':<16} {'APPELS':>8}")items = sorted(b["compteur"].items(), key=lambda kv: kv[1].value)for cle, valeur in items[-8:]: # les 8 plus actifs print(f"{cle.nom.decode():<16} {valeur.value:>8}")Deux mécanismes clés dans le programme noyau :
BPF_HASH(compteur, struct cle_t, u64)déclare la map : son nom, le type de la clé (une structure contenant le nom du processus), le type de la valeur (un entier 64 bits).lookup_or_try_initcherche l'entrée pour la clé ; si elle n'existe pas, elle la crée à zéro. La valeur renvoyée peut être nulle, d'où leif (valeur)que le vérificateur exige.
Côté Python, b["compteur"].items() lit la même map depuis l'espace utilisateur. Sortie réelle :
PROCESSUS APPELSsystemd 19multipathd 30sleep 5376uname 5520cat 5808bash 6430ls 7104id 10560Le noyau a rempli la map en temps réel ; Python l'a lue à la fin. C'est exactement le rôle d'une map : un état partagé entre les deux côtés de la frontière, sans que le noyau ait eu à envoyer chaque appel système un par un à l'espace utilisateur.
Deux façons d'utiliser une map
Section intitulée « Deux façons d'utiliser une map »L'exemple précédent illustre le premier usage. Il y en a deux, à ne pas confondre.
Stocker un état, puis le lire (comme ci-dessus). Le noyau agrège dans une hash ou un array ; l'espace utilisateur lit périodiquement le résultat. Idéal pour des compteurs, des statistiques, des tables de correspondance. Le volume reste maîtrisé : peu importe le nombre d'événements, la map ne contient que le résumé.
Diffuser un flux d'événements. Quand on veut recevoir chaque événement avec son détail (un nouveau processus, une connexion réseau), on utilise un ring buffer (littéralement « tampon circulaire »). Le programme y pousse un enregistrement à chaque événement, l'espace utilisateur les consomme au fil de l'eau. C'est la bonne structure pour un flux, là où un affichage direct ne passerait pas l'échelle.
Inspecter les maps avec bpftool
Section intitulée « Inspecter les maps avec bpftool »Comme pour les programmes, bpftool montre les maps réellement présentes dans le noyau, indépendamment de l'outil qui les a créées :
sudo bpftool map showChaque map y apparaît avec son identifiant, son type, la taille de sa clé et de sa valeur, son nombre maximal d'entrées. On peut aussi vider son contenu avec bpftool map dump. C'est précieux pour déboguer : vérifier qu'une map se remplit, ou lire son état sans écrire de code.
Sécurité et pièges
Section intitulée « Sécurité et pièges »- Toujours tester le retour d'un
lookupcontreNULL. Le vérificateur l'impose ; l'oublier est le refus le plus fréquent, détaillé dans le vérificateur eBPF. - Compteurs très sollicités : préférez une map per-CPU. Quand plusieurs cœurs incrémentent la même entrée, ils se gênent (contention). Une map per-CPU donne à chaque cœur sa copie ; l'espace utilisateur additionne à la lecture. Le débit y gagne beaucoup.
- La taille est bornée. Une map a un nombre maximal d'entrées fixé à la création. Une hash pleine refuse les nouvelles clés ; une LRU hash évince les plus anciennes à la place. Choisissez selon le besoin.
- Les maps consomment de la mémoire verrouillée. Elles occupent de la RAM non swappable ; sur un système contraint, surveillez la consommation (visible dans le champ
memlockdebpftool). - Une map peut être « épinglée ». Épingler une map dans
/sys/fs/bpfla fait survivre à l'arrêt du programme qui l'a créée, et la rend accessible à d'autres. Pratique, mais c'est un partage de données à sécuriser.
À retenir
Section intitulée « À retenir »- Une map est une structure du noyau, persistante et partagée avec l'espace utilisateur : la mémoire et la voix du programme eBPF.
- On choisit son type selon l'usage : hash/array pour un état, ring buffer pour un flux d'événements, per-CPU pour compter sans contention.
- Le noyau écrit via des helpers, l'espace utilisateur lit via des appels système : c'est le pont entre les deux mondes.
- Stocker un état (compteurs, stats) et diffuser un flux (chaque événement) sont deux usages distincts.
- Toujours tester
NULLaprès unlookup, et surveiller taille et mémoire verrouillée. - bpftool inspecte et vide les maps sans écrire une ligne de code.