web-dev-qa-db-fra.com

L'envoi de données avec PACKET_MMAP et PACKET_TX_RING est plus lent que "normal" (sans)

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

  • 1 thread: 10,77 Gbps en moyenne ~/889kfps ~
  • 2 threads: moyenne 19,19 Gbps ~/1,58 Mfps ~
  • 3 threads: moyenne 19,67 Gbps ~/1,62 Mfps ~ (c'est aussi rapide que le lien ira)

packet_tx_mmap() à 20G bond0:

  • 1 thread: Moyenne 11,08 Gbps ~/913kfps ~
  • 2 threads: Moyenne 19,0 Gbps ~/1,57 Mfps ~
  • 3 threads: Moyenne 19,66 Gbps ~/1,62 Mfps ~ (c'est aussi rapide que le lien ira)

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 =

23
jwbensley

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:

network capture of first run of packet_mmap

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).

29
Jim D.