Les concentrateurs VPN à très haut débit et les passerelles de sortie (egress gateways) manipulent couramment des flux agrégés dépassant 40 Gbps sur des serveurs multicœurs modernes. Dans la programmation de sockets Linux habituelle, chaque émission initiée via send(2) ou sendmsg(2) contraint le noyau à copier les données de l'espace utilisateur vers les structures de tampons réseau internes (sk_buff).

Bien que négligeable pour des charges modérées, cette duplication continue de mémoire devient un goulot d'étranglement sévère à grande échelle : les cœurs CPU peuvent consacrer jusqu'à 35 % de leurs cycles d'exécution aux instructions de copie de mémoire (copy_user_generic_string), saturant la bande passante du bus mémoire et expulsant les données cryptographiques actives des caches processeur L1 et L2.

Le noyau Linux répond à cette contrainte grâce à l'indicateur d'émission MSG_ZEROCOPY (introduit dans Linux 4.14 et étendu aux sockets TCP, UDP et brutes). En verrouillant directement les pages de mémoire vive de l'application dans la table des pages du noyau et en les référençant dans des listes de dispersion-regroupement (scatter-gather list) destinées à l'accès direct à la mémoire (DMA) de la carte réseau, MSG_ZEROCOPY supprime intégralement la copie de données sur le chemin d'émission.

La congestion du bus mémoire lors des émissions réseau classiques

Pour mesurer l'impact de ce transfert conventionnel sur les performances d'une passerelle VPN, observons le cheminement d'une charge utile chiffrée :

[ Démon VPN en espace utilisateur ]
       | (Tampon de données situé en mémoire RAM utilisateur)
       | sendto(fd, buf, len, 0, ...)
       v
[ Couche Socket noyau : Appel __copy_from_user() ]
       | (Duplication intégrale des données dans les pages sk_buff)
       v
[ Pile réseau du noyau : Routage IP, Netfilter, ordonnanceur Qdisc ]
       v
[ Anneau mémoire du pilote réseau : Mappage DMA ]
       | (La carte réseau lit les données en RAM via PCIe DMA)
       v
