Hardware Engineering · Buses and Interconnects
Device DMA Addressing and What the IOMMU Changes
How a device names memory without access to the CPU page tables, why 32-bit DMA needs bounce buffers, and how an IOMMU translates device addresses per requester.
A device that performs bus-mastering DMA puts an address on the link and expects memory to answer. It has no page table walker, no notion of a virtual address space, and no way to be interrupted with a page fault it can service. Historically it simply used physical addresses, which made the device a co-equal of the CPU on the memory bus and gave it the ability to scribble anywhere. The IOMMU exists to remove that ability without removing the performance, by interposing a translation that is indexed by the identity of the requester rather than by the address alone.
The address a device actually generates
The address in a PCIe memory request header is up to 64 bits wide; on PCI it was 32 bits unless the device used a dual address cycle, which took two clock phases to emit a 64-bit address. This is why a device’s DMA capability is a property worth checking: a device that can only generate 32-bit addresses cannot reach memory above the 4 GiB boundary at all.
That boundary is not a legacy curiosity. Firmware and hardware reserve the region below 4 GiB for MMIO apertures — the PCIe configuration space window, device BARs, and the memory-mapped registers of the platform — so a machine with a lot of RAM places most of it above 4 GiB even when it has plenty of space below. A 32-bit-only device attached to such a machine can only be given buffers from the low region, which is small and contended.
The workaround without an IOMMU is the bounce buffer: when a driver needs the device to read a buffer that lives above the boundary, the kernel copies the data down into a buffer below it and lets the device transfer from there; when the device writes a buffer that must end up above the boundary, the transfer goes into the low buffer and the kernel copies the result up. The copy costs bandwidth on the memory bus and adds latency, and the driver API has to know that the address a device will use is not the address the CPU uses for the same memory. That discrepancy, two addresses for one buffer, is the central complication of DMA, and the IOMMU is the mechanism that makes it programmable.
Translation indexed by requester
An IOMMU sits between the device and memory and translates the addresses in incoming requests. Crucially, the translation it applies depends on which device sent the request. The requester ID in the PCIe header — the bus, device, and function number described in PCIe topology and BDF enumeration — identifies the function, and the IOMMU selects a page table root for that ID. Two functions can therefore use the same address values and reach different physical memory, which is exactly what virtualisation of a device requires.
The page tables are structured like the CPU’s: a hierarchy of levels, each indexed by a portion of the device address, ending in a leaf that supplies the physical page frame and permissions. Page sizes are the same family: 4 KiB at the leaf, with large pages at upper levels so that a large mapping needs fewer levels of walk. A grant of read-only, write-only, or no access is enforced by the leaf, and a request that violates it is blocked and reported as a fault rather than being allowed to corrupt memory.
Three properties follow. First, a driver can present the device with a contiguous address range that is backed by scattered physical pages, because the IOMMU reassembles the scatter at translation time. That removes the requirement that DMA buffers be physically contiguous, which for a large transfer is the difference between a feasible allocation and an impossible one. Second, a device can be given a mapping that covers only its own buffers, so a bug in the device or its driver cannot write outside them. Third, a device can be given a mapping that makes a guest’s view of memory consistent with the guest’s own addresses, which is what allows a guest to program a passed-through device directly.
The cost is on the critical path of every DMA access. A translation whose page table entry is in the IOMMU’s cache — the IOTLB — costs a lookup comparable to a level-one cache access. A miss requires a walk through main memory, one read per level, and a four-level table means four dependent memory reads before the transfer can proceed. For a device issuing large sequential bursts, the walk is amortised over many cache lines and the impact is small; for a device issuing scattered single accesses, a walk per access can double effective latency. This is the reason hardware caches translations in the device itself.
Caching translations in the device
Address Translation Services, defined as an extension of PCIe, lets a device hold its own cache of translations and use them without asking the IOMMU each time. The device requests a translation for a range, the IOMMU responds with the resulting physical address and permissions, and the device tags the cached entry. Subsequent accesses in that range are sent with the translated address marked as already translated, so the IOMMU passes them through after checking the tag.
Correctness depends entirely on invalidation. When the driver changes a mapping, or unmaps a page that a device has cached, the IOMMU must send an invalidation request to every device that may hold the stale entry and wait for an acknowledgement before reusing the physical page. If the acknowledgement is not honoured, by a device that acknowledges without actually dropping the entry, the device continues to issue accesses to a physical page that has been reallocated, and the resulting corruption appears in an unrelated process’s data. This is why ATS is enabled only when both the device and the platform implement the invalidation protocol, and why a device that implements ATS partially is a correctness liability rather than a performance feature.
A related mechanism, the Page Request Interface, lets a device fault rather than fail: instead of holding a translation for a range that is not currently mapped, the device can request that the range be mapped, and the IOMMU or the driver can satisfy the request, after which the device retries. This enables on-demand paging of memory that the device is allowed to access, and it is the machinery behind shared virtual addressing, where a device and a process use the same address values. The Process Address Space ID field in the request header distinguishes which process’s address space a request belongs to, because one device may be working for several processes whose address spaces overlap numerically.
Mapping and unmapping from a driver
The kernel’s DMA API exists to hide the difference between a system with an IOMMU and one without. A driver does not compute device addresses itself; it asks the kernel to map a buffer, and the kernel returns the address to program into the device.
/* Map a kmalloc'd buffer for a single transfer; the return value is
the address the device must be given, not the CPU pointer. */
dma_addr_t addr = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, addr)) {
return -ENOMEM;
}
/* ... point the device at addr, start the transfer, wait for completion ... */
dma_unmap_single(dev, addr, len, DMA_TO_DEVICE);
The direction argument is not documentation: on architectures where a mapping is not coherent by default, it tells the kernel which way data will flow and therefore which caches to clean or invalidate before and after the transfer. Omitting the unmap leaks the mapping, and on a system with an IOMMU it also leaks IOVA space, so a driver that maps per transfer without unmapping eventually exhausts the address window and starts failing allocations.
For buffers shared with a device over a long period, dma_alloc_coherent gives memory that is guaranteed to be mutually visible without explicit cache maintenance, which is what descriptors and command rings need: a ring entry written by the CPU and read by the device must arrive without a flush around every field. The trade is that coherent memory may be uncached or mapped with weak attributes on some architectures, making CPU access slower than ordinary memory — acceptable for a descriptor ring that is touched rarely, wrong for a bulk data buffer that the CPU also streams through.
Where translation goes wrong
The first failure is the missing invalidation. If a mapping is torn down without invalidating the device’s cached translations, or without waiting for the acknowledgement, the device may access a page that has been returned to the allocator and handed to another owner. The symptom is corruption in an arbitrary process, and the fault appears far from the device that caused it.
The second is the address-width mismatch. A device whose DMA mask is set to 32 bits on a machine whose memory starts above 4 GiB will fail to map anything unless the driver arranges for low memory or the kernel sets up a bounce buffer. The failure is usually reported at map time as an allocation error, but a driver that ignores the error and programs the truncated address sends the device to an unrelated physical location.
The third is a stale translation surviving a device reset. Some devices reset their internal state without the platform knowing, which leaves the IOMMU’s cached state and the device’s view inconsistent. Platforms that support such devices require the driver to invalidate after reset, and a device that resets itself on error without driver involvement can produce exactly the corruption that isolation was meant to prevent.
The fourth is the interaction with virtualisation. A guest with a passed-through device must not be able to map physical pages it does not own, so the IOMMU must translate at two levels: the guest’s own page tables, and the host’s mapping of the guest. Nested translation costs two walks per miss, and the invalidation protocol must cover both levels, which is why passing a device through to a guest costs more than the same device used by the host. How the device is split so that each guest gets its own function is covered in SR-IOV and function partitioning, and the ordering rules that constrain a device’s writes once they are translated are in posted writes and ordering rules.
References
- PCI Express Base Specification (Address Translation Services) — PCI-SIG
- Intel Virtualization Technology for Directed I/O Architecture Specification — Intel Corporation
- DMA API, Linux kernel documentation — Linux kernel documentation
- Memory Systems: Cache, DRAM, Disk — Bruce Jacob, Spencer Ng, David Wang, Morgan Kaufmann