Envío TCP de espacio de usuario de copia cero de memoria asignada dma_mmap_coherent ()

14

Estoy ejecutando Linux 5.1 en un Cyclone V SoC, que es un FPGA con dos núcleos ARMv7 en un chip. Mi objetivo es reunir muchos datos de una interfaz externa y transmitir (parte de) estos datos a través de un socket TCP. El desafío aquí es que la velocidad de datos es muy alta y podría llegar a saturar la interfaz GbE. Tengo una implementación funcional que solo usa write()llamadas al socket, pero alcanza un máximo de 55MB / s; aproximadamente la mitad del límite teórico de GbE. Ahora estoy tratando de hacer que la transmisión TCP de copia cero funcione para aumentar el rendimiento, pero estoy golpeando una pared.

Para obtener los datos del FPGA en el espacio de usuario de Linux, he escrito un controlador de kernel. Este controlador utiliza un bloque DMA en el FPGA para copiar una gran cantidad de datos desde una interfaz externa en la memoria DDR3 conectada a los núcleos ARMv7. El controlador asigna esta memoria como un montón de búferes contiguos de 1 MB cuando se prueba dma_alloc_coherent()con GFP_USER, y los expone a la aplicación de espacio de usuario implementando mmap()en un archivo /dev/y devolviendo una dirección a la aplicación usando dma_mmap_coherent()los búferes preasignados.

Hasta aquí todo bien; la aplicación de espacio de usuario está viendo datos válidos y el rendimiento es más que suficiente a> 360 MB / s con espacio de sobra (la interfaz externa no es lo suficientemente rápida como para ver realmente cuál es el límite superior).

Para implementar la red TCP de copia cero, mi primer enfoque fue usar SO_ZEROCOPYen el socket:

sent_bytes = send(fd, buf, len, MSG_ZEROCOPY);
if (sent_bytes < 0) {
    perror("send");
    return -1;
}

Sin embargo, esto da como resultado send: Bad address.

Después de buscar en Google por un momento, mi segundo enfoque fue usar una tubería y splice()seguido de vmsplice():

ssize_t sent_bytes;
int pipes[2];
struct iovec iov = {
    .iov_base = buf,
    .iov_len = len
};

pipe(pipes);

sent_bytes = vmsplice(pipes[1], &iov, 1, 0);
if (sent_bytes < 0) {
    perror("vmsplice");
    return -1;
}
sent_bytes = splice(pipes[0], 0, fd, 0, sent_bytes, SPLICE_F_MOVE);
if (sent_bytes < 0) {
    perror("splice");
    return -1;
}

Sin embargo, el resultado es el mismo: vmsplice: Bad address.

Tenga en cuenta que si se sustituye la llamada a vmsplice()o send()a una función que sólo imprime los datos apuntado por buf(o send() sin MSG_ZEROCOPY ), todo funciona muy bien; entonces los datos son accesibles para el espacio de usuario, pero las llamadas vmsplice()/ send(..., MSG_ZEROCOPY)parecen incapaces de manejarlo.

¿Que me estoy perdiendo aqui? ¿Hay alguna forma de usar el envío TCP de copia cero con una dirección de espacio de usuario obtenida de un controlador de kernel dma_mmap_coherent()? ¿Hay otro enfoque que pueda usar?

ACTUALIZAR

Así que me sumergí un poco más en la sendmsg() MSG_ZEROCOPYruta del núcleo, y la llamada que finalmente falla es get_user_pages_fast(). Esta llamada regresa -EFAULTporque check_vma_flags()encuentra la VM_PFNMAPbandera establecida en vma. Aparentemente, este indicador se establece cuando las páginas se asignan al espacio de usuario utilizando remap_pfn_range()o dma_mmap_coherent(). Mi próximo enfoque es encontrar otra forma de acceder a mmapestas páginas.

movimiento rápido del ojo
fuente

Respuestas:

8

Como publiqué en una actualización en mi pregunta, el problema subyacente es que la red zerocopy no funciona para la memoria que se ha mapeado usando remap_pfn_range()(que también se dma_mmap_coherent()usa debajo del capó). La razón es que este tipo de memoria (con el VM_PFNMAPindicador establecido) no tiene metadatos en forma de struct page*asociados con cada página, que necesita.

La solución es, entonces, para asignar la memoria de una manera que struct page*s se asocian con la memoria.

El flujo de trabajo que ahora funciona para mí para asignar la memoria es:

  1. Utilícelo struct page* page = alloc_pages(GFP_USER, page_order);para asignar un bloque de memoria física contigua, donde se da el número de páginas contiguas que se asignarán 2**page_order.
  2. Divida la página de alto orden / compuesta en páginas de orden 0 llamando split_page(page, page_order);. Esto ahora significa que se struct page* pageha convertido en una matriz con 2**page_orderentradas.

Ahora para enviar dicha región a la DMA (para la recepción de datos):

  1. dma_addr = dma_map_page(dev, page, 0, length, DMA_FROM_DEVICE);
  2. dma_desc = dmaengine_prep_slave_single(dma_chan, dma_addr, length, DMA_DEV_TO_MEM, 0);
  3. 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 a la CPU, que se encarga de los cachés para asegurarse de que no estamos leyendo datos obsoletos:

  1. dma_unmap_page(dev, dma_addr, length, DMA_FROM_DEVICE);

Ahora, cuando queremos implementar mmap(), todo lo que realmente tenemos que hacer es llamar vm_insert_page()repetidamente a 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 olvides liberar las páginas:

for (i = 0; i < 2**page_order; ++i) {
    __free_page(&dev->shm[i].pages[i]);
}

Implementar de mmap()esta manera ahora permite que un socket use este búfer sendmsg()con la MSG_ZEROCOPYbandera.

Aunque esto funciona, hay dos cosas que no me sientan bien con este enfoque:

  • Solo puede asignar búferes de tamaño de potencia de 2 con este método, aunque puede implementar la lógica para llamar alloc_pagestantas 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á un poco de lógica para unir estos búferes en el mmap()DMA y con sgllamadas scatter-collect ( ) en lugar de single.
  • 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 :-)

movimiento rápido del ojo
fuente
2

Quizás esto lo ayude a comprender por qué alloc_pages requiere un número de página de potencia de 2.

Para optimizar el proceso de asignación de páginas (y disminuir las fragmentaciones externas), que se utiliza con frecuencia, el kernel de Linux desarrolló caché de página por CPU y buddy-allocator para asignar memoria (hay otro asignador, losa, para servir asignaciones de memoria que son más pequeñas que una página).

El caché de página por cpu sirve la solicitud de asignación de una página, mientras que buddy-allocator mantiene 11 listas, cada una con 2 ^ {0-10} páginas físicas respectivamente. Estas listas funcionan bien cuando se asignan y liberan páginas, y por supuesto, la premisa es que está solicitando un búfer de tamaño 2.

medivh
fuente