it-swarm.com.de

Linux-Kernel-Gerätetreiber an DMA von einem Gerät in den User-Space-Speicher

Ich möchte Daten von einem DMA -fähigen PCIe-Hardwaregerät so schnell wie möglich in den Benutzerbereich übertragen.

F: Wie kombiniere ich "direkte E/A zum User-Space mit/und/über eine DMA Übertragung"?

  1. Beim Lesen von LDD3 scheint es, als müsste ich einige verschiedene Arten von IO Operationen ausführen !?

    dma_alloc_coherent gibt mir die physikalische Adresse, die ich an das Hardwaregerät übergeben kann. Sie müssen jedoch get_user_pages einrichten und einen Aufruf vom Typ copy_to_user ausführen, wenn die Übertragung abgeschlossen ist. Dies scheint eine Verschwendung, fordert das Gerät auf DMA in den Kernel-Speicher (als Puffer wirken), dann wieder den User-Space zu übertragen. LDD3 p453: /* Only now is it safe to access the buffer, copy to user, etc. */

  2. Was ich im Idealfall möchte, ist eine Erinnerung, die:

    • Ich kann im User-Space verwenden (Möglicherweise Treiber über einen ioctl-Aufruf anfordern, um DMA-fähigen Speicher/Puffer zu erstellen?)
    • Ich kann eine physikalische Adresse von abrufen, um sie an das Gerät weiterzuleiten, sodass der Benutzer lediglich einen Lesevorgang für den Treiber durchführen muss
    • die Lesemethode würde die DMA Übertragung aktivieren, das Warten auf den DMA vollständigen Interrupt blockieren und den danach gelesenen User-Space freigeben (User-Space kann jetzt sicher verwendet/gelesen werden).

Benötige ich Streaming-Zuordnungen für eine Seite, Setup-Zuordnungen und Benutzerbereichspuffer, die mit get_user_pagesdma_map_page zugeordnet sind?

Mein Code richtet get_user_pages bisher an der angegebenen Adresse aus dem User-Space ein (ich nenne dies den direkten E/A-Teil). Dann dma_map_page mit einer Seite aus get_user_pages. Ich gebe dem Gerät den Rückgabewert von dma_map_page als die physikalische Übertragungsadresse DMA.

Ich verwende einige Kernelmodule als Referenz: drivers_scsi_st.c und drivers-net-sh_eth.c. Ich würde mir Infiniband-Code ansehen, kann aber nicht herausfinden, welcher der grundlegendste ist!

Vielen Dank im Voraus.

27
Ian Vaughan

Ich arbeite gerade an genau der gleichen Sache und gehe die ioctl()-Route. Die allgemeine Idee ist, dass der Benutzerraum den Puffer reserviert, der für die Übertragung von DMA verwendet wird, und eine ioctl() wird verwendet, um die Größe und Adresse dieses Puffers an den Gerätetreiber zu übergeben. Der Treiber verwendet dann Scatter-Gather-Listen zusammen mit der Streaming DMA -API, um Daten direkt zum und vom Gerät und vom User-Space-Puffer zu übertragen.

Die Implementierungsstrategie, die ich verwende, besteht darin, dass die ioctl() im Treiber in eine Schleife einläuft, in der der DMA-Speicherbereich des Benutzers in 256k-Blöcken (dies ist die von der Hardware auferlegte Grenze für die Anzahl der Streu/Gather-Einträge, die damit behandelt werden können) ist. Dies wird in einer Funktion isoliert, die blockiert, bis jede Übertragung abgeschlossen ist (siehe unten). Wenn alle Bytes übertragen wurden oder die inkrementelle Übertragungsfunktion einen Fehler zurückgibt, wird ioctl() beendet und kehrt zum Benutzerbereich zurück

Pseudocode für die ioctl()

/*serialize all DMA transfers to/from the device*/
if (mutex_lock_interruptible( &device_ptr->mtx ) )
    return -EINTR;

