Dans DPDK, chaque pool de mémoire est identifié par un nom et géré via une interface appelée gestionnaire de pool mémoire. L'implémentation par défaut repose sur des queues circulaires.
Les pools mémoire offrent des fonctionnalités optionnelles telles que :
- Cache par cœur : Pour un accès rapide aux objets au niveau de chaque cœur CPU.
- Outil d'alignement des objets : Permet de padder les objets pour une distribution uniforme sur les canaux et rangs de la mémoire DRAM.
Mode de débogage : les "Cookies"
En mode de débogage, DPDK ajoute des champs spéciaux, appelés cookies, avant et après chaque objet alloué. Ces champs servent à détecter les erreurs de dépassement ou d'écrasement de mémoire (par exemple, dépassement de tampon, accès non alignés).
Le mode de débogage est désactivé par défaut. Pour l'activer, définissez la macro suivante dans config/rte_config.h :
#define RTE_LIBRTE_MEMPOOL_DEBUG 1
Statistiques des pools mémoire
Si le mode statistiques est activé, DPDK enregistre des données sur l'acquisition et la restitution des objets depuis le pool. Ces statistiques sont stockées dans la structure mempool. Elles incluent, par exemple, le nombre d'appels à rte_mempool_get() et rte_mempool_put(). Les statistiques sont maintenues indépendamment pour chaque lcore afin d'éviter les contentions de verrouillage lors des mises à jour concurrentes.
Par défaut, le mode statistiques est désactivé. Pour l'activer, définissez la macro suivante dans rte_config.h :
#define RTE_LIBRTE_MEMPOOL_STATS 1
Contraintes d'alignement mémoire sur x86
Sur les architectures x86, ajouter un remplissage mémoire spécifique (padding) entre les objets peut améliorer significativement les performances, en fonction de la configuraton matérielle de la mémoire. L'objectif est de s'assurer que l'adresse de début de chaque objet se situe sur un canal et un rang mémoire distincts, ce qui permet d'utiliser les canaux mémoire de manière équilibrée et d'éviter les goulots d'étranglement.
Cette optimisation est particulièrement bénéfique pour les opérations de transfert L3 ou de classification de flux. La raison est que ces opérations n'accèdent souvent qu'aux 64 premiers octets des paquets. Si tous les paquets commencent sur le même canal mémoire, le CPU ne peut lire qu'à partir de ce canal unique. Une répartition uniforme des adresses de départ sur plusieurs canaux permet à plusieurs canaux de travailler en parallèle, augmentant ainsi considérablement la bande passante d'accès.
Qu'est-ce qu'un Rang ?
Un rang (rank) est un groupe de DRAM sur un même module DIMM (barrette mémoire) qui peut être accédé indépendamment pour fournir une largeur de données complète. Bien que plusieurs rangs partagent le même canal de données, un seul rang peut être actif à la fois.
Il est important de noter que la disposition physique des rangs sur un DIMM peut différer du nombre logique de rangs.
Comment activer l'optimisation des canaux/rangs ?
Lors de l'exécution d'une application DPDK, le nombre de canaux mémoire et de rangs peut être spécifié via les paramètres de démarrage de l'EAL :
./my_app --memory-channels=4
Cela indique à DPDK comment effectuer l'alignement des objets (par exemple, alignement sur la ligne de cache et répartition inter-canaux) et comment définir la stratégie de remplissage pour une meilleure distribution des objets.
Résumé simplifié :
| Concept | Description |
|---|---|
| Canal mémoire | Voies de communication à haute vitesse entre le CPU et la mémoire ; plusieurs canaux peuvent fonctionner en parallèle. |
| Rang | Groupe de DRAM sur un DIMM pouvant être accédé individuellement, mais partageant un canal. |
| Méthode d'optimisation | Ajout d'un padding approprié à chaque objet pour le répartir sur différents canaux/rangs. |
| Objectif | Permettre au CPU d'utiliser simultanément plusieurs canaux mémoire pour le chargement des données, améliorant ainsi le débit. |
| Scénarios pratiques | Plus efficace pour le traitement de paquets (L3, classification) qui n'accèdent qu'aux 64 premiers octets. |
| Activation | Spécifier via le paramètre EAL --memory-channels=N. |
Cache local
Dans les systèmes multi-cœurs, si plusieurs cœurs accèdent fréquemment à la "queue circulaire d'objets libres" du même pool mémoire, des opérations atomiques (comme CAS) sont nécessaires à chaque accès, ce qui entraîne une surcharge CPU élevée. Pour pallier ce problème, DPDK maintient un cache local par cœur.
L'allocateur de pool mémoire ne récupère plus les objets de la file d'attente globale à chaque fois. Il les prend d'abord dans le cache du cœur courant. Ce n'est que lorsque le cache local est vide ou plein qu'une interaction en masse (gestion groupée) est effectuée. Ce cache local est une petite structure de données, propre à chaque cœur, qui ne nécessite pas de partage. Son activation peut être choisie lors de la création du pool mémoire.
Gestionnaires de pools mémoire (Mempool Handlers)
Un gestionnaire de pool mémoire définit la stratégie d'allocation et de libération des objets. DPDK prend en charge l'ajout de gestionnaires personnalisés.
Implémentation d'un gestionnaire personnalisé
Pour implémenter un gestionnaire personnalisé, vous devez :
- Implémenter une structure d'opérations
mempool_ops(fonctions pour.alloc,.put,.get, etc.). - Enregistrer cette structure à l'aide de la macro : ```
RTE_MEMPOOL_REGISTER_OPS(my_mempool_ops);
Cela enregistre un nouveau type de pool mémoire.
Utilisation d'un nouveau gestionnaire
Utilisez les API suivantes pour créer un pool mémoire avec votre gestionnaire personnalisé :
rte_mempool_create_empty()pour créer un pool vide.rte_mempool_set_ops_byname()pour spécifier le nom du gestionnaire à utiliser.
Plusieurs gestionnaires peuvent être utilisés simultanément dans une même application. Par exemple, un pool peut utiliser l'implémentation par défaut basée sur les files d'attente circulaires, tandis qu'un autre utilise une implémentation personnalisée NUMA-aware.
Compatibilité avec les anciennes interfaces
Les anciennes versions de DPDK utilisaient principalement rte_mempool_create(), qui utilisait par défaut le gestionnaire basé sur les files d'attente circulaires. Pour passer à un nouveau gestionnaire, il faut modifier le code pour utiliser la combinaison de rte_mempool_create_empty() et rte_mempool_set_ops_byname().
Configuration spécifique pour pktmbuf
Pour les buffers de paquets (rte_pktmbuf), le gestionnaire par défaut peut être spécifié via une macro de configuration :
#define RTE_MBUF_DEFAULT_MEMPOOL_OPS "my_ops_name"
Notes importantes pour les bibliothèques partagées
Si votre application utilise des bibliothèques partagées DPDK :
- Vous pouvez spécifier la bibliothèque dynamique du gestionnaire à charger à l'aide du paramètre EAL
-d: ``` ./app -d my_handler.so - Dans une application multiprocessus, les paramètres
-dpour tous les processus enfants doivent être dans le même ordre pour éviter les erreurs d'enregistrement du gestionnaire.
Résumé simplifié des gestionnaires :
| Concept | Description |
|---|---|
| Gestionnaire (Handler) | Définit la méthode d'allocation/libération des objets pour un pool (ex: ring, stack, buffer GPU). |
| Utilisation | Enregistrer la structure ops + utiliser rte_mempool_create_empty() + set_ops_byname(). |
| Multiples gestionnaires | Une application peut utiliser plusieurs gestionnaires différents simultanément. |
| Anciens programmes | Utilisent par défaut le gestionnaire 'ring'. Une modification du code est nécessaire pour changer. |
| Chargement dynamique | L'ordre des paramètres -d doit être identique pour tous les processus. |
Comparaison des interfaces : Ancienne vs Nouvelle
Ancienne interface : rte_mempool_create()
- Avantages : Simple, création en une ligne, utilise par défaut le gestionnaire basé sur les files d'attente circulaires, pratique pour la plupart des cas d'utilisation génériques de
mbuf. - Limites : Gestionnaire codé en dur (non personnalisable), ne prend pas en charge la mémoire externe (comme les fichiers hugepage, les zones DMA, la mémoire partagée d'appareils), faible extensibilité pour des logiques avancées (allocation NUMA-aware, caches personnalisés, gestion de buffers inter-appareils), difficultés à définir certains indicateurs (désactivation du cache, alignement personnalisé, contrôle du mappage physique).
Nouvelle interface (API modulaire)
Le flux d'utilisasion est le suivant :
struct rte_mempool *mp = rte_mempool_create_empty("my_pool", ...);
rte_mempool_set_ops_byname(mp, "ring_mp_mc"); // Gestionnaire optionnel
rte_mempool_populate_default(mp); // Remplissage mémoire
- Avantages :
- Haute flexibilité : Le gestionnaire n'est pas codé en dur ; possibilité de changer dynamiquement (ex:
stack,bucket, ou gestionnaires personnalisés). - Support de la mémoire externe : Adapté aux scénarios zero-copy, VRAM DMA, mémoire partagée d'appareils.
- Idéal pour les applications avancées : Gestion de buffers partagés pour les périphériques réseau, zero-copy, multi-nœuds NUMA, gestion de mémoire coordonnée par les pilotes PMD.
- Contrôle granulaire : Permet d'insérer des informations de débogage ou des stratégies spécifiques à chaque étape, facilitant la construction de logiques de création de pools complexes.
- Meilleur suppport des tests et des mécanismes de plugins : Chargement dynamique de gestionnaires via des fichiers
.so.
- Haute flexibilité : Le gestionnaire n'est pas codé en dur ; possibilité de changer dynamiquement (ex:
Comparaison d'exemples
Ancienne interface :
struct rte_mempool *mp = rte_mempool_create("mypool", 1024,
2048, 512, sizeof(struct rte_pktmbuf_pool_private),
NULL, NULL, NULL, NULL, SOCKET_ID_ANY, 0);
Nouvelle interface :
struct rte_mempool *mp = rte_mempool_create_empty("mypool", 1024,2048, 512, SOCKET_ID_ANY, 0);
rte_mempool_set_ops_byname(mp, "ring_mp_mc");
rte_mempool_populate_default(mp);
Cas d'usage où la nouvelle interface offre un gain de performance significatif :
| Type de scénario | Recommandation (Nouvelle Interface) | Amélioration |
|---|---|---|
| Optimisation NUMA | Fortement recommandé | Réduction des coûts d'accès à distance. |
| Mémoire externe / VRAM DMA | Indispensable | Zero-copy, mémoire partagée d'appareils. |
| Optimisation au démarrage | Optionnel | Remplissage groupé plus rapide. |
| Mempools partagés par plusieurs PMD | Recommandé | Économie de mémoire, performances plus stables. |
| Tests logiciels, simulation | Recommandé | Plus léger, plus rapide, plus contrôlable. |
| Allocations simples de mbuf | Peut continuer à utiliser l'ancienne interface | Aucune nécessité de basculer. |