Envoi TCP d'espace utilisateur à copie nulle de la mémoire mappée dma_mmap_coherent ()

14

J'utilise Linux 5.1 sur un SoC Cyclone V, qui est un FPGA avec deux cœurs ARMv7 dans une puce. Mon objectif est de rassembler de nombreuses données à partir d'une interface externe et de diffuser (une partie de) ces données via une socket TCP. Le défi ici est que le débit de données est très élevé et pourrait être proche de saturer l'interface GbE. J'ai une implémentation de travail qui utilise uniquement des write()appels à la socket, mais elle dépasse 55 Mo / s; environ la moitié de la limite théorique de GbE. J'essaie maintenant d'obtenir une transmission TCP sans copie pour augmenter le débit, mais je frappe un mur.

Pour extraire les données du FPGA dans l'espace utilisateur Linux, j'ai écrit un pilote de noyau. Ce pilote utilise un bloc DMA dans le FPGA pour copier une grande quantité de données d'une interface externe dans la mémoire DDR3 attachée aux cœurs ARMv7. Le pilote alloue cette mémoire en tant que groupe de tampons contigus de 1 Mo lorsqu'il est sondé à l'aide dma_alloc_coherent()de GFP_USERet les expose à l'application de l'espace utilisateur en les implémentant mmap()dans un fichier /dev/et en renvoyant une adresse à l'application en utilisant dma_mmap_coherent()les tampons préalloués.

Jusqu'ici tout va bien; l'application de l'espace utilisateur voit des données valides et le débit est plus que suffisant à> 360 Mo / s avec de la place pour épargner (l'interface externe n'est pas assez rapide pour vraiment voir quelle est la limite supérieure).

Pour implémenter un réseau TCP sans copie, ma première approche a été d'utiliser SO_ZEROCOPYsur le socket:

sent_bytes = send(fd, buf, len, MSG_ZEROCOPY);
if (sent_bytes < 0) {
    perror("send");
    return -1;
}

Cependant, cela se traduit par send: Bad address.

Après avoir googlé un peu, ma deuxième approche a été d'utiliser un tuyau et a splice()suivi par vmsplice():

ssize_t sent_bytes;
int pipes[2];
struct iovec iov = {
    .iov_base = buf,
    .iov_len = len
};

pipe(pipes);

sent_bytes = vmsplice(pipes[1], &iov, 1, 0);
if (sent_bytes < 0) {
    perror("vmsplice");
    return -1;
}
sent_bytes = splice(pipes[0], 0, fd, 0, sent_bytes, SPLICE_F_MOVE);
if (sent_bytes < 0) {
    perror("splice");
    return -1;
}

Cependant, le résultat est le même: vmsplice: Bad address.

Notez que si je remplace l'appel à vmsplice()ou send()à une fonction qui imprime simplement les données pointées par buf(ou un send() sans MSG_ZEROCOPY ), tout fonctionne très bien; les données sont donc accessibles à l'espace utilisateur, mais les appels vmsplice()/ send(..., MSG_ZEROCOPY)semblent incapables de les gérer.

Qu'est-ce que j'oublie ici? Existe-t-il un moyen d'utiliser l'envoi TCP sans copie avec une adresse de l'espace utilisateur obtenue à partir d'un pilote de noyau via dma_mmap_coherent()? Y a-t-il une autre approche que je pourrais utiliser?

MISE À JOUR

J'ai donc plongé un peu plus profondément dans le sendmsg() MSG_ZEROCOPYchemin dans le noyau, et l'appel qui échoue finalement l'est get_user_pages_fast(). Cet appel renvoie -EFAULTcar check_vma_flags()trouve l' VM_PFNMAPindicateur défini dans le vma. Cet indicateur est apparemment défini lorsque les pages sont mappées dans l'espace utilisateur à l'aide de remap_pfn_range()ou dma_mmap_coherent(). Ma prochaine approche consiste à trouver un autre moyen d'accéder à mmapces pages.

rem
la source

Réponses:

8