chunk_data = (unsigned long) user_space_addr;
while( *transferred < total_bytes && !ret ) {
    chunk_bytes = total_bytes - *transferred;
    if (chunk_bytes > HW_DMA_MAX)
        chunk_bytes = HW_DMA_MAX; /* 256kb limit imposed by my device */
    ret = transfer_chunk(device_ptr, chunk_data, chunk_bytes, transferred);
    chunk_data += chunk_bytes;
    chunk_offset += chunk_bytes;
}

mutex_unlock(&device_ptr->mtx);

Pseudocode für inkrementelle Übertragungsfunktion:

/*Assuming the userspace pointer is passed as an unsigned long, */
/*calculate the first,last, and number of pages being transferred via*/

first_page = (udata & PAGE_MASK) >> PAGE_SHIFT;
last_page = ((udata+nbytes-1) & PAGE_MASK) >> PAGE_SHIFT;
first_page_offset = udata & PAGE_MASK;
npages = last_page - first_page + 1;

/* Ensure that all userspace pages are locked in memory for the */
/* duration of the DMA transfer */

down_read(&current->mm->mmap_sem);
ret = get_user_pages(current,
                     current->mm,
                     udata,
                     npages,
                     is_writing_to_userspace,
                     0,
                     &pages_array,
                     NULL);
up_read(&current->mm->mmap_sem);

/* Map a scatter-gather list to point at the userspace pages */

/*first*/
sg_set_page(&sglist[0], pages_array[0], PAGE_SIZE - fp_offset, fp_offset);

/*middle*/
for(i=1; i < npages-1; i++)
    sg_set_page(&sglist[i], pages_array[i], PAGE_SIZE, 0);

/*last*/
if (npages > 1) {
    sg_set_page(&sglist[npages-1], pages_array[npages-1],
        nbytes - (PAGE_SIZE - fp_offset) - ((npages-2)*PAGE_SIZE), 0);
}

/* Do the hardware specific thing to give it the scatter-gather list
   and tell it to start the DMA transfer */

/* Wait for the DMA transfer to complete */
ret = wait_event_interruptible_timeout( &device_ptr->dma_wait, 
         &device_ptr->flag_dma_done, HZ*2 );

if (ret == 0)
    /* DMA operation timed out */
else if (ret == -ERESTARTSYS )
    /* DMA operation interrupted by signal */
else {
    /* DMA success */
    *transferred += nbytes;
    return 0;
}

Der Interrupt-Handler ist außergewöhnlich kurz:

/* Do hardware specific thing to make the device happy */

/* Wake the thread waiting for this DMA operation to complete */
device_ptr->flag_dma_done = 1;
wake_up_interruptible(device_ptr->dma_wait);

Bitte beachten Sie, dass dies nur ein allgemeiner Ansatz ist. Ich habe in den letzten Wochen an diesem Treiber gearbeitet und habe ihn noch nicht wirklich getestet ... Behandeln Sie diesen Pseudocode bitte nicht als Evangelium und seien Sie sicher, dass Sie ihn verdoppeln Überprüfen Sie alle Logik und Parameter ;-).

14
Rakis

Sie haben im Grunde die richtige Idee: In 2.1 können Sie einfach den Userspace dazu bringen, alten Speicher zuzuweisen. Sie möchten, dass es seitenorientiert ist, sodass posix_memalign() eine praktische API ist, die verwendet werden kann.

Dann muss der Benutzerraum die virtuelle Adresse des Benutzerraums und die Größe dieses Puffers irgendwie passieren. ioctl () ist ein guter, schneller und schmutziger Weg, dies zu tun. Weisen Sie im Kernel ein entsprechend großes Puffer-Array mit struct page* - user_buf_size/PAGE_SIZE-Einträgen zu, und verwenden Sie get_user_pages(), um eine Liste mit struct page * für den Userspace-Puffer zu erhalten.

