J'écris un générateur de trafic en C à l'aide de l'option de socket PACKET_MMAP pour créer un tampon en anneau pour envoyer des données sur un socket brut. Le tampon en anneau est rempli de trames Ethernet à envoyer et sendto
est appelé. Le contenu entier du tampon en anneau est envoyé sur le socket, ce qui devrait donner des performances plus élevées que d'avoir un tampon en mémoire et d'appeler sendto
à plusieurs reprises pour chaque trame du tampon qui doit être envoyée.
Lorsque vous n'utilisez pas PACKET_MMAP, lors de l'appel de sendto
, une seule trame est copiée du tampon dans la mémoire de l'utilisateur vers un SK buf dans la mémoire du noyau, puis le noyau doit copier le paquet dans la mémoire accessible par le NIC pour DMA et signalez le NIC à DMA le cadre dans ses propres tampons matériels et la mettre en file d'attente pour la transmission. Lorsque vous utilisez l'option de socket PACKET_MMAP, la mémoire mappée est allouée par l'application et liée au socket brut. L'application place les paquets dans le tampon mmappé, appelle sendto
et au lieu que le noyau doive copier le paquets dans un buf SK, il peut les lire directement à partir du tampon mmappé. De plus, des "blocs" de paquets peuvent être lus à partir du tampon en anneau au lieu de paquets/trames individuels. L'augmentation des performances est donc d'un appel système pour copier plusieurs trames et une moins d'action de copie pour chaque trame pour la placer dans les tampons matériels NIC.
Lorsque je compare les performances d'un socket utilisant PACKET_MMAP à un socket "normal" (un tampon de caractères contenant un seul paquet), il n'y a aucun avantage en termes de performances. Pourquoi est-ce? Lorsque vous utilisez PACKET_MMAP en mode Tx, une seule trame peut être placée dans chaque bloc d'anneau (plutôt que plusieurs trames par bloc d'anneau comme avec le mode Rx) mais je crée 256 blocs donc nous devrions envoyer 256 images en un seul appel sendto
non?
Performances avec PACKET_MMAP, main()
appels packet_tx_mmap()
:
bensley@ubuntu-laptop:~/C/etherate10+$ Sudo taskset -c 1 ./etherate_mt -I 1
Using inteface lo (1)
Running in Tx mode
1. Rx Gbps 0.00 (0) pps 0 Tx Gbps 17.65 (2206128128) pps 1457152
2. Rx Gbps 0.00 (0) pps 0 Tx Gbps 19.08 (2385579520) pps 1575680
3. Rx Gbps 0.00 (0) pps 0 Tx Gbps 19.28 (2409609728) pps 1591552
4. Rx Gbps 0.00 (0) pps 0 Tx Gbps 19.31 (2414260736) pps 1594624
5. Rx Gbps 0.00 (0) pps 0 Tx Gbps 19.30 (2411935232) pps 1593088
Performances sans PACKET_MMAP, main()
appels packet_tx()
:
bensley@ubuntu-laptop:~/C/etherate10+$ Sudo taskset -c 1 ./etherate_mt -I 1
Using inteface lo (1)
Running in Tx mode
1. Rx Gbps 0.00 (0) pps 0 Tx Gbps 18.44 (2305001412) pps 1522458
2. Rx Gbps 0.00 (0) pps 0 Tx Gbps 20.30 (2537520018) pps 1676037
3. Rx Gbps 0.00 (0) pps 0 Tx Gbps 20.29 (2535744096) pps 1674864
4. Rx Gbps 0.00 (0) pps 0 Tx Gbps 20.26 (2533014354) pps 1673061
5. Rx Gbps 0.00 (0) pps 0 Tx Gbps 20.32 (2539476106) pps 1677329
La fonction packet_tx()
est légèrement plus rapide que la fonction packet_tx_mmap()
semble-t-il, mais elle est également légèrement plus courte, donc je pense que l'augmentation minimale des performances est tout simplement le nombre de lignes de code présent dans packet_tx
. Il me semble donc que les deux fonctions ont pratiquement les mêmes performances, pourquoi? Pourquoi PACKET_MMAP n'est-il pas beaucoup plus rapide, si je comprends bien, il devrait y avoir beaucoup moins d'appels système et de copies?
void *packet_tx_mmap(void* thd_opt_p) {
struct thd_opt *thd_opt = thd_opt_p;
int32_t sock_fd = setup_socket_mmap(thd_opt_p);
if (sock_fd == EXIT_FAILURE) exit(EXIT_FAILURE);
struct tpacket2_hdr *hdr;
uint8_t *data;
int32_t send_ret = 0;
uint16_t i;
while(1) {
for (i = 0; i < thd_opt->tpacket_req.tp_frame_nr; i += 1) {
hdr = (void*)(thd_opt->mmap_buf + (thd_opt->tpacket_req.tp_frame_size * i));
data = (uint8_t*)(hdr + TPACKET_ALIGN(TPACKET2_HDRLEN));
memcpy(data, thd_opt->tx_buffer, thd_opt->frame_size);
hdr->tp_len = thd_opt->frame_size;
hdr->tp_status = TP_STATUS_SEND_REQUEST;
}
send_ret = sendto(sock_fd, NULL, 0, 0, NULL, 0);
if (send_ret == -1) {
perror("sendto error");
exit(EXIT_FAILURE);
}
thd_opt->tx_pkts += thd_opt->tpacket_req.tp_frame_nr;
thd_opt->tx_bytes += send_ret;
}
return NULL;
}
Notez que la fonction ci-dessous appelle setup_socket()
et non setup_socket_mmap()
:
void *packet_tx(void* thd_opt_p) {
struct thd_opt *thd_opt = thd_opt_p;
int32_t sock_fd = setup_socket(thd_opt_p);
if (sock_fd == EXIT_FAILURE) {
printf("Can't create socket!\n");
exit(EXIT_FAILURE);
}
while(1) {
thd_opt->tx_bytes += sendto(sock_fd, thd_opt->tx_buffer,
thd_opt->frame_size, 0,
(struct sockaddr*)&thd_opt->bind_addr,
sizeof(thd_opt->bind_addr));
thd_opt->tx_pkts += 1;
}
}
La seule différence dans les fonctions de configuration de socket est collée ci-dessous, mais essentiellement ses exigences pour configurer un SOCKET_RX_RING ou SOCKET_TX_RING:
// Set the TPACKET version, v2 for Tx and v3 for Rx
// (v2 supports packet level send(), v3 supports block level read())
int32_t sock_pkt_ver = -1;
if(thd_opt->sk_mode == SKT_TX) {
static const int32_t sock_ver = TPACKET_V2;
sock_pkt_ver = setsockopt(sock_fd, SOL_PACKET, PACKET_VERSION, &sock_ver, sizeof(sock_ver));
} else {
static const int32_t sock_ver = TPACKET_V3;
sock_pkt_ver = setsockopt(sock_fd, SOL_PACKET, PACKET_VERSION, &sock_ver, sizeof(sock_ver));
}
if (sock_pkt_ver < 0) {
perror("Can't set socket packet version");
return EXIT_FAILURE;
}
memset(&thd_opt->tpacket_req, 0, sizeof(struct tpacket_req));
memset(&thd_opt->tpacket_req3, 0, sizeof(struct tpacket_req3));
//thd_opt->block_sz = 4096; // These are set else where
//thd_opt->block_nr = 256;
//thd_opt->block_frame_sz = 4096;
int32_t sock_mmap_ring = -1;
if (thd_opt->sk_mode == SKT_TX) {
thd_opt->tpacket_req.tp_block_size = thd_opt->block_sz;
thd_opt->tpacket_req.tp_frame_size = thd_opt->block_sz;
thd_opt->tpacket_req.tp_block_nr = thd_opt->block_nr;
// Allocate per-frame blocks in Tx mode (TPACKET_V2)
thd_opt->tpacket_req.tp_frame_nr = thd_opt->block_nr;
sock_mmap_ring = setsockopt(sock_fd, SOL_PACKET , PACKET_TX_RING , (void*)&thd_opt->tpacket_req , sizeof(struct tpacket_req));
} else {
thd_opt->tpacket_req3.tp_block_size = thd_opt->block_sz;
thd_opt->tpacket_req3.tp_frame_size = thd_opt->block_frame_sz;
thd_opt->tpacket_req3.tp_block_nr = thd_opt->block_nr;
thd_opt->tpacket_req3.tp_frame_nr = (thd_opt->block_sz * thd_opt->block_nr) / thd_opt->block_frame_sz;
thd_opt->tpacket_req3.tp_retire_blk_tov = 1;
thd_opt->tpacket_req3.tp_feature_req_Word = 0;
sock_mmap_ring = setsockopt(sock_fd, SOL_PACKET , PACKET_RX_RING , (void*)&thd_opt->tpacket_req3 , sizeof(thd_opt->tpacket_req3));
}
if (sock_mmap_ring == -1) {
perror("Can't enable Tx/Rx ring for socket");
return EXIT_FAILURE;
}
thd_opt->mmap_buf = NULL;
thd_opt->rd = NULL;
if (thd_opt->sk_mode == SKT_TX) {
thd_opt->mmap_buf = mmap(NULL, (thd_opt->block_sz * thd_opt->block_nr), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED | MAP_POPULATE, sock_fd, 0);
if (thd_opt->mmap_buf == MAP_FAILED) {
perror("mmap failed");
return EXIT_FAILURE;
}
} else {
thd_opt->mmap_buf = mmap(NULL, (thd_opt->block_sz * thd_opt->block_nr), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED | MAP_POPULATE, sock_fd, 0);
if (thd_opt->mmap_buf == MAP_FAILED) {
perror("mmap failed");
return EXIT_FAILURE;
}
// Per bock rings in Rx mode (TPACKET_V3)
thd_opt->rd = (struct iovec*)calloc(thd_opt->tpacket_req3.tp_block_nr * sizeof(struct iovec), 1);
for (uint16_t i = 0; i < thd_opt->tpacket_req3.tp_block_nr; ++i) {
thd_opt->rd[i].iov_base = thd_opt->mmap_buf + (i * thd_opt->tpacket_req3.tp_block_size);
thd_opt->rd[i].iov_len = thd_opt->tpacket_req3.tp_block_size;
}
}
Mise à jour 1: résultat par rapport aux interfaces physiques Il a été mentionné qu'une des raisons pour lesquelles je ne voyais pas de différence de performances lors de l'utilisation de PACKET_MMAP était que j'envoyais le trafic vers l'interface de bouclage (qui, pour une chose, n'a pas de QDISC). Étant donné que l'exécution des routines packet_tx_mmap()
ou packet_tx()
peut générer plus de 10 Gbit/s et que je n'ai que des interfaces 10 Gbit/s à ma disposition, j'en ai lié deux ensemble et ce sont les résultats, qui montrent à peu près comme ci-dessus, la différence de vitesse entre les deux fonctions est minime:
packet_tx()
à 20G bond0
packet_tx_mmap()
à 20G bond0:
C'était avec des trames de 1514 octets (pour le garder comme les tests de bouclage d'origine ci-dessus).
Dans tous les tests ci-dessus, le nombre d'IRQ souples était à peu près le même (mesuré à l'aide de ce script ). Avec un thread exécutant packet_tx()
, il y avait environ 40 000 interruptions par seconde sur un cœur de processeur. Avec 2 et 3 threads s'exécutant là 40k sur 2 et 3 noyaux respectivement. Les résultats lors de l'utilisation de packet_tx_mmap()
où les mêmes. IRQ souples d'environ 40 000 pour un seul thread sur un cœur de processeur. 40k par cœur lors de l'exécution de 2 et 3 threads.
Mise à jour 2: code source complet
J'ai téléchargé le code source complet maintenant, j'écris toujours cette application donc elle a probablement beaucoup de défauts mais ils sont en dehors du champ de cette question: https://github.com/jwbensley/EtherateMT =
De nombreuses interfaces vers le noyau Linux ne sont pas bien documentées. Ou même si elles semblent bien documentées, elles peuvent être assez complexes et cela peut rendre difficile la compréhension des propriétés fonctionnelles ou, souvent encore plus difficiles, de l'interface.
Pour cette raison, mon conseil à toute personne souhaitant une solide compréhension des API du noyau ou ayant besoin de créer des applications hautes performances à l'aide des API du noyau doit être en mesure de s'engager avec le code du noyau pour réussir.
Dans ce cas, le questionneur veut comprendre les caractéristiques de performance de l'envoi de trames brutes via une interface de mémoire partagée (packet mmap) au noyau.
La documentation Linux est ici . Il a un lien périmé vers un "comment faire", qui peut maintenant être trouvé ici et comprend une copie de packet_mmap.c
(j'ai une version légèrement différente disponible ici .
La documentation est largement orientée vers la lecture, ce qui est le cas d'utilisation typique pour utiliser mmap de paquets: lire efficacement les trames brutes d'une interface pour, par exemple obtenir efficacement un capture de paquets à partir d'une interface haut débit avec peu ou pas de perte.
L'OP s'intéresse cependant à l'écriture haute performance , qui est un cas d'utilisation beaucoup moins courant, mais potentiellement utile pour un générateur/simulateur de trafic qui semble être ce que le PO veut en faire. Heureusement, le "mode d'emploi" consiste à écrire des cadres.
Même ainsi, il y a très peu d'informations fournies sur la façon dont cela fonctionne réellement, et rien d'aide évidente pour répondre à la question des OP sur pourquoi l'utilisation du packet mmap ne semble pas être plus rapide que de ne pas l'utiliser et d'envoyer à la place une trame à la fois.
Heureusement, la source du noyau est open source et bien indexée, nous pouvons donc nous tourner vers la source pour nous aider à obtenir la réponse à la question.
Afin de trouver le code du noyau approprié, vous pouvez rechercher plusieurs mots clés, mais PACKET_TX_RING
se distingue comme une option de socket unique à cette fonctionnalité. La recherche sur les interwebs de "PACKET_TX_RING linux cross reference" fait apparaître un petit nombre de références, y compris af_packet.c
, qui, avec un peu d'inspection, semble être la mise en œuvre de toutes les fonctionnalités AF_PACKET
, y compris packet mmap.
En parcourant af_packet.c
, il semble que le cœur du travail de transmission avec packet mmap se déroule dans tpacket_snd()
. Mais est-ce exact? Comment savoir si cela a quelque chose à voir avec ce que nous pensons qu'il fait?
Un outil très puissant pour obtenir des informations comme celle-ci du noyau est SystemTap . (L'utilisation de cela nécessite l'installation de symboles de débogage pour votre noyau. Il se trouve que j'utilise Ubuntu, et this est une recette pour faire fonctionner SystemTap sur Ubuntu.)
Une fois que SystemTap fonctionne, vous pouvez utiliser SystemTap en conjonction avec packet_mmap.c
pour voir si tpacket_snd()
est même invoqué en installant une sonde sur la fonction du noyau tpacket_snd
, puis exécutant packet_mmap
pour envoyer une trame via une sonnerie TX partagée:
$ Sudo stap -e 'probe kernel.function("tpacket_snd") { printf("W00T!\n"); }' &
[1] 19961
$ Sudo ./packet_mmap -c 1 eth0
[...]
STARTING TEST:
data offset = 32 bytes
start fill() thread
send 1 packets (+150 bytes)
end of task fill()
Loop until queue empty (0)
END (number of error:0)
W00T!
W00T!
W00T! Nous sommes sur quelque chose; tpacket_snd
est actuellement appelé. Mais notre victoire sera de courte durée. Si nous continuons d'essayer d'obtenir plus d'informations sur une construction de noyau de stock, SystemTap se plaindra de ne pas trouver les variables que nous voulons inspecter et les arguments de fonction s'imprimeront avec des valeurs telles que ?
ou ERROR
. En effet, le noyau est compilé avec optimisation et toutes les fonctionnalités de AF_PACKET
sont définies dans l'unité de traduction unique af_packet.c
; la plupart des fonctions sont intégrées par le compilateur, ce qui perd effectivement les variables et arguments locaux.
Afin d'extraire plus d'informations de af_packet.c
, nous allons devoir construire une version du noyau où af_packet.c
est construit sans optimisation. Regardez ici pour quelques conseils. J'attendrai.
OK, j'espère que ce n'était pas trop difficile et que vous avez réussi à démarrer un noyau dont SystemTap peut obtenir beaucoup de bonnes informations. Gardez à l'esprit que cette version du noyau est juste pour nous aider à comprendre comment fonctionne mmap de paquets. Nous ne pouvons obtenir aucune information directe sur les performances de ce noyau car af_packet.c
a été construit sans optimisation. S'il s'avère que nous devons obtenir des informations sur le comportement de la version optimisée, nous pouvons construire un autre noyau avec af_packet.c
compilé avec optimisation, mais avec du code d'instrumentation ajouté qui expose les informations via des variables qui ne le seront pas. être optimisé afin que SystemTap puisse les voir.
Alors utilisons-le pour obtenir des informations. Jetez un œil à status.stp
:
# This is specific to net/packet/af_packet.c 3.13.0-116
function print_ts() {
ts = gettimeofday_us();
printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}
# 325 static void __packet_set_status(struct packet_sock *po, void *frame, int status)
# 326 {
# 327 union tpacket_uhdr h;
# 328
# 329 h.raw = frame;
# 330 switch (po->tp_version) {
# 331 case TPACKET_V1:
# 332 h.h1->tp_status = status;
# 333 flush_dcache_page(pgv_to_page(&h.h1->tp_status));
# 334 break;
# 335 case TPACKET_V2:
# 336 h.h2->tp_status = status;
# 337 flush_dcache_page(pgv_to_page(&h.h2->tp_status));
# 338 break;
# 339 case TPACKET_V3:
# 340 default:
# 341 WARN(1, "TPACKET version not supported.\n");
# 342 BUG();
# 343 }
# 344
# 345 smp_wmb();
# 346 }
probe kernel.statement("__packet_set_status@net/packet/af_packet.c:334") {
print_ts();
printf("SET(V1): %d (0x%.16x)\n", $status, $frame);
}
probe kernel.statement("__packet_set_status@net/packet/af_packet.c:338") {
print_ts();
printf("SET(V2): %d\n", $status);
}
# 348 static int __packet_get_status(struct packet_sock *po, void *frame)
# 349 {
# 350 union tpacket_uhdr h;
# 351
# 352 smp_rmb();
# 353
# 354 h.raw = frame;
# 355 switch (po->tp_version) {
# 356 case TPACKET_V1:
# 357 flush_dcache_page(pgv_to_page(&h.h1->tp_status));
# 358 return h.h1->tp_status;
# 359 case TPACKET_V2:
# 360 flush_dcache_page(pgv_to_page(&h.h2->tp_status));
# 361 return h.h2->tp_status;
# 362 case TPACKET_V3:
# 363 default:
# 364 WARN(1, "TPACKET version not supported.\n");
# 365 BUG();
# 366 return 0;
# 367 }
# 368 }
probe kernel.statement("__packet_get_status@net/packet/af_packet.c:358") {
print_ts();
printf("GET(V1): %d (0x%.16x)\n", $h->h1->tp_status, $frame);
}
probe kernel.statement("__packet_get_status@net/packet/af_packet.c:361") {
print_ts();
printf("GET(V2): %d\n", $h->h2->tp_status);
}
# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2136 do {
# 2137 ph = packet_current_frame(po, &po->tx_ring,
# 2138 TP_STATUS_SEND_REQUEST);
# 2139
# 2140 if (unlikely(ph == NULL)) {
# 2141 schedule();
# 2142 continue;
# 2143 }
# 2144
# 2145 status = TP_STATUS_SEND_REQUEST;
# 2146 hlen = LL_RESERVED_SPACE(dev);
# 2147 tlen = dev->needed_tailroom;
# 2148 skb = sock_alloc_send_skb(&po->sk,
# 2149 hlen + tlen + sizeof(struct sockaddr_ll),
# 2150 0, &err);
# 2151
# 2152 if (unlikely(skb == NULL))
# 2153 goto out_status;
# 2154
# 2155 tp_len = tpacket_fill_skb(po, skb, ph, dev, size_max, proto,
# 2156 addr, hlen);
# [...]
# 2176 skb->destructor = tpacket_destruct_skb;
# 2177 __packet_set_status(po, ph, TP_STATUS_SENDING);
# 2178 atomic_inc(&po->tx_ring.pending);
# 2179
# 2180 status = TP_STATUS_SEND_REQUEST;
# 2181 err = dev_queue_xmit(skb);
# 2182 if (unlikely(err > 0)) {
# [...]
# 2195 }
# 2196 packet_increment_head(&po->tx_ring);
# 2197 len_sum += tp_len;
# 2198 } while (likely((ph != NULL) ||
# 2199 ((!(msg->msg_flags & MSG_DONTWAIT)) &&
# 2200 (atomic_read(&po->tx_ring.pending))))
# 2201 );
# 2202
# [...]
# 2213 return err;
# 2214 }
probe kernel.function("tpacket_snd") {
print_ts();
printf("tpacket_snd: args(%s)\n", $$parms);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2140") {
print_ts();
printf("tpacket_snd:2140: current frame ph = 0x%.16x\n", $ph);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2141") {
print_ts();
printf("tpacket_snd:2141: (ph==NULL) --> schedule()\n");
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2142") {
print_ts();
printf("tpacket_snd:2142: flags 0x%x, pending %d\n",
$msg->msg_flags, $po->tx_ring->pending->counter);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2197") {
print_ts();
printf("tpacket_snd:2197: flags 0x%x, pending %d\n",
$msg->msg_flags, $po->tx_ring->pending->counter);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
print_ts();
printf("tpacket_snd: return(%d)\n", $err);
}
# 1946 static void tpacket_destruct_skb(struct sk_buff *skb)
# 1947 {
# 1948 struct packet_sock *po = pkt_sk(skb->sk);
# 1949 void *ph;
# 1950
# 1951 if (likely(po->tx_ring.pg_vec)) {
# 1952 __u32 ts;
# 1953
# 1954 ph = skb_shinfo(skb)->destructor_arg;
# 1955 BUG_ON(atomic_read(&po->tx_ring.pending) == 0);
# 1956 atomic_dec(&po->tx_ring.pending);
# 1957
# 1958 ts = __packet_set_timestamp(po, ph, skb);
# 1959 __packet_set_status(po, ph, TP_STATUS_AVAILABLE | ts);
# 1960 }
# 1961
# 1962 sock_wfree(skb);
# 1963 }
probe kernel.statement("tpacket_destruct_skb@net/packet/af_packet.c:1959") {
print_ts();
printf("tpacket_destruct_skb:1959: ph = 0x%.16x, ts = 0x%x, pending %d\n",
$ph, $ts, $po->tx_ring->pending->counter);
}
Cela définit une fonction (print_ts
pour imprimer le temps Epix unix avec une résolution en microsecondes) et un certain nombre de sondes.
Nous définissons d'abord les sondes pour imprimer des informations lorsque les paquets dans le tx_ring ont leur statut défini ou lu. Ensuite, nous définissons des sondes pour l'appel et le retour de tpacket_snd
et aux points de la boucle do {...} while (...)
traitant les paquets dans le tx_ring. Enfin, nous ajoutons une sonde au destructeur skb.
Nous pouvons démarrer le script SystemTap avec Sudo stap status.stp
. Exécutez ensuite Sudo packet_mmap -c 2 <interface>
pour envoyer 2 images via l'interface. Voici la sortie que j'ai obtenue du script SystemTap:
[1492581245.839850] tpacket_snd: args(po=0xffff88016720ee38 msg=0x14)
[1492581245.839865] GET(V1): 1 (0xffff880241202000)
[1492581245.839873] tpacket_snd:2140: current frame ph = 0xffff880241202000
[1492581245.839887] SET(V1): 2 (0xffff880241202000)
[1492581245.839918] tpacket_snd:2197: flags 0x40, pending 1
[1492581245.839923] GET(V1): 1 (0xffff88013499c000)
[1492581245.839929] tpacket_snd:2140: current frame ph = 0xffff88013499c000
[1492581245.839935] SET(V1): 2 (0xffff88013499c000)
[1492581245.839946] tpacket_snd:2197: flags 0x40, pending 2
[1492581245.839951] GET(V1): 0 (0xffff88013499e000)
[1492581245.839957] tpacket_snd:2140: current frame ph = 0x0000000000000000
[1492581245.839961] tpacket_snd:2141: (ph==NULL) --> schedule()
[1492581245.839977] tpacket_snd:2142: flags 0x40, pending 2
[1492581245.839984] tpacket_snd: return(300)
[1492581245.840077] tpacket_snd: args(po=0x0 msg=0x14)
[1492581245.840089] GET(V1): 0 (0xffff88013499e000)
[1492581245.840098] tpacket_snd:2140: current frame ph = 0x0000000000000000
[1492581245.840093] tpacket_destruct_skb:1959: ph = 0xffff880241202000, ts = 0x0, pending 1
[1492581245.840102] tpacket_snd:2141: (ph==NULL) --> schedule()
[1492581245.840104] SET(V1): 0 (0xffff880241202000)
[1492581245.840112] tpacket_snd:2142: flags 0x40, pending 1
[1492581245.840116] tpacket_destruct_skb:1959: ph = 0xffff88013499c000, ts = 0x0, pending 0
[1492581245.840119] tpacket_snd: return(0)
[1492581245.840123] SET(V1): 0 (0xffff88013499c000)
Et voici la capture réseau:
Il y a beaucoup d'informations utiles dans la sortie SystemTap. Nous pouvons voir tpacket_snd
obtenir le statut de la première image de l'anneau ( TP_STATUS_SEND_REQUEST
est 1), puis la définir sur TP_STATUS_SENDING
(2). Il en va de même avec le second. La trame suivante a le statut TP_STATUS_AVAILABLE
(0), qui n'est pas une demande d'envoi, elle appelle donc schedule()
pour céder et continue la boucle. Puisqu'il n'y a plus de trames à envoyer (ph==NULL
) et qu'un non-blocage a été demandé (msg->msg_flags ==
MSG_DONTWAIT
) le do {...} while (...)
se termine et tpacket_snd
renvoie 300
, le nombre d'octets mis en file d'attente pour la transmission.
Ensuite, packet_mmap
appelle à nouveau sendto
(via le code "boucle jusqu'à ce que la file d'attente soit vide"), mais il n'y a plus de données à envoyer dans l'anneau tx, et le non-blocage est demandé, donc il renvoie immédiatement 0, car aucune donnée n'a été mise en file d'attente. Notez que la trame dont il a vérifié l'état est la même trame qu'il a vérifiée en dernier lors de l'appel précédent --- il n'a pas commencé avec la première trame dans l'anneau tx, il a vérifié la head
(qui n'est pas disponible dans l'espace utilisateur).
De manière asynchrone, le destructeur est appelé, d'abord sur la première trame, définissant l'état de la trame sur TP_STATUS_AVAILABLE
et décrémentant le nombre en attente, puis sur la deuxième trame. Notez que si le non-blocage n'a pas été demandé, le test à la fin de la boucle do {...} while (...)
attendra que tous les paquets en attente aient été transférés vers le NIC (en supposant qu'il prend en charge les données dispersées) avant de retour. Vous pouvez regarder cela en exécutant packet_mmap
avec l'option -t
pour "threaded" qui utilise le blocage des E/S (jusqu'à ce qu'il arrive à "boucler jusqu'à ce que la file d'attente soit vide").
Quelques choses à noter. Tout d'abord, les horodatages sur la sortie SystemTap n'augmentent pas: il n'est pas sûr de déduire l'ordre temporel à partir de la sortie SystemTap. Deuxièmement, notez que les horodatages sur la capture réseau (effectuée localement) sont différents. FWIW, l'interface est un 1G bon marché dans un ordinateur tour bon marché.
Donc, à ce stade, je pense que nous savons plus ou moins comment af_packet
traite l'anneau tx partagé. La prochaine étape est de savoir comment les trames de l'anneau tx parviennent à l'interface réseau. Il pourrait être utile de revoir cette section (sur la façon dont la transmission de la couche 2 est gérée) d'un aperç du flux de contrôle dans le noyau de mise en réseau Linux.
OK, donc si vous avez une compréhension de base de la façon dont la transmission de la couche 2 est gérée, il semblerait que cette interface mmap de paquets devrait être un énorme tuyau d'incendie; charger un anneau tx partagé avec des paquets, appeler sendto()
avec MSG_DONTWAIT
, puis tpacket_snd
itérera dans la file d'attente tx en créant des skb et en les mettant en file d'attente sur le qdisc. Asychroniquement, les skb seront retirés de la file d'attente du qdisc et envoyés à l'anneau tx matériel. Les skb doivent être non linéaires afin qu'ils référencent les données dans l'anneau tx plutôt que la copie, et un Nice NIC moderne devrait être capable de gérer des données dispersées et de référencer les données dans le tx sonne aussi. Bien sûr, l'une de ces hypothèses pourrait être erronée, alors essayons de vider beaucoup de mal sur un disque avec ce tuyau d'incendie.
Mais d'abord, un fait peu commun sur le fonctionnement des qdisc. Ils contiennent une quantité limitée de données (généralement comptées en nombre de trames, mais dans certains cas, elles peuvent être mesurées en octets) et si vous essayez de mettre en file d'attente une trame dans un qdisc complet, la trame sera généralement supprimée (en fonction de ce que le enqueuer décide de faire). Je vais donc donner l'allusion que mon hypothèse d'origine était que l'OP utilisait le packet mmap pour faire sauter des trames dans un qdisc si rapidement que beaucoup étaient abandonnés. Mais ne vous en tenez pas trop à cette idée; cela vous emmène dans une direction, mais gardez toujours un esprit ouvert. Essayons de découvrir ce qui se passe.
Le premier problème en essayant ceci est que le qdisc par défaut pfifo_fast
ne conserve pas les statistiques. Remplaçons donc cela par le qdisc pfifo
qui le fait. Par défaut, pfifo
limite la file d'attente à TXQUEUELEN
trames (qui par défaut est généralement 1000). Mais comme nous voulons démontrer un qdisc écrasant, définissons-le explicitement à 50:
$ Sudo tc qdisc add dev eth0 root pfifo limit 50
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8004: root refcnt 2 limit 50p
Sent 42 bytes 1 pkt (dropped 0, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
Mesurons également le temps nécessaire pour traiter les trames dans tpacket_snd
avec le script SystemTap call-return.stp
:
# This is specific to net/packet/af_packet.c 3.13.0-116
function print_ts() {
ts = gettimeofday_us();
printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}
# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2213 return err;
# 2214 }
probe kernel.function("tpacket_snd") {
print_ts();
printf("tpacket_snd: args(%s)\n", $$parms);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
print_ts();
printf("tpacket_snd: return(%d)\n", $err);
}
Démarrez le script SystemTap avec Sudo stap call-return.stp
, puis injectons 8096 trames de 1500 octets dans ce qdisc avec une maigre capacité de 50 trames:
$ Sudo ./packet_mmap -c 8096 -s 1500 eth0
[...]
STARTING TEST:
data offset = 32 bytes
start fill() thread
send 8096 packets (+12144000 bytes)
end of task fill()
Loop until queue empty (0)
END (number of error:0)
Voyons donc combien de paquets ont été abandonnés par le qdisc:
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8004: root refcnt 2 limit 50p
Sent 25755333 bytes 8606 pkt (dropped 1, overlimits 0 requeues 265)
backlog 0b 0p requeues 265
WAT ? Vous avez supprimé une des 8096 images sur un qdisc de 50 images? Vérifions la sortie SystemTap:
[1492603552.938414] tpacket_snd: args(po=0xffff8801673ba338 msg=0x14)
[1492603553.036601] tpacket_snd: return(12144000)
[1492603553.036706] tpacket_snd: args(po=0x0 msg=0x14)
[1492603553.036716] tpacket_snd: return(0)
WAT ? Il a fallu près de 100 ms pour traiter 8096 images en tpacket_snd
? Vérifions combien de temps cela prendrait réellement pour transmettre; c'est 8096 images à 1500 octets/image à 1 gigabit/s ~ = 97 ms. WAT ? Ça sent que quelque chose bloque.
Examinons de plus près tpacket_snd
. Gémissement:
skb = sock_alloc_send_skb(&po->sk,
hlen + tlen + sizeof(struct sockaddr_ll),
0, &err);
Ce 0
semble assez inoffensif, mais c'est en fait l'argument noblock. Il devrait être msg->msg_flags & MSG_DONTWAIT
(il s'avère que c'est corrigé dans 4.1 ). Ce qui se passe ici, c'est que la taille du qdisc n'est pas la seule ressource limitante. Si l'allocation d'espace pour le skb dépasse la taille de la limite sndbuf du socket, alors cet appel bloquera pour attendre que les skb soient libérés ou renverra -EAGAIN
à un appelant non bloquant. Dans le correctif de la V4.1, si le non-blocage est demandé, il retournera le nombre d'octets écrits s'il est différent de zéro, sinon -EAGAIN
à l'appelant, ce qui semble presque que quelqu'un ne veut pas que vous le fassiez comprendre comment utiliser cela ( par exemple vous remplissez un anneau tx avec 80 Mo de données, appelez sendto avec MSG_DONTWAIT
, et vous récupérer un résultat que vous avez envoyé 150 Ko plutôt que EWOULDBLOCK
).
Donc, si vous exécutez un noyau avant 4.1 (je pense que l'OP fonctionne> 4.1 et n'est pas affecté par ce bogue), vous devrez patcher af_packet.c
et construire un nouveau noyau ou mettre à niveau vers un noyau 4.1 ou mieux.
J'ai maintenant démarré une version corrigée de mon noyau, car la machine que j'utilise exécute 3.13. Bien que nous ne bloquions pas si le sndbuf est plein, nous reviendrons toujours avec -EAGAIN
. J'ai apporté quelques modifications à packet_mmap.c
pour augmenter la taille par défaut de sndbuf et utiliser SO_SNDBUFFORCE
pour remplacer le système max par socket si nécessaire (il semble avoir besoin d'environ 750 octets + le cadre taille pour chaque cadre). J'ai également fait quelques ajouts à call-return.stp
pour enregistrer la taille maximale de sndbuf (sk_sndbuf
), le montant utilisé (sk_wmem_alloc
), toute erreur renvoyée par sock_alloc_send_skb
et toute erreur renvoyée par dev_queue_xmit
lors de la mise en file d'attente du skb vers le qdisc. Voici la nouvelle version:
# This is specific to net/packet/af_packet.c 3.13.0-116
function print_ts() {
ts = gettimeofday_us();
printf("[%10d.%06d] ", ts/1000000, ts%1000000);
}
# 2088 static int tpacket_snd(struct packet_sock *po, struct msghdr *msg)
# 2089 {
# [...]
# 2133 if (size_max > dev->mtu + reserve + VLAN_HLEN)
# 2134 size_max = dev->mtu + reserve + VLAN_HLEN;
# 2135
# 2136 do {
# [...]
# 2148 skb = sock_alloc_send_skb(&po->sk,
# 2149 hlen + tlen + sizeof(struct sockaddr_ll),
# 2150 msg->msg_flags & MSG_DONTWAIT, &err);
# 2151
# 2152 if (unlikely(skb == NULL))
# 2153 goto out_status;
# [...]
# 2181 err = dev_queue_xmit(skb);
# 2182 if (unlikely(err > 0)) {
# 2183 err = net_xmit_errno(err);
# 2184 if (err && __packet_get_status(po, ph) ==
# 2185 TP_STATUS_AVAILABLE) {
# 2186 /* skb was destructed already */
# 2187 skb = NULL;
# 2188 goto out_status;
# 2189 }
# 2190 /*
# 2191 * skb was dropped but not destructed yet;
# 2192 * let's treat it like congestion or err < 0
# 2193 */
# 2194 err = 0;
# 2195 }
# 2196 packet_increment_head(&po->tx_ring);
# 2197 len_sum += tp_len;
# 2198 } while (likely((ph != NULL) ||
# 2199 ((!(msg->msg_flags & MSG_DONTWAIT)) &&
# 2200 (atomic_read(&po->tx_ring.pending))))
# 2201 );
# [...]
# 2213 return err;
# 2214 }
probe kernel.function("tpacket_snd") {
print_ts();
printf("tpacket_snd: args(%s)\n", $$parms);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2133") {
print_ts();
printf("tpacket_snd:2133: sk_sndbuf = %d sk_wmem_alloc = %d\n",
$po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2153") {
print_ts();
printf("tpacket_snd:2153: sock_alloc_send_skb err = %d, sk_sndbuf = %d sk_wmem_alloc = %d\n",
$err, $po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2182") {
if ($err != 0) {
print_ts();
printf("tpacket_snd:2182: dev_queue_xmit err = %d\n", $err);
}
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2187") {
print_ts();
printf("tpacket_snd:2187: destructed: net_xmit_errno = %d\n", $err);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2194") {
print_ts();
printf("tpacket_snd:2194: *NOT* destructed: net_xmit_errno = %d\n", $err);
}
probe kernel.statement("tpacket_snd@net/packet/af_packet.c:2213") {
print_ts();
printf("tpacket_snd: return(%d) sk_sndbuf = %d sk_wmem_alloc = %d\n",
$err, $po->sk->sk_sndbuf, $po->sk->sk_wmem_alloc->counter);
}
Essayons encore:
$ Sudo tc qdisc add dev eth0 root pfifo limit 50
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8001: root refcnt 2 limit 50p
Sent 2154 bytes 21 pkt (dropped 0, overlimits 0 requeues 0)
backlog 0b 0p requeues 0
$ Sudo ./packet_mmap -c 200 -s 1500 eth0
[...]
c_sndbuf_sz: 1228800
[...]
STARTING TEST:
data offset = 32 bytes
send buff size = 1228800
got buff size = 425984
buff size smaller than desired, trying to force...
got buff size = 2457600
start fill() thread
send: No buffer space available
end of task fill()
send: No buffer space available
Loop until queue empty (-1)
[repeated another 17 times]
send 3 packets (+4500 bytes)
Loop until queue empty (4500)
Loop until queue empty (0)
END (number of error:0)
$ tc -s -d qdisc show dev eth0
qdisc pfifo 8001: root refcnt 2 limit 50p
Sent 452850 bytes 335 pkt (dropped 19, overlimits 0 requeues 3)
backlog 0b 0p requeues 3
Et voici la sortie SystemTap:
[1492759330.907151] tpacket_snd: args(po=0xffff880393246c38 msg=0x14)
[1492759330.907162] tpacket_snd:2133: sk_sndbuf = 2457600 sk_wmem_alloc = 1
[1492759330.907491] tpacket_snd:2182: dev_queue_xmit err = 1
[1492759330.907494] tpacket_snd:2187: destructed: net_xmit_errno = -105
[1492759330.907500] tpacket_snd: return(-105) sk_sndbuf = 2457600 sk_wmem_alloc = 218639
[1492759330.907646] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.907653] tpacket_snd:2133: sk_sndbuf = 2457600 sk_wmem_alloc = 189337
[1492759330.907688] tpacket_snd:2182: dev_queue_xmit err = 1
[1492759330.907691] tpacket_snd:2187: destructed: net_xmit_errno = -105
[1492759330.907694] tpacket_snd: return(-105) sk_sndbuf = 2457600 sk_wmem_alloc = 189337
[repeated 17 times]
[1492759330.908541] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.908543] tpacket_snd:2133: sk_sndbuf = 2457600 sk_wmem_alloc = 189337
[1492759330.908554] tpacket_snd: return(4500) sk_sndbuf = 2457600 sk_wmem_alloc = 196099
[1492759330.908570] tpacket_snd: args(po=0x0 msg=0x14)
[1492759330.908572] tpacket_snd:2133: sk_sndbuf = 2457600 sk_wmem_alloc = 196099
[1492759330.908576] tpacket_snd: return(0) sk_sndbuf = 2457600 sk_wmem_alloc = 196099
Maintenant, les choses fonctionnent comme prévu; nous avons corrigé un bogue nous obligeant à bloquer la limite de sndbuf et nous avons ajusté la limite de sndbuf afin qu'elle ne soit pas une contrainte, et maintenant nous voyons que les trames de l'anneau tx sont mises en file d'attente sur le qdisc jusqu'à ce qu'il soit plein , auquel point nous obtenons retourné ENOBUFS
.
Le problème suivant est maintenant de savoir comment continuer à publier efficacement sur le qdisc pour garder l'interface occupée. Notez que l'implémentation de packet_poll
est inutile dans le cas où nous remplissons le qdisc et récupérons ENOBUFS
, car il demande simplement si la tête est TP_STATUS_AVAILABLE
, qui dans ce cas restera TP_STATUS_SEND_REQUEST
jusqu'à ce qu'un appel ultérieur à sendto
réussisse à mettre la trame en file d'attente sur le qdisc. Une simple opportunité (mise à jour dans packet_mmap.c) est de boucler sur le sendto jusqu'à succès ou une erreur autre que ENOBUFS
ou EAGAIN
.
Quoi qu'il en soit, nous en savons beaucoup plus que suffisant pour répondre à la question des PO maintenant, même si nous n'avons pas de solution complète pour empêcher efficacement le NIC d'être affamé.
D'après ce que nous avons appris, nous savons que lorsque OP appelle sendto avec une sonnerie tx en mode blocage, tpacket_snd
commencera la mise en file d'attente des skbs sur le qdisc jusqu'à ce que la limite sndbuf soit dépassée (et la valeur par défaut est généralement assez petite, environ 213K, et en outre, j'ai découvert que les données de trame référencées dans l'anneau tx partagé sont comptées pour cela) quand elles se bloqueront (tout en maintenant pg_vec_lock
). Au fur et à mesure que skb est libéré, plus de trames seront mises en file d'attente, et peut-être que le sndbuf sera à nouveau dépassé et nous bloquerons à nouveau. Finalement, toutes les données auront été mises en file d'attente sur le qdisc mais tpacket_snd
continuera à bloquer jusqu'à ce que toutes les trames aient été transmises (vous ne pouvez pas marquer une image dans l'anneau tx comme disponible tant que NIC ne l'a pas reçue, car un skb dans l'anneau pilote fait référence à une image dans l'anneau tx) tout en maintenant pg_vec_lock
. À ce stade, le NIC est affamé et tous les autres écrivains de socket ont été bloqués par le verrou.
D'un autre côté, lorsque OP publie un paquet à la fois, il sera géré par packet_snd
qui bloquera s'il n'y a pas de place dans le sndbuf, puis mettra la trame en file d'attente sur le qdisc et reviendra immédiatement. Il n'attend pas la transmission de la trame. Pendant que le qdisc est vidé, des trames supplémentaires peuvent être mises en file d'attente. Si l'éditeur peut suivre, le NIC ne sera jamais affamé.
De plus, l'op copie dans l'anneau tx pour chaque appel sendto et le compare au passage d'un tampon de trame fixe lorsqu'il n'utilise pas un anneau tx. Vous ne verrez pas d'accélération de ne pas copier de cette façon (bien que ce ne soit pas le seul avantage de l'utilisation de l'anneau tx).