GNU/Linux >> LINUX-Kenntnisse >  >> Linux

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

Ich bin immer verwirrt mit der Richtung zu implementieren. Ich möchte...

Berücksichtigen Sie die Anwendung beim Entwerfen eines Treibers.
Was ist die Art der Datenbewegung, Häufigkeit, Größe und was könnte sonst noch im System vor sich gehen?

Reicht die herkömmliche Lese-/Schreib-API aus? Ist die direkte Abbildung des Geräts auf den Benutzerbereich in Ordnung? Ist ein reflektierender (semi-kohärenter) gemeinsam genutzter Speicher wünschenswert?

Die manuelle Manipulation von Daten (Lesen/Schreiben) ist eine ziemlich gute Option, wenn die Daten sich gut verstehen lassen. Die Verwendung von Allzweck-VM und Lese-/Schreibzugriff kann bei einer Inline-Kopie ausreichend sein. Das direkte Abbilden von nicht zwischenspeicherbaren Zugriffen auf das Peripheriegerät ist bequem, kann aber umständlich sein. Wenn der Zugriff die relativ seltene Bewegung großer Blöcke ist, kann es sinnvoll sein, regulären Speicher zu verwenden, den Laufwerksstift zu haben, Adressen zu übersetzen, DMA durchzuführen und die Seiten freizugeben. Zur Optimierung können die (vielleicht riesigen) Seiten vorab angeheftet und übersetzt werden; Das Laufwerk kann dann den vorbereiteten Speicher erkennen und die Komplexität der dynamischen Übersetzung vermeiden. Bei vielen kleinen I/O-Operationen ist es sinnvoll, den Antrieb asynchron laufen zu lassen. Wenn Eleganz wichtig ist, kann das VM-Dirty-Page-Flag verwendet werden, um automatisch zu identifizieren, was verschoben werden muss, und ein (meta_sync())-Aufruf kann verwendet werden, um Seiten zu leeren. Vielleicht funktioniert eine Mischung der oben genannten...

Zu oft betrachten die Leute nicht das größere Problem, bevor sie sich mit den Details befassen. Oft reichen die einfachsten Lösungen aus. Ein wenig Aufwand beim Erstellen eines Verhaltensmodells kann dabei helfen, festzulegen, welche API vorzuziehen ist.


Ich arbeite gerade an genau der gleichen Sache und gehe zum ioctl() Route. Die allgemeine Idee ist, dass der Benutzerraum den Puffer zuweist, der für die DMA-Übertragung und einen ioctl() verwendet wird 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 User-Space-Puffer zu übertragen.

Die Implementierungsstrategie, die ich verwende, ist die ioctl() Im Treiber tritt eine Schleife ein, die den Userspace-Puffer in Blöcken von 256 KB per DMA verarbeitet (das ist die von der Hardware auferlegte Grenze für die Anzahl der Scatter/Gather-Einträge, die er verarbeiten kann). Dies wird innerhalb einer Funktion isoliert, die blockiert, bis jede Übertragung abgeschlossen ist (siehe unten). Wenn alle Bytes übertragen wurden oder die inkrementelle Übertragungsfunktion den Fehler ioctl() zurückgibt beendet und kehrt zum Benutzerbereich zurück

Pseudocode für 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 ihn noch nicht wirklich getestet ... Also behandeln Sie diesen Pseudo-Code bitte nicht als Evangelium und verdoppeln Sie ihn unbedingt Überprüfen Sie alle Logik und Parameter;-).


Irgendwann wollte ich der User-Space-Anwendung erlauben, DMA-Puffer zuzuweisen und sie dem User-Space zuordnen zu lassen und die physische Adresse zu erhalten, um mein Gerät steuern und DMA-Transaktionen (Bus-Mastering) vollständig vom User-Space aus durchführen zu können den Linux-Kernel umgehen. Ich habe jedoch einen etwas anderen Ansatz verwendet. Zuerst begann ich mit einem minimalen Kernelmodul, das PCIe-Geräte initialisierte/sondierte 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 mit remap_pfn_range() dem Benutzerbereich zu Funktion.
  2. Ordnen Sie DMA-Puffer zu und geben Sie sie frei, ordnen Sie sie dem Userspace zu und übergeben Sie eine physische Busadresse an die Userspace-Anwendung.