Sobald Sie das haben, können Sie ein Array von struct scatterlist zuweisen, das die gleiche Größe wie Ihr Seitenarray hat, und die Liste der Seiten durchlaufen, die sg_set_page() ausführen. Nach dem Einrichten der SG-Liste führen Sie dma_map_sg() für das Array der Scatterlist aus. Anschließend können Sie sg_dma_address und sg_dma_len für jeden Eintrag in der Scatterlist abrufen. Beachten Sie, dass Sie den Rückgabewert von dma_map_sg() verwenden müssen, da Sie möglicherweise weniger zugeordnet bekommen Einträge, weil die Dinge möglicherweise durch den DMA Zuordnungscode zusammengeführt werden).

Damit haben Sie alle Busadressen, die an Ihr Gerät übergeben werden sollen, und Sie können dann das DMA auslösen und darauf warten, wie Sie möchten. Das auf Read () basierende Schema ist wahrscheinlich in Ordnung.

Sie können sich auf drivers/infiniband/core/umem.c, speziell ib_umem_get(), für einen Code beziehen, der diese Zuordnung aufbaut, obwohl die Allgemeinheit, mit der dieser Code umgehen muss, dies etwas verwirrend erscheinen kann.

Wenn Ihr Gerät mit Streulicht-/Sammellisten nicht gut umgehen kann und Sie zusammenhängenden Speicher wünschen, können Sie alternativ get_free_pages() verwenden, um einen physisch zusammenhängenden Puffer zuzuweisen, und verwenden Sie dma_map_page(). Um dem Benutzerraum Zugriff auf diesen Speicher zu gewähren, muss Ihr Treiber lediglich eine mmap-Methode anstelle des Ioctl-Protokolls implementieren, wie oben beschrieben.

12
Roland

Irgendwann wollte ich der User-Space-Anwendung erlauben, DMA Puffer zuzuordnen und diese dem User-Space zuzuordnen und die physische Adresse zu erhalten, um mein Gerät steuern zu können und do DMA Transaktionen (Bus-Mastering) vollständig aus dem Benutzerraum, wobei der Linux-Kernel vollständig umgangen wird. Ich habe jedoch eine etwas andere Herangehensweise verwendet. Zuerst begann ich mit einem minimalen Kernel-Modul, das ein PCIe-Gerät initialisierte/prüfte und ein Zeichengerät erstellte. Dieser Treiber erlaubte dann einer User-Space-Anwendung, zwei Dinge zu tun:

  1. Ordnen Sie die E/A-Leiste des PCIe-Geräts mithilfe der Funktion remap_pfn_range() in den Benutzerbereich auf.
  2. Zuordnen und free DMA Puffer, ordnen Sie sie dem Benutzerraum zu und übergeben Sie eine physische Busadresse an die Benutzerraumanwendung.

Im Grunde läuft es auf eine benutzerdefinierte Implementierung von mmap() call (obwohl file_operations). Eine für I/O-Leiste ist einfach:

struct vm_operations_struct a2gx_bar_vma_ops = {
};

static int a2gx_cdev_mmap_bar2(struct file *filp, struct vm_area_struct *vma)
{
    struct a2gx_dev *dev;
    size_t size;

    size = vma->vm_end - vma->vm_start;
    if (size != 134217728)
        return -EIO;

    dev = filp->private_data;
    vma->vm_ops = &a2gx_bar_vma_ops;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_private_data = dev;

    if (remap_pfn_range(vma, vma->vm_start,
                        vmalloc_to_pfn(dev->bar2),
                        size, vma->vm_page_prot))
    {
        return -EAGAIN;
    }

    return 0;
}

Und noch eine, die DMA Puffer mit pci_alloc_consistent() zuweist, ist etwas komplizierter:

static void a2gx_dma_vma_close(struct vm_area_struct *vma)
{
    struct a2gx_dma_buf *buf;
    struct a2gx_dev *dev;

    buf = vma->vm_private_data;
    dev = buf->priv_data;

    pci_free_consistent(dev->pci_dev, buf->size, buf->cpu_addr, buf->dma_addr);
    buf->cpu_addr = NULL; /* Mark this buffer data structure as unused/free */
}

