Como publiqué en una actualización en mi pregunta, el problema subyacente es que la red de copia cero no funciona para la memoria que se ha asignado usando remap_pfn_range()
(que dma_mmap_coherent()
pasa a usar debajo del capó también). La razón es que este tipo de memoria (con el VM_PFNMAP
flag set) no tiene metadatos en forma de struct page*
asociado con cada página, que necesita.
Entonces, la solución es asignar la memoria de manera que struct page*
s son asociado con la memoria.
El flujo de trabajo que ahora me funciona para asignar la memoria es:
- Utilice
struct page* page = alloc_pages(GFP_USER, page_order);
para asignar un bloque de memoria física contigua, donde el número de páginas contiguas que se asignarán viene dado por2**page_order
. - Divida la página de orden superior/compuesta en páginas de orden 0 llamando a
split_page(page, page_order);
. Esto ahora significa questruct page* page
se ha convertido en una matriz con2**page_order
entradas.
Ahora, para enviar dicha región a la DMA (para la recepción de datos):
dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
dmaengine_submit(dma_desc);
Cuando recibimos una devolución de llamada del DMA de que la transferencia ha finalizado, debemos desasignar la región para transferir la propiedad de este bloque de memoria nuevamente a la CPU, que se encarga de los cachés para asegurarse de que no estemos leyendo datos obsoletos:
dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);
Ahora, cuando queremos implementar mmap()
, todo lo que tenemos que hacer es llamar a vm_insert_page()
repetidamente para todas las páginas de orden 0 que asignamos previamente:
static int my_mmap(struct file *file, struct vm_area_struct *vma) {
int res;
...
for (i = 0; i < 2**page_order; ++i) {
if ((res = vm_insert_page(vma, vma->vm_start + i*PAGE_SIZE, &page[i])) < 0) {
break;
}
}
vma->vm_flags |= VM_LOCKED | VM_DONTCOPY | VM_DONTEXPAND | VM_DENYWRITE;
...
return res;
}
Cuando el archivo esté cerrado, no olvide liberar las páginas:
for (i = 0; i < 2**page_order; ++i) {
__free_page(&dev->shm[i].pages[i]);
}
Implementando mmap()
de esta manera ahora permite que un socket use este búfer para sendmsg()
con el MSG_ZEROCOPY
bandera.
Aunque esto funciona, hay dos cosas que no me sientan bien con este enfoque:
- Solo puede asignar búferes de potencia de 2 con este método, aunque podría implementar lógica para llamar a
alloc_pages
tantas veces como sea necesario con órdenes decrecientes para obtener un búfer de cualquier tamaño formado por sub-búferes de diferentes tamaños. Esto requerirá algo de lógica para unir estos búferes en elmmap()
y para DMA con scatter-gather (sg
) llama en lugar desingle
. split_page()
dice en su documentación:
* Note: this is probably too low level an operation for use in drivers.
* Please consult with lkml before using this in your driver.
Estos problemas se resolverían fácilmente si hubiera alguna interfaz en el núcleo para asignar una cantidad arbitraria de páginas físicas contiguas. No sé por qué no lo hay, pero no encuentro los problemas anteriores tan importantes como para investigar por qué esto no está disponible / cómo implementarlo :-)
Quizás esto te ayude a comprender por qué alloc_pages requiere un número de página potencia de 2.
Para optimizar el proceso de asignación de páginas (y disminuir las fragmentaciones externas), que se realiza con frecuencia, el kernel de Linux desarrolló un caché de página por CPU y un asignador de amigos para asignar memoria (hay otro asignador, losa, para servir asignaciones de memoria que son más pequeñas que un página).
La caché de página por CPU atiende la solicitud de asignación de una página, mientras que el asignador de amigos mantiene 11 listas, cada una de las cuales contiene 2^{0-10} páginas físicas respectivamente. Estas listas funcionan bien cuando asignan y liberan páginas y, por supuesto, la premisa es que está solicitando un búfer del tamaño de una potencia de 2.