Linux kernel device driver to DMA from a device into user-space memory


Quiero obtener datos de un dispositivo de hardware PCIe habilitado para DMA en el espacio de usuario lo más rápido posible.

P: ¿Cómo combino "E / S directas al espacio de usuario con / y / a través de una transferencia DMA"

  1. Leyendo LDD3, parece que necesito realizar algunos tipos diferentes de operaciones de IO!?

    dma_alloc_coherent me da la dirección física que puedo pasar al dispositivo de hardware. Pero tendría que haber configurado get_user_pages y realizar una llamada de tipo copy_to_user cuando la transferencia completo. Esto parece un desperdicio, pidiendo al Dispositivo DMA en la memoria del kernel (actuando como buffer) y luego transfiriéndolo de nuevo al espacio de usuario. LDD3 p453: /* Only now is it safe to access the buffer, copy to user, etc. */

  2. Lo que idealmente quiero es algún recuerdo que: {[13]]}

    • Puedo usar en el espacio de usuario (Tal vez solicitar el controlador a través de una llamada ioctl para crear memoria/búfer DMA?)
    • Puedo obtener una dirección física para pasar al dispositivo de modo que todo el espacio de usuario tiene que hacer es realizar una lectura en el controlador
    • la lectura el método activaría la transferencia de DMA, bloquearía la espera de la interrupción completa de DMA y liberaría el espacio de usuario leído después (el espacio de usuario ahora es seguro de usar/leer la memoria).

¿Necesito asignaciones de streaming de una sola página, asignación de configuración y búferes de espacio de usuario mapeados con get_user_pages dma_map_page?

Mi código hasta ahora establece get_user_pages en la dirección dada desde el espacio de usuario (llamo a esto la parte de E/S directa). Luego, dma_map_page con una página de get_user_pages. Le doy al dispositivo el valor devuelto desde dma_map_page como la dirección de transferencia física DMA.

Estoy usando algunos módulos del núcleo como referencia: drivers_scsi_st.c y drivers-net-sh_eth.c. Me gustaría mirar el código infiniband, pero no puedo encontrar cuál es el más básico!

Muchas gracias de antemano.

Author: Jorge Bellón, 2011-04-04

6 answers

En realidad estoy trabajando en exactamente lo mismo ahora mismo y voy por la ruta ioctl(). La idea general es que el espacio de usuario asigne el búfer que se utilizará para la transferencia DMA y se utilizará un ioctl() para pasar el tamaño y la dirección de este búfer al controlador del dispositivo. A continuación, el controlador utilizará listas de recopilación de dispersión junto con la API de streaming DMA para transferir datos directamente desde y hacia el búfer del dispositivo y el espacio de usuario.

La estrategia de implementación que estoy usando es que el ioctl() en el controlador ingresa un bucle que DMA es el búfer del espacio de usuario en trozos de 256k (que es el límite impuesto por hardware para cuántas entradas de dispersión/recolección puede manejar). Esto está aislado dentro de una función que bloquea hasta que se completa cada transferencia (ver más abajo). Cuando se transfieren todos los bytes o la función de transferencia incremental devuelve un error, ioctl() sale y regresa al espacio de usuario

Pseudo código para el 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);

Pseudo código para transferencia incremental función:

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

El manejador de interrupciones es excepcionalmente breve:

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

Tenga en cuenta que esto es solo un enfoque general, he estado trabajando en este controlador durante las últimas semanas y aún no lo he probado... Así que, por favor, no trates este pseudo código como evangelio y asegúrate de verificar dos veces toda la lógica y los parámetros ;-).

 14
Author: Rakis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-04 14:38:03

Básicamente tienes la idea correcta: en la versión 2.1, puedes hacer que el espacio de usuario asigne cualquier memoria antigua. Lo quieres alineado con la página, por lo que posix_memalign() es una API útil para usar.

Entonces haga pasar el espacio de usuario en la dirección virtual del espacio de usuario y el tamaño de este búfer de alguna manera; ioctl() es una buena forma rápida y sucia de hacer esto. En el núcleo, asignar una matriz de búfer de tamaño adecuado de struct page* -- user_buf_size/PAGE_SIZE entries entries y use get_user_pages() para obtener una lista de la página de estructura* para el búfer del espacio de usuario.

Una vez usted tiene que, usted puede asignar una matriz de struct scatterlist que es del mismo tamaño que su matriz de página y bucle a través de la lista de páginas haciendo sg_set_page(). Después de configurar la lista sg, lo hace dma_map_sg() en la matriz de scatterlist y luego puede obtener sg_dma_address y sg_dma_len para cada entrada en la lista de dispersión (tenga en cuenta que debe usar el valor devuelto de dma_map_sg() porque puede terminar con menos entradas asignadas porque las cosas pueden fusionarse con el código de asignación DMA).

Eso le da todas las direcciones de autobús para pasar a su dispositivo, y luego puede activar el DMA y esperar a que lo desee. El esquema basado en read () que tienes probablemente esté bien.

Puede hacer referencia a drivers/infiniband/core/umem.c, específicamente ib_umem_get(), para algún código que construye esta asignación, aunque la generalidad con la que ese código necesita lidiar puede hacerlo un poco confuso.

Alternativamente, si su dispositivo no maneja las listas de dispersión/recopilación demasiado bien y desea una memoria contigua, puede usar get_free_pages() para asigne un búfer físicamente contiguo y use dma_map_page() en eso. Para dar acceso en espacio de usuario a esa memoria, su controlador solo necesita implementar un método mmap en lugar del ioctl como se describió anteriormente.

 12