struct vm_operations_struct a2gx_dma_vma_ops = {
    .close = a2gx_dma_vma_close
};

static int a2gx_cdev_mmap_dma(struct file *filp, struct vm_area_struct *vma)
{
    struct a2gx_dev *dev;
    struct a2gx_dma_buf *buf;
    size_t size;
    unsigned int i;

    /* Obtain a pointer to our device structure and calculate the size
       of the requested DMA buffer */
    dev = filp->private_data;
    size = vma->vm_end - vma->vm_start;

    if (size < sizeof(unsigned long))
        return -EINVAL; /* Something fishy is happening */

    /* Find a structure where we can store extra information about this
       buffer to be able to release it later. */
    for (i = 0; i < A2GX_DMA_BUF_MAX; ++i) {
        buf = &dev->dma_buf[i];
        if (buf->cpu_addr == NULL)
            break;
    }

    if (buf->cpu_addr != NULL)
        return -ENOBUFS; /* Oops, hit the limit of allowed number of
                            allocated buffers. Change A2GX_DMA_BUF_MAX and
                            recompile? */

    /* Allocate consistent memory that can be used for DMA transactions */
    buf->cpu_addr = pci_alloc_consistent(dev->pci_dev, size, &buf->dma_addr);
    if (buf->cpu_addr == NULL)
        return -ENOMEM; /* Out of juice */

    /* There is no way to pass extra information to the user. And I am too lazy
       to implement this mmap() call using ioctl(). So we simply tell the user
       the bus address of this buffer by copying it to the allocated buffer
       itself. Hacks, hacks everywhere. */
    memcpy(buf->cpu_addr, &buf->dma_addr, sizeof(buf->dma_addr));

    buf->size = size;
    buf->priv_data = dev;
    vma->vm_ops = &a2gx_dma_vma_ops;
    vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
    vma->vm_private_data = buf;

    /*
     * Map this DMA buffer into user space.
     */
    if (remap_pfn_range(vma, vma->vm_start,
                        vmalloc_to_pfn(buf->cpu_addr),
                        size, vma->vm_page_prot))
    {
        /* Out of luck, rollback... */
        pci_free_consistent(dev->pci_dev, buf->size, buf->cpu_addr,
                            buf->dma_addr);
        buf->cpu_addr = NULL;
        return -EAGAIN;
    }

    return 0; /* All good! */
}

Sobald diese vorhanden sind, kann die Benutzerraumanwendung praktisch alles tun - das Gerät durch Lesen/Schreiben von/in E/A-Register steuern, zuweisen und freigeben DMA Puffer beliebiger Größe und das Gerät ausführen lassen DMA Transaktionen. Der einzige fehlende Teil ist das Interrupt-Handling. Ich habe Abfragen im Benutzerraum durchgeführt, meine CPU gebrannt und die Interrupts deaktiviert.

Ich hoffe es hilft. Viel Glück!

6
user405725

Ich werde mit der Richtung der Implementierung verwirrt. Ich möchte...

Berücksichtigen Sie die Anwendung, wenn Sie einen Treiber entwerfen.
.__ Was ist die Art der Datenverschiebung, Häufigkeit, Größe und was könnte sonst noch im System passieren?

Ist die herkömmliche Lese-/Schreib-API ausreichend? Ist die direkte Zuordnung des Geräts in den Benutzerbereich OK? Ist ein reflektierender (halbkohärenter) gemeinsam genutzter Speicher wünschenswert?

