Controlador de dispositivo del kernel de Linux a DMA desde un dispositivo a la memoria del espacio de usuario
linux-kernel linux-device-driver (6)
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 directa al espacio de usuario con / y / a través de una transferencia DMA"
Al leer 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 configuradoget_user_pages
y realizar unacopy_to_user
tipocopy_to_user
cuando se complete la transferencia. Esto parece un desperdicio, pedirle al Dispositivo a DMA en la memoria del kernel (que actúa como búfer) y luego transferirlo nuevamente al espacio de usuario. LDD3 p453:/* Only now is it safe to access the buffer, copy to user, etc. */
Lo que idealmente quiero es algo de memoria que:
- Puedo usar en el espacio de usuario (¿Tal vez solicite un controlador a través de una llamada ioctl para crear una memoria / buffer DMA''able?)
- Puedo obtener una dirección física desde la que pasar al dispositivo para que todo lo que el espacio de usuario tenga que hacer sea realizar una lectura en el controlador.
- el método de lectura activaría la transferencia de DMA, el bloque a la espera de la interrupción completa de DMA y liberaría la lectura del espacio de usuario posteriormente (el espacio de usuario ahora es seguro de usar / leer memoria).
¿Necesito asignaciones de transmisión de una sola página, asignación de configuración y búferes de espacio de usuario asignados con get_user_pages
dma_map_page
?
Mi código hasta ahora configura get_user_pages
en la dirección dada desde el espacio de usuario (yo lo llamo la parte de E / S directa). Luego, dma_map_page
con una página de get_user_pages
. Le doy al dispositivo el valor de retorno de dma_map_page
como la dirección de transferencia física DMA.
Estoy utilizando algunos módulos del núcleo como referencia: drivers_scsi_st.c
y drivers-net-sh_eth.c
. Miraría el código infiniband, ¡pero no puedo encontrar cuál es el más básico!
Muchas gracias de antemano.
Me estoy confundiendo con la dirección a implementar. Quiero...
Considere 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 la API de lectura / escritura tradicional suficiente? ¿El mapeo directo del dispositivo en el espacio de usuario está bien? ¿Es deseable una memoria compartida reflexiva (semi-coherente)?
La manipulación manual de los datos (lectura / escritura) es una opción bastante buena si los datos se prestan para que se entiendan bien. Usar VM de propósito general y lectura / escritura puede ser suficiente con una copia en línea. El mapeo directo de accesos no cacheables al periférico es conveniente, pero puede ser torpe. Si el acceso es el movimiento relativamente infrecuente de bloques grandes, puede tener sentido usar memoria normal, tener el pin de la unidad, traducir las direcciones, DMA y liberar las páginas. Como optimización, las páginas (tal vez enormes) pueden ser fijadas y traducidas; la unidad puede reconocer la memoria preparada y evitar las complejidades de la traducción dinámica. Si hay muchas operaciones de E / S pequeñas, hacer que la unidad funcione de forma asíncrona tiene sentido. Si la elegancia es importante, se puede usar la marca de página sucia de VM para identificar automáticamente lo que se debe mover y se puede usar una llamada (meta_sync ()) para vaciar las páginas. Tal vez una mezcla de los trabajos anteriores ...
Con demasiada frecuencia, las personas no ven 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.
Básicamente, tiene la idea correcta: en 2.1, solo puede 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.
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 kernel, asigne una matriz de búfer de tamaño adecuado de la struct page*
- user_buf_size/PAGE_SIZE
entradas - y use get_user_pages()
para obtener una lista de struct page * para el búfer del espacio de usuario.
Una vez que tenga eso, puede asignar una matriz de lista de struct scatterlist
que es del mismo tamaño que la matriz de su página y recorrer la lista de páginas haciendo sg_set_page()
. Después de configurar la lista de sg, debe hacer dma_map_sg()
en la matriz de listas 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 porque el código de asignación DMA podría fusionar las cosas).
Eso le da todas las direcciones de bus para pasar a su dispositivo, y luego puede activar el DMA y esperar por lo que desee. El esquema basado en read () que tienes probablemente esté bien.
Puede referirse a drivers / infiniband / core / umem.c, específicamente ib_umem_get()
, para obtener algún código que ib_umem_get()
este mapeo, aunque la generalidad con la que ese código debe lidiar puede hacerlo un poco confuso.
Alternativamente, si su dispositivo no maneja demasiado 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 eso. Para dar acceso al espacio de usuario a esa memoria, su controlador solo necesita implementar un método mmap
lugar del ioctl como se describe anteriormente.
En algún momento quise permitir que la aplicación de espacio de usuario asigne búferes DMA y asignarlos al espacio de usuario y obtener la dirección física para poder controlar mi dispositivo y realizar transacciones DMA (masterización de bus) completamente desde el espacio de usuario, totalmente sin pasar por el kernel de Linux. Sin embargo, he utilizado un enfoque un poco diferente. Primero comencé con un módulo de kernel mínimo que inicializaba / probaba un dispositivo PCIe y creaba un dispositivo de caracteres. Ese controlador permitió que una aplicación de espacio de usuario hiciera dos cosas:
- Asigne la barra de E / S del dispositivo PCIe al espacio de usuario utilizando la función
remap_pfn_range()
. - Asigne y libere buffers DMA, asignelos 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 la llamada mmap()
(aunque file_operations
). Uno para 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 buffers 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 prácticamente todo: controlar el dispositivo leyendo / escribiendo desde / hacia registros de E / S, asignando y liberando buffers DMA de tamaño arbitrario, y haciendo que el dispositivo realice transacciones DMA. La única parte faltante es el manejo de interrupciones. Estaba haciendo el sondeo en el espacio del usuario, quemando mi CPU y tenía las interrupciones desactivadas.
Espero eso ayude. ¡Buena suerte!
En realidad estoy trabajando exactamente en lo mismo en este momento y voy por la ruta ioctl()
. La idea general es que el espacio de usuario asigne el búfer que se usará para la transferencia DMA y se usará un ioctl()
para pasar el tamaño y la dirección de este búfer al controlador del dispositivo. El controlador luego utilizará listas de dispersión y recolección junto con la API DMA de transmisión para transferir datos directamente desde y hacia 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 en un bucle en el que el 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 se aísla dentro de una función que bloquea hasta que se completa cada transferencia (ver más abajo). Cuando todos los bytes se transfieren o la función de transferencia incremental devuelve un error, ioctl()
sale y vuelve 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 la 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(¤t->mm->mmap_sem);
ret = get_user_pages(current,
current->mm,
udata,
npages,
is_writing_to_userspace,
0,
&pages_array,
NULL);
up_read(¤t->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 todavía no lo he probado ... Así que, por favor, no trate este pseudo código como un evangelio y asegúrese de duplicar Compruebe toda la lógica y los parámetros ;-).
Vale la pena mencionar que el controlador con soporte DMA Scatter-Gather y la asignación de memoria de espacio de usuario es más eficiente y tiene el rendimiento más alto. Sin embargo, en caso de que no necesitemos un alto rendimiento o deseamos desarrollar un controlador en algunas condiciones simplificadas, podemos usar algunos trucos.
Renunciar a cero copia de diseño. Vale la pena considerar cuando el rendimiento de datos no es demasiado grande. En tal diseño, los datos se pueden copiar al usuario mediante copy_to_user(user_buffer, kernel_dma_buffer, count);
user_buffer podría ser, por ejemplo, un argumento de búfer en la implementación de llamadas al sistema read () del dispositivo de caracteres. Todavía tenemos que cuidar de la asignación kernel_dma_buffer
. Podría, por ejemplo, obtener de la memoria obtenida de dma_alloc_coherent()
.
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 DGA FPGA y no se recomienda en entornos de producción. Digamos que la PC tiene 32GB de RAM. Si agregamos mem=20GB
a la lista de parámetros de arranque del kernel, podemos usar 12GB como un enorme búfer dma contiguo. Para asignar esta memoria al espacio de usuario simplemente implemente 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 sistema operativo de 12 GB se omite por completo y solo se puede usar por el proceso que lo ha asignado a su espacio de direcciones. Podemos intentar evitarlo utilizando el Asignador de memoria contiguo (CMA).
Una vez más, los trucos anteriores no reemplazarán el controlador DMA de copia cero, Scatter-Gather completo, pero son útiles durante el tiempo de desarrollo o en algunas plataformas de menor rendimiento.
first_page_offset = udata & PAGE_MASK;
Parece mal Debería ser:
first_page_offset = udata & ~PAGE_MASK;
o
first_page_offset = udata & (PAGE_SIZE - 1)