Im Grunde läuft es auf eine benutzerdefinierte Implementierung von mmap() hinaus anrufen (obwohl file_operations ). Einer für die 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 ein weiterer, der 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 User Space-Anwendung so ziemlich alles tun – das Gerät durch Lesen/Schreiben von/in E/A-Register steuern, DMA-Puffer beliebiger Größe zuweisen und freigeben und das Gerät DMA-Transaktionen durchführen lassen. Der einzige fehlende Teil ist die Interrupt-Behandlung. Ich habe Abfragen im Benutzerbereich durchgeführt, meine CPU verbrannt und Interrupts deaktiviert.

Ich hoffe es hilft. Viel Glück!


Sie haben im Grunde die richtige Idee:In 2.1 können Sie den Userspace einfach beliebigen alten Speicher zuweisen lassen. Sie möchten es seitenausgerichtet haben, also posix_memalign() ist eine praktische API.

Lassen Sie dann den Benutzerbereich die virtuelle Adresse des Benutzerbereichs und die Größe dieses Puffers irgendwie übergeben; ioctl() ist ein guter schneller und schmutziger Weg, dies zu tun. Ordnen Sie im Kernel ein entsprechend großes Pufferarray von struct page* zu -- user_buf_size/PAGE_SIZE Einträge -- und verwenden Sie get_user_pages() um eine Liste von struct page* für den Userspace-Puffer zu erhalten.

Sobald Sie das haben, können Sie ein Array von struct scatterlist zuweisen das hat die gleiche Größe wie Ihr Seitenarray und durchlaufen Sie die Liste der Seiten, die sg_set_page() ausführen . Nachdem die SG-Liste eingerichtet ist, machen Sie dma_map_sg() auf dem Scatterlist-Array und dann können Sie den sg_dma_address erhalten und sg_dma_len für jeden Eintrag in der Streuliste (beachten Sie, dass Sie den Rückgabewert von dma_map_sg() verwenden müssen da Sie am Ende möglicherweise weniger zugeordnete Einträge haben, da die Dinge möglicherweise durch den DMA-Zuordnungscode zusammengeführt werden).

Dadurch erhalten Sie alle Busadressen, die Sie an Ihr Gerät weitergeben können, und dann können Sie den DMA auslösen und darauf warten, wie Sie möchten. Das read()-basierte Schema, das Sie haben, ist wahrscheinlich in Ordnung.

Sie können auf drivers/infiniband/core/umem.c verweisen, insbesondere ib_umem_get() , für einigen Code, der diese Zuordnung aufbaut, obwohl die Allgemeingültigkeit, mit der dieser Code umgehen muss, es etwas verwirrend machen kann.

Alternativ können Sie get_free_pages() verwenden, wenn Ihr Gerät Streu-/Sammellisten nicht so gut handhabt und Sie zusammenhängenden Speicher wünschen um einen physisch zusammenhängenden Puffer zuzuweisen und verwenden Sie dma_map_page() auf diesem. Um dem Userspace Zugriff auf diesen Speicher zu geben, muss Ihr Treiber nur einen mmap implementieren Methode anstelle von ioctl wie oben beschrieben.


Linux
  1. Ausführen einer User-Space-Funktion aus dem Kernel-Space

  2. Wie lade ich Linux-Kernel-Module aus C-Code?

  3. Wie greife ich (wenn möglich) auf den Kernel-Space aus dem User-Space zu?

  4. IOCTL-Linux-Gerätetreiber

  5. Rufen Sie eine Userspace-Funktion innerhalb eines Linux-Kernel-Moduls auf

So erstellen Sie einen Linux-Kernel von Grund auf neu {Schritt-für-Schritt-Anleitung}

So erstellen Sie einen Linux-Kernel von Grund auf neu

Wie Sie von Windows aus SSH in Ihren Linux-Server einbinden

So kompilieren Sie den Linux-Kernel aus der Quelle, um einen benutzerdefinierten Kernel zu erstellen

Wie man SSH in einen Windows 10-Rechner von Linux ODER Windows ODER überall her einfügt

Linux-Speichersegmentierung