Scatter-gather y la máscara de DMA
Transferir buffers físicamente fragmentados en una sola operación con struct scatterlist, sg_table y dma_map_sgtable, incluida la fusión de segmentos que hace el IOMMU. La máscara de DMA (dma_set_mask_and_coherent) para dispositivos con límites de direccionamiento.
Los buffers reales rara vez son un bloque físico contiguo. Un buffer de usuario abarca decenas de páginas dispersas por toda la RAM; una E/S de disco reúne fragmentos de aquí y de allá. Mapear y transferir cada trozo por separado sería absurdo. El scatter-gather describe todo el buffer como una lista de segmentos y lo entrega al dispositivo en una única operación lógica. Y la máscara de DMA declara hasta qué dirección puede llegar el dispositivo, para que la API respete sus límites.
- Entender el problema de los buffers físicamente fragmentados y la solución scatter-gather.
- Construir una lista de segmentos con
struct scatterlistystruct sg_table. - Mapear la lista completa con
dma_map_sgtabley recorrer los segmentos DMA resultantes. - Declarar la capacidad de direccionamiento del dispositivo con
dma_set_mask_and_coherent.
El problema: memoria dispersa
La memoria virtual crea una ilusión de continuidad que la física no comparte. Un buffer de 256 KiB que un proceso ve contiguo puede estar repartido en 64 páginas físicas esparcidas por toda la RAM. Un dispositivo que hiciera DMA necesitaría 64 transferencias con 64 direcciones distintas. El scatter-gather (SG) resuelve esto: se describe el buffer como una lista de segmentos, cada uno un par de dirección y longitud, y el motor de DMA del dispositivo recorre la lista —gather al leer, scatter al escribir— en una sola operación.
struct scatterlist y sg_table
El kernel representa cada segmento con un struct scatterlist, y la lista completa con un struct sg_table que agrupa el array y su cuenta. Se inicializa con sg_alloc_table y se rellena apuntando cada entrada a una página con sg_set_page (o a un buffer con sg_set_buf):
#include <linux/scatterlist.h>
#include <linux/dma-mapping.h>
struct sg_table sgt;
struct scatterlist *sg;
int i, ret;
ret = sg_alloc_table(&sgt, nr_pages, GFP_KERNEL);
if (ret)
return ret;
for_each_sgtable_sg(&sgt, sg, i)
sg_set_page(sg, pages[i], PAGE_SIZE, 0); /* lado CPU: pagina, longitud, offset */
En esta fase la lista describe el lado de la CPU: qué páginas físicas componen el buffer. Todavía no hay ninguna dirección de bus; eso lo produce el mapeo.
Mapear la lista y la fusión del IOMMU
dma_map_sgtable mapea la lista entera de una vez. Es la API moderna, preferible al viejo dma_map_sg: devuelve 0 o un -errno limpio en lugar de la ambigua cuenta que devolvía su antecesor. Tras mapear, no recorres los segmentos originales, sino los segmentos DMA, que pueden ser menos, y se leen con sg_dma_address y sg_dma_len:
ret = dma_map_sgtable(dev, &sgt, DMA_TO_DEVICE, 0);
if (ret)
goto free_table;
/* recorrer los SEGMENTOS DMA: pueden ser menos que las paginas de entrada */
for_each_sgtable_dma_sg(&sgt, sg, i) {
dma_addr_t seg = sg_dma_address(sg);
unsigned int n = sg_dma_len(sg);
programar_descriptor(dev, seg, n); /* un descriptor de hardware por segmento */
}
La razón de que haya menos segmentos es preciosa: cuando hay un IOMMU (nivel 27.4), páginas físicamente dispersas pueden mapearse a IOVA contiguas, y entonces la API las fusiona (coalesce) en un solo segmento DMA. Sesenta y cuatro páginas sueltas pueden convertirse en un único segmento que el dispositivo ve como una extensión continua: un solo descriptor en lugar de sesenta y cuatro. Por eso jamás debes leer sg->length ni asumir que el número de segmentos DMA iguala al de páginas; usa siempre los descriptores DMA y la cuenta que quedó tras el mapeo.
Un scatterlist guarda por separado la descripción de la CPU (sg_page, sg->length, sg->offset) y la del dispositivo (sg_dma_address, sg_dma_len). Para programar el hardware usa siempre los accesos sg_dma_* y recorre con for_each_sgtable_dma_sg. Mezclarlos —programar sg->length en el dispositivo— parece funcionar sin IOMMU y se rompe en cuanto la fusión entra en juego.
El desmontaje es simétrico y usa la tabla original; la API sabe deshacer la fusión:
dma_unmap_sgtable(dev, &sgt, DMA_TO_DEVICE, 0);
sg_free_table(&sgt);
flowchart LR P0[Pagina fisica 3] --> IO[IOMMU] P1[Pagina fisica 9] --> IO P2[Pagina fisica 1] --> IO IO -->|un solo segmento contiguo| DEV[El dispositivo ve una extension unica]
La máscara de DMA: hasta dónde alcanza el dispositivo
No todo dispositivo puede direccionar toda la RAM. Uno legado de 32 bits no llega por encima de los 4 GiB. El dispositivo declara su alcance con la máscara de DMA, y la API la respeta: si un buffer cae fuera, recurre a un buffer de rebote o, con IOMMU, entrega una IOVA dentro del alcance. Se fija con dma_set_mask_and_coherent, que además comprueba que la plataforma puede satisfacerlo:
/* declarar 64 bits, con reserva a 32 si la plataforma no da mas */
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret) {
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
if (ret) {
dev_err(dev, "sin DMA de 32 bits utilizable\n");
return ret;
}
dev_info(dev, "limitado a DMA de 32 bits\n");
}
Si no fijas nada, el valor por defecto es 32 bits, conservador y a menudo subóptimo en máquinas con mucha RAM porque fuerza rebotes innecesarios. Declarar la máscara real es de las primeras cosas que hace un driver en su probe, y comprobar el retorno es obligatorio: en algunas plataformas ni siquiera los 32 bits están garantizados.
Cierra el nivel viendo el patrón que lo ha recorrido entero: la indirección como respuesta universal. La memoria virtual de un proceso (nivel 24) te dejó ver como contiguo un espacio que físicamente está hecho pedazos; la MMU reúne páginas dispersas bajo la ilusión de un bloque continuo. Scatter-gather con IOMMU es exactamente ese mismo truco, pero para el dispositivo. Las páginas de tu buffer están esparcidas por la RAM igual que las de un malloc, y cuando la fusión colapsa sesenta y cuatro de ellas en un único segmento DMA contiguo, estás presenciando al IOMMU hacer por la tarjeta de red lo que la MMU hace por tu programa: fabricar continuidad a partir de fragmentos mediante una tabla de traducción. El “gather” no es una optimización menor de E/S; es magia de tablas de página aplicada a los periféricos. Y una vez que ves esa simetría, todo el nivel se recompone en una sola idea: cada actor con acceso a la RAM —el proceso vía MMU, el dispositivo vía IOMMU— vive en un espacio de direcciones virtual propio, aislado y con su continuidad inventada, y la DMA API es el puente que negocia entre esos mundos. La descentralización del poder sobre la memoria que abrió el nivel 27.1 se cierra aquí con su consecuencia más elegante: el dispositivo, como el proceso, ya no ve la RAM cruda, sino una ficción cómoda y confinada construida a su medida.
- Reserva un array de páginas y constrúyeles un
sg_tableconsg_alloc_tableysg_set_page. - Fija la máscara con
dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64))y una reserva a 32 bits. - Mapea con
dma_map_sgtabley recorre confor_each_sgtable_dma_sg, imprimiendo cuántos segmentos DMA salen frente al número de páginas de entrada. - Repite con el IOMMU activo (
intel_iommu=on) y sin él, y explica por qué la cuenta de segmentos cambia. - Justifica por qué programar
sg->lengthen lugar desg_dma_len(sg)es un bug que solo se manifiesta con fusión.