[ Carte d'interface réseau physique (NIC) ]

Lorsqu'une application transmet un bloc de 16 Ko, le processeur doit transférer les octets de l'espace virtuel vers les registres CPU, puis les réécrire dans des pages nouvellement allouées par le noyau. À un débit soutenu de 40 Gbps, ce mécanisme brasse plus de 5 Go de données brutes par seconde à travers le bus mémoire.

Ce comportement engendre trois inconvénients majeurs :

  1. Compétition pour la bande passante du bus mémoire : Les calculs cryptographiques — comme le chiffrement de flux ChaCha20-Poly1305 et le hachage Blake2s — sollicitent intensivement les canaux de mémoire vive. Les copies réseau répétitives réduisent la bande passante disponible pour les moteurs de chiffrement.
  2. Pollution des caches CPU : La lecture et l'écriture incessantes de paquets éphémères chassent les tables de routage et les contextes de session des mémoires caches de premier niveau L1 et L2.
  3. Surcoût des changements de contexte : L'exécution synchrone bloque le fil d'exécution dans le noyau jusqu'à l'achèvement de la duplication mémoire.

Analyse architecturale du mécanisme MSG_ZEROCOPY

L'indicateur MSG_ZEROCOPY délègue la lecture de la mémoire directement au contrôleur de la carte réseau :

[ Démon VPN : Tampon applicatif buf ]
       |
       | 1. send(fd, buf, len, MSG_ZEROCOPY)
       |    (Le noyau verrouille les pages via get_user_pages_fast)
       |
       +======================+ (Référence directe, ZÉRO copie d'octets)
       |                      |
       v                      v
[ Structure skb : frags[] ] -> [ Contrôleur réseau : Lecture DMA directe ]
       |
       | 2. Émission du paquet sur le réseau physique
       | 3. La carte réseau déclenche l'interruption de fin de DMA
       v
[ File d'erreurs du socket : SO_EE_ORIGIN_ZEROCOPY ]
       |
       | 4. Le démon récupère buf via recvmsg(MSG_ERRQUEUE)
       v
[ Réutilisation sécurisée du tampon pour le cycle suivant ]

1. Verrouillage des pages via get_user_pages_fast()

Dès que l'application transmet MSG_ZEROCOPY dans l'appel send(2), le noyau Linux n'alloue aucun tampon pour les données applicatives. Il fait appel à get_user_pages_fast() pour figer les pages physiques correspondant au pointeur de l'application. Le noyau inscrit simplement ces références dans la structure d'information partagée du tampon réseau (skb_shinfo(skb)->frags[]).

Aucune donnée n'étant dupliquée par le processeur, celui-ci assemble uniquement les en-têtes (UDP et IP). La carte réseau récupère la charge utile directement en mémoire RAM via son contrôleur DMA sur le bus PCIe.

2. File d'erreurs du socket et notifications asynchrones

Puisque la carte réseau lit la mémoire de manière asynchrone, le processus utilisateur ne doit ni écraser ni désallouer le tampon immédiatement après l'appel, afin de prévenir toute altération des paquets en cours d'expédition. Le noyau orchestre la libération de la mémoire via la file d'erreurs du socket (MSG_ERRQUEUE) :

#define _GNU_SOURCE
#include <sys/socket.h>
#include <linux/errqueue.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>

/* Activer la prise en charge Zero-copy sur le descripteur */
int enable_zerocopy(int sock_fd) {
    int one = 1;
    if (setsockopt(sock_fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one)) < 0) {
        perror("Échec de setsockopt SO_ZEROCOPY");
        return -1;
    }
    return 0;
}

/* Récupérer les tampons libérés après l'achèvement du DMA physique */
void reap_zerocopy_completions(int sock_fd) {
    char control[1024];
    struct msghdr msg = {0};
    struct cmsghdr *cmsg;
    struct sock_extended_err *serr;

    msg.msg_control = control;
    msg.msg_controllen = sizeof(control);

    /* Lecture asynchrone des événements de complétion */
    while (recvmsg(sock_fd, &msg, MSG_ERRQUEUE | MSG_DONTWAIT) > 0) {
        for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
            if (cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVERR) {
                serr = (struct sock_extended_err *)CMSG_DATA(cmsg);
                if (serr->ee_origin == SO_EE_ORIGIN_ZEROCOPY) {
                    uint32_t lo = serr->ee_info;  /* Premier identifiant achevé */
                    uint32_t hi = serr->ee_data;  /* Dernier identifiant achevé */
                    /* Les tampons compris dans [lo, hi] peuvent être réutilisés */
                }
            }
        }
    }
}

Le noyau associe un numéro d'ordre 32 bits à chaque envoi sans copie. Une fois le descripteur traité par la carte réseau et le paquet émis, un message indiquant la plage d'indices libérés (ee_info à ee_data) est déposé dans la file d'erreurs.

Critères de rentabilité opérationnelle du Zero-Copy

L'approche zero-copy n'est pas adaptée à toutes les tailles de paquets. Le verrouillage des pages et le traitement des accusés de réception imposent un coût algorithmique fixe. Les ingénieurs appliquent des seuils d'arbitrage dimensionnels :

Taille de la charge utile Appel classique sendmsg() Mode MSG_ZEROCOPY Recommandation technique
Petite (< 1 500 octets) Très rapide (copie en cache) Coût de verrouillage trop élevé Utiliser sendmsg()
Moyenne (1 500 – 4 000 octets) Performances équivalentes Performances équivalentes Neutre
Grande (4 000 – 16 000 octets) Charge du bus mémoire sensible Réduction de 30 % des cycles CPU Recommandé
Très grande (> 16 Ko / GSO) Goulot d'étranglement mémoire Réduction de 55 % des cycles CPU Optimisation indispensable

Pour les passerelles combinant la segmentation UDP GSO (regroupement de paquets en super-blocs de 16 à 64 Ko), MSG_ZEROCOPY offre une accélération spectaculaire.

Intégration dans l'infrastructure VPNRice et RiceGuard

Au cœur du réseau VPNRice, les passerelles Anycast et les routeurs frontaliers associent MSG_ZEROCOPY aux flux d'émission du protocole RiceGuard :

[ Moteur de chiffrement RiceGuard ]
       | (Chiffrement dans un anneau fixe de tampons de 64 Ko)
       |
       | 1. sendto(udp_fd, ring_slot, 64KB, MSG_ZEROCOPY, ...)
       v
[ Couche Socket du noyau Linux ]
       | (Verrouillage direct des pages ring_slot sans copie)
       v
[ Carte réseau 100 GbE Mellanox / Intel ]
       | (Transmission directe par DMA PCIe)
       |
       | 2. Notification Error Queue restituant le slot au pool
       v
[ Réutilisation du tampon par RiceGuard ]
  1. Tampons circulaires pré-alloués : RiceGuard met en place un ensemble de tampons pré-verrouillés. Les trames chiffrées sont écrites directement dans ces emplacements, éliminant les allocations dynamiques en cours d'exploitation.
  2. Synergie UDP GSO et Zero-Copy : RiceGuard regroupe jusqu'à 45 datagrammes de tunnel au sein d'un bloc GSO unique de 64 Ko émis avec MSG_ZEROCOPY. Cela combine un appel système unique avec l'absence totale de duplication mémoire.
  3. Priorité mémoire aux calculs cryptographiques : La décharge du transfert vers la carte réseau libère le bus mémoire pour le chiffrement ChaCha20-Poly1305, autorisant un débit soutenu de 40 Gbps par passerelle sans rejet de paquets.

Commandes d'audit et diagnostic sous Linux

Les opérateurs système peuvent auditer l'activité des sockets et superviser les files de complétion via les outils système usuels :

# Vérifier les compteurs de sockets réseau
cat /proc/net/snmp | grep -E "Udp:|Ip:"

# Analyser l'état des files d'attente de sockets
ss -u -m -i

En affranchissant l'émission réseau de la duplication mémoire, Linux MSG_ZEROCOPY permet aux passerelles VPN modernes d'atteindre la vitesse de ligne matérielle tout en préservant l'efficacité énergétique du processeur.