Author: Roland,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-21 07:16:16

En algún momento quise permitir que la aplicación de espacio de usuario asignara búferes DMA y lo mapeara al espacio de usuario y obtuviera la dirección física para poder controlar mi dispositivo y hacer transacciones DMA (dominio de bus) completamente desde el espacio de usuario, evitando totalmente el kernel de Linux. Sin embargo, he utilizado un enfoque un poco diferente. Primero comencé con un módulo de núcleo mínimo que inicializaba / probaba el dispositivo PCIe y creaba un dispositivo de caracteres. Ese controlador entonces permitió un espacio de usuario aplicación para hacer dos cosas:

  1. Asigna la barra de E/S del dispositivo PCIe al espacio de usuario usando la función remap_pfn_range().
  2. Asigne y libere buffers DMA, mapéelos al espacio de usuario y pase una dirección de bus física a la aplicación de espacio de usuario.

Básicamente, se reduce a una implementación personalizada de mmap() call (aunque file_operations). Uno para la barra de E/S es fácil:

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

Y otro que asigna búferes DMA usando pci_alloc_consistent() es un poco más complicado:

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! */
}

Una vez que los están en su lugar, la aplicación de espacio de usuario puede prácticamente hacer todo: controlar el dispositivo leyendo/escribiendo desde/hacia registros de E / S, asignar y liberar búferes DMA de tamaño arbitrario y hacer que el dispositivo realice transacciones DMA. La única parte que falta es el manejo de interrupciones. Estaba haciendo encuestas en el espacio de usuario, quemando mi CPU y tenía interrupciones desactivadas.

Espero que ayude. ¡Buena suerte!

 6
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-03 13:44:49

Me estoy confundiendo con la dirección a implementar. Quiero hacerlo...

Tenga en cuenta la aplicación al diseñar un controlador.
¿Cuál es la naturaleza del movimiento de datos, la frecuencia, el tamaño y qué más podría estar sucediendo en el sistema?

¿ Es suficiente la API de lectura/escritura tradicional? ¿Está bien la asignación directa del dispositivo al espacio de usuario? ¿Es deseable una memoria compartida reflexiva (semi-coherente)?

La manipulación manual de datos (lectura/escritura) es una opción bastante buena si el los datos se prestan a ser bien entendidos. El uso de VM de propósito general y lectura/escritura puede ser suficiente con una copia en línea. La asignación directa de accesos no accesibles al periférico es conveniente, pero puede ser torpe. Si el acceso es el movimiento relativamente poco frecuente de bloques grandes, puede tener sentido usar memoria regular, tener el pin de la unidad, traducir direcciones, DMA y liberar las páginas. Como una optimización, las páginas (tal vez enormes) pueden ser pre ancladas y traducidas; la unidad entonces puede reconozca la memoria preparada y evite las complejidades de la traducción dinámica. Si hay un montón de pequeñas operaciones de E/S, tener la unidad ejecutada asincrónicamente tiene sentido. Si la elegancia es importante, el indicador de página sucia de VM se puede usar para identificar automáticamente lo que se necesita mover y una llamada (meta_sync ()) se puede usar para vaciar las páginas. Tal vez una mezcla de las obras anteriores...

Con demasiada frecuencia la gente no mira el problema más grande, antes de profundizar en los detalles. A menudo el más simple las soluciones son suficientes. Un poco de esfuerzo para construir un modelo de comportamiento puede ayudar a guiar qué API es preferible.

 1
Author: fbp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-04-17 13:22:20
first_page_offset = udata & PAGE_MASK; 

Parece incorrecto. Debe ser:

first_page_offset = udata & ~PAGE_MASK;

O

first_page_offset = udata & (PAGE_SIZE - 1)
 0
Author: Suman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-02-16 07:02:51

Vale la pena mencionar que el controlador con soporte de DMA Scatter-Gather y la asignación de memoria de espacio de usuario es más eficiente y tiene el mayor rendimiento. Sin embargo, en caso de que no necesitemos un alto rendimiento o queramos desarrollar un controlador en algunas condiciones simplificadas podemos usar algunos trucos.

Renunciar a cero diseño de copia. Vale la pena considerar cuando el rendimiento de datos no es demasiado grande. En tal diseño los datos pueden copiarse al usuario por copy_to_user(user_buffer, kernel_dma_buffer, count); user_buffer podría ser, por ejemplo, un argumento de búfer en carácter implementación de llamada al sistema device read (). Todavía tenemos que ocuparnos de kernel_dma_buffer la asignación. Podría por memoria obtenida de dma_alloc_coherent() llamar por ejemplo.

El otro truco es limitar la memoria del sistema en el momento del arranque y luego usarla como un enorme búfer DMA contiguo. Es especialmente útil durante el desarrollo de controladores y controladores DMA FPGA y no se recomienda en entornos de producción. Digamos que la PC tiene 32 GB de RAM. Si añadimos mem=20GB a la lista de parámetros de arranque del kernel podemos usar 12GB como enorme buffer dma contiguo. Para asignar esta memoria al espacio de usuario simplemente implementa mmap () como

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

Por supuesto, este 12GB es completamente omitido por el sistema operativo y solo puede ser utilizado por el proceso que lo ha asignado a su espacio de direcciones. Podemos intentar evitarlo usando el Asignador de Memoria Contiguo (CMA).

Una vez más los trucos anteriores no reemplazarán el controlador DMA full Scatter-Gather, zero copy, pero son útiles durante el tiempo de desarrollo o en algunas plataformas con menos rendimiento.

 0
Author: SlawekS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-14 12:57:21