Direcciones y coherencia: virtual, física y de bus
Los tres espacios de direcciones que conviven en un DMA —virtual del kernel, física y de bus/DMA— y por qué difieren en muchas plataformas (offset, IOMMU, buffers de rebote). La coherencia de caché y por qué un mapeo streaming puede exigir sincronización explícita.
La DMA API te oculta un hecho incómodo: en una misma máquina conviven tres sistemas de direcciones para el mismo byte de memoria, y no siempre coinciden. La CPU lo ve con una dirección virtual, el controlador de memoria con una física, y el dispositivo con una de bus. Además, en muchas plataformas la caché de la CPU y la RAM pueden divergir cuando un dispositivo escribe sin avisar. Este nivel abre la caja negra para que entiendas qué hace dma_map por debajo.
- Separar con claridad las direcciones virtual del kernel, física y de bus/DMA.
- Conocer las tres razones por las que la de bus difiere de la física: offset, IOMMU y rebote.
- Entender la coherencia de caché y las operaciones de limpieza e invalidación.
- Usar
dma_sync_single_for_cpuydma_sync_single_for_deviceen un buffer aún mapeado.
Tres espacios de direcciones para un mismo byte
Una misma página de RAM tiene, simultáneamente, tres nombres. La dirección virtual del kernel es la que devuelve kmalloc; la resuelve la MMU de la CPU. La dirección física es la que entiende el controlador de memoria; la obtienes con virt_to_phys. La dirección de bus o de DMA es la que el dispositivo emite en el bus; la produce la DMA API y vive en un dma_addr_t.
void *va = kmalloc(len, GFP_KERNEL); /* virtual del kernel */
phys_addr_t pa = virt_to_phys(va); /* fisica: la ve el controlador de memoria */
dma_addr_t da = dma_map_single(dev, va, len, DMA_TO_DEVICE); /* de bus: la ve el dispositivo */
flowchart LR VA[Virtual del kernel] -->|MMU de la CPU| PA[Fisica] PA -->|offset o IOMMU o rebote| BA[De bus o DMA] BA -->|la emite el dispositivo| DEV[Dispositivo]
Virtual del kernel
La que devuelve kmalloc y usa la CPU; la resuelve la MMU por tablas de página. Es la única que puedes desreferenciar en código del kernel.
Física
La que ve el controlador de memoria, sin cachés ni traducción de dispositivo. virt_to_phys la da, pero programarla en el dispositivo es un error.
De bus o DMA
La que el dispositivo emite en el bus, guardada en dma_addr_t. La produce la DMA API tras aplicar offset, IOMMU o rebote.
El drama de todo el nivel es que estas tres coinciden lo justo para que un atajo incorrecto parezca correcto en tu portátil y estalle en el hardware de otro. Un puntero del kernel y su física comparten valor bajo el mapa lineal; la física y la de bus comparten valor cuando no hay ni offset ni IOMMU ni rebote. Apoyarse en esas coincidencias es la fuente inagotable de bugs de DMA no portables.
Por qué la de bus difiere de la física
En un x86 de escritorio sin IOMMU activo la dirección de bus suele igualar a la física, y por eso el error de programar virt_to_phys a veces “funciona”. Pero hay tres motivos frecuentes por los que divergen, y cualquiera basta para romper ese atajo.
- Desplazamiento de bus (offset): en muchos SoC la ventana con que el dispositivo ve la RAM está corrida respecto a la física. El kernel lo modela en
dev->dma_range_map, y la dirección de bus es la física menos ese offset. Un valor crudovirt_to_physcaería fuera de la ventana del dispositivo. - IOMMU: si hay una unidad de traducción de E/S (nivel 27.4), la dirección de bus es una IOVA completamente virtual que el IOMMU traduce a física mediante tablas de página propias. Puede ser cualquier valor, sin relación aritmética con la física.
- Buffers de rebote (bounce, swiotlb): si el dispositivo no puede direccionar la página original —por ejemplo un dispositivo de 32 bits y un buffer por encima de los 4 GiB—, el kernel copia los datos a un buffer bajo y devuelve la dirección de ese buffer. El dispositivo nunca toca tu memoria original.
if (dma_mapping_error(dev, da)) /* con rebote o IOMMU, mapear puede fallar */
return -ENOMEM;
El rebote resucitó con fuerza en la computación confidencial. En una VM cifrada con AMD SEV-SNP o Intel TDX, el dispositivo del anfitrión no puede leer la memoria cifrada del huésped, así que todo DMA debe pasar por un buffer de rebote en memoria compartida no cifrada. Se activa con swiotlb=force y explica por qué la E/S en enclaves paga una copia extra: la confidencialidad de la RAM y el acceso directo del dispositivo son, por diseño, incompatibles sin ese intermediario.
Coherencia de caché: cuándo el hardware no te cubre
Segundo problema, independiente de las direcciones. La CPU no lee la RAM directamente: pasa por sus cachés. Si el dispositivo escribe la RAM por su cuenta, ¿ve la CPU el dato nuevo o una copia rancia en caché? Depende de la arquitectura.
En x86 y en muchos SoC modernos el DMA es coherente: la interconexión vigila (snoops) las cachés de la CPU, y el hardware mantiene ambos mundos sincronizados sin que tú hagas nada. En muchos ARM y sistemas embebidos el DMA no es coherente: el dispositivo accede a la RAM saltándose la caché, y aparecen dos peligros simétricos.
- Antes de
DMA_TO_DEVICE: la CPU escribió datos que quizá siguen sucios en caché, sin bajar a RAM. Hay que limpiar (clean/flush) la caché para que el dispositivo lea lo actual. - Antes de
DMA_FROM_DEVICE: el dispositivo escribió RAM, pero la CPU aún tiene líneas cacheadas del contenido viejo. Hay que invalidar esas líneas para que la CPU relea de RAM.
Esto no es teoría: es la razón de que un mapeo streaming lleve dirección. dma_map_single realiza la limpieza o invalidación inicial correcta según ese argumento; equivocar la dirección corrompe datos de forma intermitente solo en plataformas no coherentes, un bug que nunca reproducirás en tu x86.
Ahora entiendes el otro lado de dma_alloc_coherent: en plataformas no coherentes suele entregar memoria no cacheada para evitar la sincronización, y leerla o escribirla desde la CPU es mucho más lento que la RAM normal. Por eso los anillos de descriptores —pequeños y tocados por ambos— van en coherente, pero un buffer de datos grande va en streaming, que respeta la caché y solo paga la limpieza en los bordes. Si necesitas control fino existe dma_alloc_noncoherent, que te da memoria cacheada y te obliga a sincronizarla tú.
Sincronizar un mapeo streaming
A veces necesitas mirar o tocar un buffer que sigue mapeado, entre dos transferencias, sin desmapearlo. Como la propiedad es del dispositivo, debes pedirla y devolverla explícitamente con las funciones de sincronización, que ejecutan la limpieza o invalidación necesaria en plataformas no coherentes:
/* buffer mapeado de forma persistente; alternar la propiedad CPU <-> dispositivo */
dma_sync_single_for_cpu(dev, da, len, DMA_FROM_DEVICE); /* invalida caché: ya puedo leer */
consumir(va); /* la CPU es dueña ahora */
dma_sync_single_for_device(dev, da, len, DMA_FROM_DEVICE);/* devuelve el buffer al dispositivo */
En una plataforma coherente estas llamadas son casi gratis; en una no coherente emiten la operación de caché justa. Escríbelas siempre: como con las barreras del nivel 18, tu código debe ser correcto en el modelo débil aunque tu hardware de pruebas te perdone omitirlas.
Un último matiz sobre el origen del buffer. dma_map_single exige un puntero del mapa lineal del kernel, así que no sirve para memoria que no esté ahí —como una página de usuario o de highmem—. Para esas hay dma_map_page, que parte de un struct page, un offset y una longitud, y devuelve igualmente una dma_addr_t:
/* mapear una página cualquiera, aunque no esté en el mapa lineal del kernel */
dma_addr_t da = dma_map_page(dev, page, offset, len, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, da))
return -ENOMEM;
/* ...transferencia... */
dma_unmap_page(dev, da, len, DMA_FROM_DEVICE);
Es la base sobre la que se construye el scatter-gather del nivel 27.5: una lista de páginas mapeadas de golpe.
Aquí se revela por qué la DMA API es una de las piezas de portabilidad más infravaloradas del kernel. Un dma_addr_t es, en teoría de abstracciones, una leaky abstraction: promete ser “una dirección” pero por debajo puede ser una física idéntica, una física menos un offset, una IOVA inventada por un IOMMU, o el índice de un buffer de rebote en memoria no cifrada. Cada una exige un tratamiento distinto de caché, de fallo y de límites. Y sin embargo, el mismo driver, sin un solo #ifdef, corre correcto sobre un x86 coherente con direcciones de bus idénticas a las físicas y sobre un SoC ARM no coherente con offset de bus y un SMMU delante. ¿Cómo? Porque el driver nunca calcula una dirección: le pide a la plataforma “dale al dispositivo una vista válida de esta memoria y mantén las cachés honestas”, y la plataforma rellena los detalles sucios detrás de map, sync y unmap. Ese cambio de verbo —de computar a solicitar— es la esencia de toda buena abstracción de sistemas: expones la intención y ocultas la mecánica. Cuando entiendes que dma_map_single no te devuelve un número sino que negocia un contrato con la memoria, la caché y el IOMMU a la vez, dejas de ver la DMA API como una molestia burocrática y empiezas a verla como lo que es: el traductor universal que permite que un solo cuerpo de código de driver sobreviva a la brutal diversidad del hardware real.
- En un módulo, imprime
va,virt_to_phys(va)y ladma_addr_tde un mismo buffer y compáralos en tu máquina. - Arranca con
swiotlb=forcey observa endmesgcómo se reserva el buffer de rebote; explica por qué ahora ladma_addr_tno se parece a la física. - Describe qué dato corrupto verías si en una plataforma no coherente olvidaras el
dma_sync_single_for_cpuantes de leer un bufferDMA_FROM_DEVICE. - Justifica por qué la memoria coherente de
dma_alloc_coherentno necesita estas sincronizaciones.