Comme je l'ai publié dans une mise à jour dans ma question, le problème sous-jacent est que le réseau zerocopy ne fonctionne pas pour la mémoire qui a été mappée à l'aide remap_pfn_range()(qui dma_mmap_coherent()se trouve également être utilisée sous le capot). La raison en est que ce type de mémoire (avec l' VM_PFNMAPindicateur défini) n'a pas de métadonnées sous forme d' struct page*associé à chaque page, dont il a besoin.

La solution consiste alors à allouer la mémoire de manière à ce que les struct page*s soient associés à la mémoire.

Le workflow qui fonctionne maintenant pour moi pour allouer la mémoire est le suivant:

  1. Utilisez struct page* page = alloc_pages(GFP_USER, page_order);pour allouer un bloc de mémoire physique contiguë, où le nombre de pages contiguës qui seront allouées est donné par 2**page_order.
  2. Divisez la page de haut niveau / composée en pages de 0 ordre en appelant split_page(page, page_order);. Cela signifie maintenant que c'est struct page* pagedevenu un tableau avec des 2**page_orderentrées.

Maintenant, pour soumettre une telle région au DMA (pour la réception des données):

  1. dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
  2. dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
  3. dmaengine_submit(dma_desc);

Lorsque nous recevons un rappel du DMA indiquant que le transfert est terminé, nous devons annuler le mappage de la région pour transférer la propriété de ce bloc de mémoire au CPU, qui prend soin des caches pour s'assurer que nous ne lisons pas les données périmées:

  1. dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);

Maintenant, lorsque nous voulons implémenter mmap(), tout ce que nous avons vraiment à faire est d'appeler à vm_insert_page()plusieurs reprises pour toutes les pages d'ordre 0 que nous avons pré-allouées:

static int my_mmap(struct file *file, struct vm_area_struct *vma) {
    int res;
...
    for (i = 0; i < 2**page_order; ++i) {
        if ((res = vm_insert_page(vma, vma->vm_start + i*PAGE_SIZE, &page[i])) < 0) {
            break;
        }
    }
    vma->vm_flags |= VM_LOCKED | VM_DONTCOPY | VM_DONTEXPAND | VM_DENYWRITE;
...
    return res;
}

Lorsque le dossier est fermé, n'oubliez pas de libérer les pages:

for (i = 0; i < 2**page_order; ++i) {
    __free_page(&dev->shm[i].pages[i]);
}

L'implémentation de mmap()cette façon permet désormais à un socket d'utiliser ce tampon sendmsg()avec l' MSG_ZEROCOPYindicateur.

Bien que cela fonctionne, il y a deux choses qui ne me conviennent pas avec cette approche:

  • Vous pouvez uniquement allouer des tampons de taille 2 avec cette méthode, bien que vous puissiez implémenter la logique pour appeler alloc_pagesautant de fois que nécessaire avec des ordres décroissants pour obtenir n'importe quel tampon de taille composé de sous-tampons de tailles différentes. Cela nécessitera alors une certaine logique pour lier ces tampons ensemble dans le mmap()et pour les DMA avec des sgappels scatter-regroupe ( ) plutôt que single.
  • split_page() dit dans sa documentation:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Ces problèmes seraient facilement résolus s'il y avait une interface dans le noyau pour allouer une quantité arbitraire de pages physiques contiguës. Je ne sais pas pourquoi il n'y en a pas, mais je ne trouve pas les problèmes ci-dessus si importants que d'aller chercher pourquoi cela n'est pas disponible / comment le mettre en œuvre :-)

rem
la source
2

Peut-être que cela vous aidera à comprendre pourquoi alloc_pages nécessite un numéro de page avec puissance de 2.

Pour optimiser le processus d'allocation de pages (et réduire les fragmentations externes), qui est fréquemment engagé, le noyau Linux a développé un cache de pages par processeur et un copain-allocateur pour allouer de la mémoire (il existe un autre allocateur, slab, pour servir des allocations de mémoire plus petites qu'un page).

Le cache de pages par processeur traite la demande d'allocation d'une page, tandis que l'allocateur de contacts conserve 11 listes, chacune contenant respectivement 2 ^ {0-10} pages physiques. Ces listes fonctionnent bien lors de l'allocation et de la libération de pages, et bien sûr, le principe est que vous demandez un tampon de taille 2.

medivh
la source