Zero-Copy-User-Space-TCP-Send des zugeordneten Speichers von dma_mmap_coherent ()

14

Ich verwende Linux 5.1 auf einem Cyclone V SoC, einem FPGA mit zwei ARMv7-Kernen in einem Chip. Mein Ziel ist es, viele Daten von einer externen Schnittstelle zu sammeln und diese Daten (einen Teil davon) über einen TCP-Socket zu streamen. Die Herausforderung hierbei ist, dass die Datenrate sehr hoch ist und die GbE-Schnittstelle nahezu gesättigt sein könnte. Ich habe eine funktionierende Implementierung, die nur write()Aufrufe an den Socket verwendet, aber 55 MB / s erreicht. ungefähr die Hälfte der theoretischen GbE-Grenze. Ich versuche jetzt, die TCP-Übertragung ohne Kopie zum Laufen zu bringen, um den Durchsatz zu erhöhen, aber ich stoße an eine Wand.

Um die Daten aus dem FPGA in den Linux-User-Space zu bringen, habe ich einen Kernel-Treiber geschrieben. Dieser Treiber verwendet einen DMA-Block im FPGA, um eine große Datenmenge von einer externen Schnittstelle in den DDR3-Speicher zu kopieren, der an die ARMv7-Kerne angeschlossen ist. Der Treiber weist diesen Speicher als eine Reihe zusammenhängender 1-MB-Puffer zu, wenn er dma_alloc_coherent()mit verwendet wird GFP_USER, und stellt diese der Userspace-Anwendung zur Verfügung, indem er mmap()eine Datei in implementiert /dev/und eine Adresse an die Anwendung dma_mmap_coherent()zurückgibt , die die vorab zugewiesenen Puffer verwendet.

So weit, ist es gut; Die User-Space-Anwendung sieht gültige Daten und der Durchsatz ist mehr als ausreichend bei> 360 MB / s mit genügend Platz (die externe Schnittstelle ist nicht schnell genug, um wirklich zu erkennen, wie hoch die Obergrenze ist).

Um ein TCP-Netzwerk ohne Kopie zu implementieren, war mein erster Ansatz die Verwendung SO_ZEROCOPYauf dem Socket:

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

Dies führt jedoch zu send: Bad address.

Nachdem ich ein bisschen gegoogelt hatte, bestand mein zweiter Ansatz darin, eine Pfeife zu verwenden, splice()gefolgt von 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;
}

Das Ergebnis ist jedoch dasselbe : vmsplice: Bad address.

Beachten Sie, dass alles einwandfrei funktioniert , wenn ich den Aufruf einer Funktion vmsplice()oder send()einer Funktion ersetze, die nur die Daten druckt, auf die buf(oder eine send() ohne MSG_ZEROCOPY ) zeigt. Daher sind die Daten für den Benutzerbereich zugänglich, aber die vmsplice()/ send(..., MSG_ZEROCOPY)-Aufrufe scheinen nicht in der Lage zu sein, damit umzugehen.

Was fehlt mir hier? Gibt es eine Möglichkeit, TCP-Sendungen ohne Kopie mit einer Benutzerbereichsadresse zu verwenden, die von einem Kerneltreiber über erhalten wurde dma_mmap_coherent()? Gibt es einen anderen Ansatz, den ich verwenden könnte?

AKTUALISIEREN

Also bin ich etwas tiefer in den sendmsg() MSG_ZEROCOPYPfad im Kernel eingetaucht, und der Aufruf, der schließlich fehlschlägt, ist get_user_pages_fast(). Dieser Aufruf wird zurückgegeben, -EFAULTda check_vma_flags()das VM_PFNMAPin der gesetzte Flag gefunden wird vma. Dieses Flag wird anscheinend gesetzt, wenn die Seiten mit remap_pfn_range()oder in den Benutzerbereich abgebildet werden dma_mmap_coherent(). Mein nächster Ansatz ist es, einen anderen Weg zu mmapdiesen Seiten zu finden.

rem
quelle

Antworten:

8

