GNU/Linux >> Tutoriales Linux >  >> Linux

Controlador de dispositivo del kernel de Linux a DMA desde un dispositivo a la memoria del espacio del usuario

Me estoy confundiendo con la dirección para implementar. Quiero...

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 pasando en el sistema?

¿Es suficiente la API de lectura/escritura tradicional? ¿Está bien el mapeo directo del dispositivo en el espacio del usuario? ¿Es deseable una memoria compartida reflexiva (semi-coherente)?

La manipulación manual de datos (lectura/escritura) es una muy buena opción si 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. El mapeo directo de accesos no almacenables en caché al periférico es conveniente, pero puede ser complicado. Si el acceso es el movimiento relativamente poco frecuente de bloques grandes, puede tener sentido usar la memoria normal, tener el pin de la unidad, traducir direcciones, DMA y liberar las páginas. Como una optimización, las páginas (quizás enormes) se pueden prefijar y traducir; la unidad entonces puede reconocer la memoria preparada y evitar las complejidades de la traducción dinámica. Si hay muchas pequeñas operaciones de E/S, tiene sentido que la unidad funcione de forma asíncrona. Si la elegancia es importante, el indicador de página sucia de VM se puede usar para identificar automáticamente lo que se debe mover y se puede usar una llamada (meta_sync()) para vaciar las páginas. Quizás una mezcla de lo anterior funcione...

Con demasiada frecuencia, las personas no miran el problema más grande antes de profundizar en los detalles. A menudo, las soluciones más simples son suficientes. Un poco de esfuerzo en la construcción de un modelo de comportamiento puede ayudar a guiar qué API es preferible.


De hecho, estoy trabajando exactamente en lo mismo en este momento y voy al ioctl() ruta. La idea general es que el espacio del usuario asigne el búfer que se usará para la transferencia DMA y un ioctl() se utilizará para pasar el tamaño y la dirección de este búfer al controlador del dispositivo. Luego, el controlador utilizará listas de dispersión y recopilación junto con la API DMA de transmisión para transferir datos directamente hacia y desde el dispositivo y el búfer del espacio del 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 fragmentos de 256k (que es el límite impuesto por el hardware para la cantidad de entradas de dispersión/recopilación que 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, el ioctl() sale y vuelve al espacio de usuario

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

Pseudocódigo para función de transferencia incremental:

/*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 controlador 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 este es solo un enfoque general, he estado trabajando en este controlador durante las últimas semanas y aún tengo que probarlo... Así que, por favor, no trate este pseudocódigo como un evangelio y asegúrese de duplicar comprobar toda la lógica y los parámetros;-).


En algún momento, quería permitir que la aplicación del espacio del usuario asignara búferes DMA y lo asignara al espacio del usuario y obtuviera la dirección física para poder controlar mi dispositivo y realizar transacciones DMA (masterización de bus) completamente desde el espacio del usuario, totalmente pasando por alto el kernel de Linux. Sin embargo, he usado un enfoque un poco diferente. Primero comencé con un módulo kernel mínimo que estaba inicializando/probando el dispositivo PCIe y creando un dispositivo de caracteres. Ese controlador luego permitió que una aplicación de espacio de usuario hiciera dos cosas:

  1. Mapee la barra de E/S del dispositivo PCIe en el espacio del usuario usando remap_pfn_range() función.
  2. Asigne y libere búferes DMA, asígnelos al espacio del usuario y pase una dirección de bus físico a la aplicación del espacio del usuario.

Básicamente, se reduce a una implementación personalizada de mmap() llamar (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 están en su lugar, la aplicación de espacio de usuario puede hacer casi 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 del usuario, quemando mi CPU y tenía las interrupciones deshabilitadas.

Espero eso ayude. ¡Buena suerte!


Básicamente tiene la idea correcta:en 2.1, puede hacer que el espacio de usuario asigne cualquier memoria antigua. Lo quieres alineado con la página, así que posix_memalign() es una API útil para usar.

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

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

Eso le da todas las direcciones de bus para pasar a su dispositivo, y luego puede activar el DMA y esperarlo como quiera. El esquema basado en lectura () que tiene probablemente esté bien.

Puede consultar drivers/infiniband/core/umem.c, específicamente ib_umem_get() , para algún código que construye este mapeo, aunque la generalidad con la que ese código debe lidiar puede hacer que sea un poco confuso.

Alternativamente, si su dispositivo no maneja muy bien las listas de dispersión/recopilación y quiere memoria contigua, puede usar get_free_pages() para asignar un búfer físicamente contiguo y usar dma_map_page() en ese. Para dar acceso al espacio de usuario a esa memoria, su controlador solo necesita implementar un mmap método en lugar de ioctl como se describe arriba.


Linux
  1. Ejecutar una función de espacio de usuario desde el espacio del kernel

  2. ¿Cómo acceder (si es posible) al espacio del kernel desde el espacio del usuario?

  3. Controlador de dispositivo IOCTL Linux

  4. Llame a una función de espacio de usuario desde un módulo del kernel de Linux

  5. ¿Cómo puedo reservar un bloque de memoria del kernel de Linux?

Cómo compilar el kernel de Linux desde cero {Guía paso a paso}

Cómo construir el kernel de Linux desde cero

Cómo SSH en su servidor Linux desde Windows

Cómo compilar el kernel de Linux desde el origen para crear un kernel personalizado

Cómo SSH en una máquina con Windows 10 desde Linux O Windows O en cualquier lugar

Segmentación de memoria de Linux