wandres.dev
DMA E IOMMU · transferencias a dispositivos

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.

⏱ 16 min

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.

🎯 Al terminar esta lección sabrás
  • Situar la DMA API de #include <linux/dma-mapping.h> como la interfaz portable.
  • Asignar memoria coherente con dma_alloc_coherent para estructuras compartidas.
  • Mapear buffers puntuales con dma_map_single y liberarlos con dma_unmap_single.
  • Interiorizar el modelo de propiedad: entre map y unmap el 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.

⚠️
dma_addr_t no es un puntero

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.

Un mapeo de DMA es un contrato de propiedad, no una traducción de direcciones

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.

⚔️ Los dos mapeos en un driver mínimo
  1. Asigna un anillo de descriptores con dma_alloc_coherent y libéralo simétricamente con dma_free_coherent en el remove.
  2. Mapea un buffer con dma_map_single(dev, buf, len, DMA_TO_DEVICE) y añade el chequeo con dma_mapping_error.
  3. Explica por qué no puedes hacer kfree(buf) antes del dma_unmap_single correspondiente.
  4. Justifica dónde colocarías dma_wmb en el camino de transmisión y qué carrera evita frente al doorbell.
  5. 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é.