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 beaucoup de 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 se rapprocher à saturer l'interface GbE. J'ai une implémentation fonctionnelle qui utilise uniquement des appels write()
à la socket, mais elle dépasse 55 Mo/s; environ la moitié de la limite GbE théorique. J'essaie maintenant d'obtenir zéro -copy TCP pour travailler 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 de dma_alloc_coherent()
avec GFP_USER
, et les expose à l'application de l'espace utilisateur en implémentant mmap()
sur un fichier dans /dev/
et en renvoyant une adresse à l'application en utilisant dma_mmap_coherent()
sur 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 zero-copy TCP networking, ma première approche a été d'utiliser SO_ZEROCOPY
sur 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 tube et splice()
suivi de 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 zero-copy TCP envoi 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 chemin sendmsg()
MSG_ZEROCOPY
dans le noyau, et l'appel qui échoue finalement est get_user_pages_fast()
. Cet appel renvoie -EFAULT
car check_vma_flags()
trouve l'indicateur VM_PFNMAP
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 de mmap
ces pages.
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 de remap_pfn_range()
(que dma_mmap_coherent()
arrive à utiliser sous le capot comme bien). La raison en est que ce type de mémoire (avec le drapeau VM_PFNMAP
Défini) n'a pas de métadonnées sous la forme struct page*
Associées à chaque page dont il a besoin.
La solution consiste alors à allouer la mémoire de manière à ce que struct page*
S sont associés à la mémoire.
Le flux de travail qui fonctionne maintenant pour moi pour allouer la mémoire est le suivant:
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
.split_page(page, page_order);
. Cela signifie maintenant que struct page* page
Est devenu un tableau avec 2**page_order
Entrées.Maintenant, pour soumettre une telle région au DMA (pour la réception des données):
dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
dmaengine_submit(dma_desc);
Lorsque nous recevons un rappel du DMA 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 nous ne lisons pas les données périmées:
dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);
Maintenant, quand 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()
de cette façon permet désormais à un socket d'utiliser ce tampon pour sendmsg()
avec l'indicateur MSG_ZEROCOPY
.
Bien que cela fonctionne, il y a deux choses qui ne me conviennent pas avec cette approche:
alloc_pages
Autant de fois que nécessaire avec des ordres décroissants pour obtenir n'importe quel tampon de taille composé de sous-tampons de différentes tailles. Cela nécessitera alors une certaine logique pour lier ces tampons ensemble dans les mmap()
et à DMA les avec des appels scatter-regrouper (sg
) 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 :-)
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 unité centrale traite la demande d'allocation d'une page, tandis que l'allocateur-copain 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.