Wie ich in einem Update in meiner Frage gepostet habe, besteht das zugrunde liegende Problem darin, dass das Zerocopy-Netzwerk nicht für Speicher funktioniert, der mithilfe von remap_pfn_range()(der dma_mmap_coherent()auch unter der Haube verwendet wird) zugeordnet wurde. Der Grund dafür ist, dass dieser Speichertyp (mit VM_PFNMAPgesetztem Flag) keine Metadaten in Form von struct page*zu jeder Seite zugeordnet hat, die er benötigt.

Die Lösung wird dann den Speicher in einer Weise zu verteilen , dass struct page*s sind mit dem Speicher verbunden.

Der Workflow, der jetzt für mich funktioniert, um den Speicher zuzuweisen, ist:

  1. Verwenden Sie struct page* page = alloc_pages(GFP_USER, page_order);diese Option, um einen Block zusammenhängenden physischen Speichers zuzuweisen, wobei die Anzahl der zusammenhängenden Seiten, die zugewiesen werden, durch angegeben wird 2**page_order.
  2. Teilen Sie die Seite höherer Ordnung / zusammengesetzte Seite durch Aufrufen in Seiten 0 Ordnung auf split_page(page, page_order);. Dies bedeutet nun, dass dies struct page* pageein Array mit 2**page_orderEinträgen geworden ist.

Um nun eine solche Region beim DMA einzureichen (zum Datenempfang):

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

Wenn wir vom DMA einen Rückruf erhalten, dass die Übertragung abgeschlossen ist, müssen wir die Zuordnung der Region aufheben, um den Besitz dieses Speicherblocks zurück an die CPU zu übertragen, die sich um die Caches kümmert, um sicherzustellen, dass keine veralteten Daten gelesen werden:

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

Wenn wir jetzt implementieren möchten mmap(), müssen wir nur noch vm_insert_page()wiederholt alle Seiten 0-Ordnung aufrufen, die wir vorab zugewiesen haben:

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;
}

Vergessen Sie beim Schließen der Datei nicht, die Seiten freizugeben:

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

Durch die Implementierung auf mmap()diese Weise kann ein Socket diesen Puffer jetzt sendmsg()mit dem MSG_ZEROCOPYFlag verwenden.

Obwohl dies funktioniert, gibt es zwei Dinge, die bei diesem Ansatz nicht gut zu mir passen:

  • Mit dieser Methode können Sie nur Puffer mit einer Größe von 2 Potenzen zuweisen. Sie können jedoch eine Logik implementieren, die alloc_pagesso viele Aufrufe wie nötig mit abnehmender Reihenfolge aufruft, um einen Puffer beliebiger Größe zu erhalten, der aus Unterpuffern unterschiedlicher Größe besteht. Dies erfordert dann eine gewisse Logik, um diese Puffer im mmap()und mit DMA mit Scatter-Gather ( sg) -Aufrufen zusammenzubinden und nicht single.
  • split_page() sagt in seiner Dokumentation:
 * Note: this is probably too low level an operation for use in drivers.
 * Please consult with lkml before using this in your driver.

Diese Probleme lassen sich leicht lösen, wenn im Kernel eine Schnittstelle vorhanden wäre, über die eine beliebige Anzahl zusammenhängender physischer Seiten zugewiesen werden kann. Ich weiß nicht, warum es das nicht gibt, aber ich finde die oben genannten Probleme nicht so wichtig, um herauszufinden, warum dies nicht verfügbar ist / wie man es implementiert :-)

rem
quelle
2

Vielleicht hilft Ihnen dies zu verstehen, warum alloc_pages eine Seitenzahl mit einer Potenz von 2 erfordert.

Um den häufig verwendeten Seitenzuweisungsprozess (und die Verringerung externer Fragmentierungen) zu optimieren, hat der Linux-Kernel einen CPU-Seitencache und einen Buddy-Allokator entwickelt, um Speicher zuzuweisen (es gibt einen anderen Allokator, slab, um Speicherzuordnungen bereitzustellen, die kleiner als a sind Seite).

Der CPU-Seiten-Cache dient der einseitigen Zuweisungsanforderung, während der Buddy-Allokator 11 Listen mit jeweils 2 ^ {0-10} physischen Seiten führt. Diese Listen funktionieren gut, wenn Seiten zugewiesen und freigegeben werden. Voraussetzung ist natürlich, dass Sie einen Puffer mit einer Potenz von 2 anfordern.

medivh
quelle