La DMA API: memoria coherente frente a streaming
Las dos familias de mapeo de la DMA API portable: dma_alloc_coherent para estructuras compartidas de larga vida entre CPU y dispositivo, y dma_map_single/dma_unmap_single para transferencias puntuales. El tipo dma_addr_t y el modelo de propiedad del buffer.
Ya sabes que el dispositivo necesita una dma_addr_t, no un puntero del kernel. La DMA API es la fábrica portable de esas direcciones, y ofrece dos productos con filosofías opuestas: memoria coherente, que CPU y dispositivo comparten permanentemente sin ceremonias, y mapeos streaming, que prestan un buffer al dispositivo para una sola transferencia y lo recuperan. Elegir bien entre ambos define la eficiencia y la corrección de todo driver.
- Situar la DMA API de
#include <linux/dma-mapping.h>como la interfaz portable. - Asignar memoria coherente con
dma_alloc_coherentpara estructuras compartidas. - Mapear buffers puntuales con
dma_map_singley liberarlos condma_unmap_single. - Interiorizar el modelo de propiedad: entre
mapyunmapel buffer es del dispositivo.
La DMA API portable
Toda la interfaz vive en #include <linux/dma-mapping.h> y gira en torno a un struct device, que representa al dispositivo y lleva colgadas sus restricciones: máscara de direcciones, desplazamiento de bus y si hay un IOMMU detrás. Cada función toma ese dev como primer argumento porque la respuesta correcta —qué dirección devolver, si hace falta rebote, si hay que tocar la caché— depende del dispositivo concreto. El resultado siempre incluye una dma_addr_t: la dirección que el dispositivo usará.
Memoria coherente: dma_alloc_coherent
dma_alloc_coherent asigna un buffer que CPU y dispositivo pueden mirar a la vez sin gestión manual de caché. Es el mapeo para estructuras de larga vida que ambos tocan constantemente: los anillos de descriptores de una tarjeta de red, buzones de comandos, bloques de control. Devuelve un puntero virtual del kernel (para que la CPU lo rellene) y, por referencia, la dma_addr_t correspondiente (para programar el dispositivo).
struct nic_ring {
struct nic_desc *desc; /* la CPU escribe descriptores aqui */
dma_addr_t desc_dma; /* la direccion que lee el dispositivo */
size_t bytes;
};
static int ring_alloc(struct device *dev, struct nic_ring *r, unsigned int n)
{
r->bytes = n * sizeof(struct nic_desc);
r->desc = dma_alloc_coherent(dev, r->bytes, &r->desc_dma, GFP_KERNEL);
if (!r->desc)
return -ENOMEM;
return 0;
}
static void ring_free(struct device *dev, struct nic_ring *r)
{
dma_free_coherent(dev, r->bytes, r->desc, r->desc_dma);
}
Coherente significa que no necesitas sincronizar cachés, pero no significa que no necesites ordenar. Si escribes un descriptor y luego tocas el “timbre” (doorbell) que despierta al dispositivo, hace falta una barrera para que las escrituras del descriptor sean visibles antes de la del timbre. Para eso está dma_wmb, primo de las barreras del nivel 18 orientado a memoria compartida con dispositivos:
r->desc[i].addr = cpu_to_le64(pkt_dma);
r->desc[i].len = cpu_to_le32(pkt_len);
dma_wmb(); /* los descriptores, visibles antes del timbre */
writel(i + 1, nic->regs + REG_TAIL); /* doorbell: el dispositivo empieza a leer */
DMA en streaming: dma_map_single
Un mapeo streaming presta un buffer ya existente al dispositivo para una transferencia concreta. No asigna memoria: toma tu puntero y devuelve una dma_addr_t válida mientras dure el mapeo. Es lo idóneo para datos transitorios y grandes: un paquete de red, un buffer de E/S de bloques. La dirección de la transferencia —DMA_TO_DEVICE, DMA_FROM_DEVICE o DMA_BIDIRECTIONAL— le dice a la API qué sincronización de caché aplicar (nivel 27.3).
dma_addr_t h = dma_map_single(dev, skb->data, skb->len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, h)) /* comprobar SIEMPRE el resultado */
return -ENOMEM;
arrancar_dma(nic, h, skb->len); /* a partir de aqui el buffer es del dispositivo */
/* ...la CPU NO debe tocar skb->data mientras esté mapeado... */
/* en la interrupcion de fin de transmision: */
dma_unmap_single(dev, h, skb->len, DMA_TO_DEVICE);
dev_kfree_skb(skb);
Nunca asumas que dma_map_single no puede fallar: con un IOMMU puede agotarse el espacio de IOVA, y con rebote puede faltar buffer bajo. Por eso dma_mapping_error es obligatorio. Y fíjate en la simetría: cada map exige su unmap con el mismo tamaño y dirección, igual que cada kmalloc exige su kfree.
flowchart LR A[CPU rellena el buffer] --> B[dma_map_single] B --> C[El dispositivo posee el buffer] C --> D[dma_unmap_single] D --> E[La CPU recupera el buffer]
Coherente o streaming: cómo elegir
Coherente
Larga vida, tamaño pequeño, acceso frecuente por ambos lados. Anillos de descriptores, buzones. Sin sincronización de caché, pero puede ser memoria no cacheada y por tanto lenta para la CPU. Se asigna una vez y se libera al desmontar el driver.
Streaming
Transferencias puntuales de buffers que ya existen. Paquetes, bloques de disco. Se mapea y desmapea alrededor de cada transferencia; respeta la caché de la CPU y colabora con el IOMMU y el rebote. Óptimo para el camino rápido de datos.
La regla práctica: coherente para el metadato de control, streaming para la carga útil. El anillo de descriptores de una NIC vive en memoria coherente durante toda la vida del driver; cada paquete que ese anillo apunta se mapea en streaming justo para su transferencia y se desmapea al completarse. Confundir los papeles —mapear en streaming un anillo que se toca miles de veces por segundo, o dejar coherente un buffer gigante de un solo uso— es un error de rendimiento clásico.
Repítelo hasta que sea reflejo: una dma_addr_t no se desreferencia desde la CPU. Para tocar el contenido usas el puntero virtual (el que devolvió dma_alloc_coherent, o el original que pasaste a dma_map_single). La dma_addr_t solo se escribe en los registros y descriptores del dispositivo. Tratarla como puntero del kernel compila y luego corrompe memoria.
El nombre “map” engaña: sugiere una simple conversión de direcciones, cuando lo que de verdad establece es una transferencia de propiedad con un contrato estricto en el tiempo. Piénsalo con los ojos del nivel 14 de Rust: entre dma_map_single y dma_unmap_single, el buffer está prestado al dispositivo de forma exclusiva. Tocarlo desde la CPU en ese intervalo es exactamente tan grave como un data race o un use-after-free: la caché de la CPU y la RAM pueden divergir, el dispositivo puede estar escribiendo justo esos bytes, y ninguna ley del lenguaje te protege porque C no modela ese préstamo. La DMA API es un sistema de ownership dibujado a mano, sin verificador que lo imponga: tú eres el borrow checker. Y el contrato esconde mucho más que una dirección: bajo el capó, map puede asignar una IOVA en el IOMMU, reservar un buffer de rebote y copiar tus datos allí, o programar una limpieza de caché; unmap deshace todo eso, y omitirlo no es solo una fuga de memoria sino, con IOMMU, una ventana de seguridad abierta (nivel 27.4). Cuando dejas de leer dma_map_single como “dame una dirección” y empiezas a leerlo como “cedo este buffer al dispositivo hasta que lo reclame”, los bugs de DMA más sutiles —cachés obsoletas, corrupciones intermitentes bajo carga— dejan de ser misterios y se vuelven violaciones evidentes del préstamo.
- Asigna un anillo de descriptores con
dma_alloc_coherenty libéralo simétricamente condma_free_coherenten elremove. - Mapea un buffer con
dma_map_single(dev, buf, len, DMA_TO_DEVICE)y añade el chequeo condma_mapping_error. - Explica por qué no puedes hacer
kfree(buf)antes deldma_unmap_singlecorrespondiente. - Justifica dónde colocarías
dma_wmben el camino de transmisión y qué carrera evita frente al doorbell. - Decide, para un buffer de firmware de 2 MB que se sube una sola vez al arrancar, si usarías coherente o streaming, y por qué.