Das manuelle Manipulieren von Daten (Lesen/Schreiben) ist eine ziemlich gute Option, wenn sich die Daten gut verstehen lassen. Die Verwendung von Allzweck VM und Lesen/Schreiben kann bei einer Inline-Kopie ausreichend sein. Direktes Mapping von nicht zwischengespeicherten Zugriffen auf das Peripheriegerät ist praktisch, kann jedoch unbeholfen sein. Wenn es sich bei dem Zugriff um die relativ seltene Bewegung großer Blöcke handelt, kann es sinnvoll sein, regulären Speicher zu verwenden, die Pin des Laufwerks zu verwenden, Adressen zu übersetzen, DMA und die Seiten freizugeben. Zur Optimierung können die Seiten (möglicherweise sehr groß) vorgeheftet und übersetzt werden. Das Laufwerk kann dann den vorbereiteten Speicher erkennen und die Komplexität der dynamischen Übersetzung vermeiden. Bei vielen kleinen E/A-Vorgängen ist es sinnvoll, den Antrieb asynchron auszuführen. Wenn Eleganz wichtig ist, kann mit dem VM - Dirty-Page-Flag automatisch identifiziert werden, was verschoben werden muss. Mit einem Aufruf (meta_sync ()) können Seiten geleert werden. Vielleicht eine Mischung der oben genannten Werke ...

Zu oft betrachten die Menschen das größere Problem nicht, bevor sie sich in die Details vertiefen. Oft sind die einfachsten Lösungen ausreichend. Ein wenig Aufwand beim Erstellen eines Verhaltensmodells kann dabei helfen, zu bestimmen, welche API bevorzugt wird.

1
fbp
first_page_offset = udata & PAGE_MASK; 

Es scheint falsch zu sein. Es sollte entweder sein:

first_page_offset = udata & ~PAGE_MASK;

oder 

first_page_offset = udata & (PAGE_SIZE - 1)
0
Suman

Es ist erwähnenswert, dass Treiber mit Unterstützung für Scatter-Gather DMA und Speicherzuweisung für Benutzer am effizientesten sind und die höchste Leistung aufweisen. Wenn wir jedoch keine hohe Leistung benötigen oder unter vereinfachten Bedingungen einen Treiber entwickeln möchten, können wir einige Tricks anwenden.

Verzichten Sie auf ein Design mit null Kopien. Es lohnt sich zu überlegen, wann der Datendurchsatz nicht zu groß ist. In einem solchen Entwurf können Daten von copy_to_user(user_buffer, kernel_dma_buffer, count); user_buffer in den Benutzer kopiert werden. Dies kann beispielsweise ein Pufferargument in der Systemaufrufimplementierung von character device read () sein. Wir müssen uns noch um die Zuweisung von kernel_dma_buffer Kümmern. Es kann sich beispielsweise um den Speicher handeln, der durch den Aufruf von dma_alloc_coherent() erhalten wurde.

Der andere Trick besteht darin, den Systemspeicher zum Startzeitpunkt zu begrenzen und ihn dann als großen zusammenhängenden DMA-Puffer zu verwenden. Es ist besonders nützlich während der Treiber- und FPGA DMA Controller-Entwicklung und wird in Produktionsumgebungen eher nicht empfohlen. Nehmen wir an, der PC verfügt über 32 GB RAM. Wenn wir mem=20GB Zur Liste der Kernel-Boot-Parameter hinzufügen, können wir 12 GB als riesigen zusammenhängenden DMA-Puffer verwenden. Um diesen Speicher dem Benutzerraum zuzuordnen, implementieren Sie einfach mmap () as

remap_pfn_range(vma,
    vma->vm_start,
    (0x500000000 >> PAGE_SHIFT) + vma->vm_pgoff, 
    vma->vm_end - vma->vm_start,
    vma->vm_page_prot)

Natürlich werden diese 12 GB vom Betriebssystem komplett weggelassen und können nur von Prozessen verwendet werden, die sie in ihren Adressraum abgebildet haben. Wir können versuchen, dies zu vermeiden, indem wir den Contiguous Memory Allocator (CMA) verwenden.

Wieder oben genannte Tricks ersetzen nicht den vollständigen Treiber für Scatter-Gather, Nullkopie DMA, sind jedoch während der Entwicklungszeit oder auf einigen Plattformen mit geringerer Leistung nützlich.

0
SlawekS