COMP 3150Operating Systems

01 Introduction

Lesson 11.1 presented the I/O subsystem's hardware and its three data-transfer mechanisms. Programmed I/O transfers each byte or word with the processor's own load and store instructions, so the processor is occupied for the entire transfer. Interrupt-driven I/O frees the processor during the device's operation, but the processor still moves the data, and each transfer costs an interrupt. Direct memory access (DMA) removes the processor from the data path entirely: a separate DMA engine transfers the data between the device and memory, and the processor is involved only at the transfer's setup and its completion.

The saving is enormous. A 1 MB transfer at 100 MB/s takes 10 ms. If the processor moved the data with programmed I/O, it would spend those 10 ms executing load and store instructions — millions of cycles that could have been spent on the application's work. With DMA, the processor spends a few hundred cycles setting up the transfer and a few hundred handling its completion, and the remaining cycles are available for other work. The observation is why DMA is used by every high-throughput device: hard disks, solid-state drives, network adapters, graphics cards, and the high-speed buses that connect them.

DMA's design has three parts. The first is the DMA engine — the hardware that performs the transfer. It may be a separate chip (the classical 8237 DMA controller) or, on modern systems, a part of the device's own controller (the bus master DMA engine in an NVMe SSD, a network adapter, or a graphics card). The second is the transfer's description — the addresses, the length, and the direction that the processor writes to the DMA engine (or to a descriptor in memory) to start the transfer. The third is the address translation — the mapping from the buffer's virtual address to the physical addresses that the DMA engine uses, which the operating system handles with contiguous allocation, scatter-gather lists, or an IOMMU.

This lesson presents DMA in detail. It defines DMA and explains why it exists. It presents the DMA controller and the two kinds of DMA engines (the external controller and the bus-mastering device controller). It presents the DMA transfer cycle — setup, transfer, and completion — and the descriptors and descriptor rings that modern devices use to describe their transfers. It presents the physical-address problem and the three solutions: contiguous allocation (the bounce buffer), scatter-gather DMA, and the IOMMU. It presents cache coherency — the interaction between DMA and the processor's caches — and the distinction between coherent and streaming DMA mappings. It presents DMA in the Linux kernel, the NVMe queue pairs that exploit DMA's parallelism, and DMA's role in virtualization. It closes with the DMA's latency and throughput characteristics and the modern devices that push them to their limits.

Prerequisites

This lesson builds on Lessons 11.1 and 11.2. The I/O subsystem's layers, the device controller and its registers, the three data-transfer mechanisms, and the interrupt mechanism are all assumed. The virtual-memory concepts from Unit 9 — the page table, the TLB, and the distinction between virtual and physical addresses (Lessons 9.1–9.3) — are the basis for the physical-address problem, and the memory hierarchy from Lesson 8.1 is the basis for the cache-coherency discussion. No new operating-system concepts are introduced; the lesson is about the DMA mechanism and the kernel's handling of it. Familiarity with the Linux command line from Lesson 2.2 is helpful, since the interactive activity uses standard Unix tools.

02 Learning objectives

By the end of this lesson you will be able to

  • Define direct memory access and explain why it exists, contrasting it with programmed I/O and interrupt-driven I/O.
  • Describe the DMA engine and distinguish between an external DMA controller and a bus-mastering device controller.
  • Describe the DMA transfer cycle and explain the role of descriptors and descriptor rings.
  • Explain the physical-address problem and compare the three solutions: contiguous allocation, scatter-gather lists, and the IOMMU.
  • Explain cache coherency in the context of DMA and distinguish between coherent and streaming DMA mappings.
  • Describe how the Linux kernel's DMA API is used and explain how NVMe's queue pairs exploit DMA, and how the IOMMU supports virtualization.

This lesson contributes to U11.3 and U11.4, and to course learning outcomes CLO1, CLO5, and CLO6.

03 Required reading

  • Operating System Concepts — Chapter 12, I/O Systems, Section 12.2.4, Direct Memory Access, and Section 12.7, Performance. The standard treatment of the DMA mechanism, the DMA controller, the transfer cycle, and the performance implications.
  • OSTEP — Chapter 36, I/O Devices. OSTEP's treatment of DMA as the third data-transfer mechanism, and its discussion of the DMA engine and the transfer's setup and completion, is the clearest available account, and it is the basis for the worked example in this lesson.
  • Tanenbaum & Bos — Modern Operating Systems, Chapter 5, Section 5.1.3, Direct Memory Access. A complementary treatment that develops the DMA controller and the transfer cycle from first principles.
  • Corbet, J., Rubini, A., & Kroah-Hartman, G. — Linux Device Drivers, 3rd ed. O'Reilly, 2005. Chapter 15, Memory Mapping and DMA. The Linux kernel's DMA API: the coherent and streaming mappings, the DMA pools, the scatter-gather mappings, and the cache-coherency rules.
  • Bovet, D. P., & Cesati, M. — Understanding the Linux Kernel, 3rd ed. O'Reilly, 2005. Chapter 13, I/O Architecture and Device Drivers. The Linux kernel's DMA structures and the bus-mastering device model.
  • Love, R. — Linux Kernel Development, 3rd ed., Chapter 14, Block Devices I/O, and Chapter 16, The Block I/O Layer. The kernel's block layer and the request queue that the DMA engine consumes.
  • Intel Corporation. — Intel Virtualization Technology for Directed I/O Architecture Specification. The authoritative reference for the IOMMU and its role in DMA translation and device isolation.
  • Linux man pages: man 5 proc (for /proc/interrupts and the /sys interfaces), man 1 lspci, man 8 ethtool, man 1 iostat, man 1 perf, man 8 smartctl, man 5 vfio (if available), man 1 nvme.

Reading strategy

Read the OSTEP chapter first. It develops DMA from the canonical device protocol and shows step by step how the operating system sets up the transfer, how the DMA engine performs it, and how the completion interrupt is handled. Then read the Silberschatz section for the formal mechanism — the DMA controller, the transfer cycle, the cycle stealing — and the Tanenbaum section for the hardware's perspective. The Linux kernel chapters (Corbet et al. and Bovet & Cesati) are the practical view: they show how the kernel's DMA API is used, why the coherent and streaming mappings are separate, and how the cache-coherency rules affect the driver's code. The Intel VT-d specification is the authoritative reference for the IOMMU, and it is worth consulting for the translation's details and for the isolation guarantees. Finally, the Linux man pages are the practical tools: lspci shows the device's DMA capabilities, ethtool shows the network adapter's scatter-gather and checksum offload capabilities, and /proc/interrupts shows the completion interrupts. Pay particular attention to the distinction between the DMA engine (the hardware that performs the transfer) and the DMA mapping (the operating system's translation of the buffer's virtual address to the addresses the engine uses). The engine is the device's concern; the mapping is the kernel's, and the two must agree for the transfer to succeed.

04 Core content

4.1 What DMA is and why it exists

Direct memory access (DMA) is the mechanism by which a device transfers data to or from memory without the processor's per-byte involvement. The processor sets up the transfer — it writes the memory address, the length, and the direction to the DMA engine — and the engine performs the transfer, reading from or writing to memory directly. When the transfer is complete, the engine raises an interrupt, and the processor handles the completion.

The mechanism's purpose is to free the processor from the data movement. The alternative — programmed I/O — requires the processor to execute a load or a store for every byte or word, and the processor's time is proportional to the amount of data transferred. For a device that transfers megabytes per second, the processor's time is enormous: a 1 GB transfer at 1 GB/s takes one second of device time, but the processor's per-byte involvement would consume hundreds of millions of cycles — most of the processor's capacity for that second. DMA reduces the processor's involvement to a setup and a completion, and the remaining cycles are available for the application's work.

The saving's size depends on the device's rate and the processor's speed:

The processor's involvement for a 1 MB transfer
Mechanism Processor's involvement Cycles (approx.) Notes
Programmed I/O Every byte or word 250,000,000 One load/store per 4-byte word, plus loop overhead
Interrupt-driven One interrupt per block ~25,000,000 One interrupt per 4 KB block, plus the per-byte move
DMA Setup and completion only ~1,000 A few hundred cycles for the setup, a few hundred for the interrupt

The table is illustrative — the actual numbers depend on the processor, the bus, and the device — but the pattern is what matters. DMA reduces the processor's involvement by five orders of magnitude for a 1 MB transfer, and the reduction grows with the transfer's size. The observation is why DMA is used by every high-throughput device: the processor's time is too valuable to spend on moving bytes.

4.2 The DMA engine and bus mastering

The DMA engine is the hardware that performs the transfer. Its design has two forms:

External DMA controller (third-party DMA)
A separate chip that sits between the devices and memory. The classical example is the Intel 8237 DMA controller, which was used in the IBM PC and its successors. The processor programs the controller with the transfer's parameters, and the controller performs the transfer by arbitrating with the processor for the memory bus. The controller is shared among the devices, and each device has a DMA channel through which it requests the controller's service. External DMA controllers are still used for the low-bandwidth devices (the legacy floppy controller, the parallel port, some embedded systems), but they have been replaced by the bus-mastering devices for the high-bandwidth ones.
Bus-mastering device controller (first-party DMA)
The device's own controller contains the DMA engine, and the controller is the bus master: it arbitrates with the processor for the memory bus directly, without an external controller's mediation. The classical examples are the SCSI controller, the ATA controller, and the modern NVMe SSD, network adapter, and graphics card. The bus-mastering design has three advantages: it scales with the number of devices (each device has its own engine, so the engines do not contend for a shared controller), it is faster (the device's engine can be optimized for the device's characteristics), and it is more flexible (the engine can implement descriptor-based transfers, scatter-gather, and other features that a shared controller cannot). Modern systems use bus-mastering devices almost exclusively.

The two designs differ in the bus master — the component that controls the bus during the transfer. In third-party DMA, the external controller is the master; in first-party DMA, the device's controller is the master. The distinction matters because the bus master determines the transfer's capabilities: a bus-mastering device can perform descriptor-based and scatter-gather transfers, while an external controller's transfers are typically limited to a single contiguous buffer.

The bus master's role also determines the transfer's relationship to the processor. While the DMA engine is the bus master, the processor cannot use the bus; the engine's accesses and the processor's accesses are interleaved by the bus's arbitration protocol. On modern systems, the interleaving is efficient: the memory controller and the bus's protocol allow the DMA engine and the processor to share the memory's bandwidth with little contention. On older systems, the DMA engine's accesses stole cycles from the processor — the condition known as cycle stealing — and the processor's execution slowed during the transfer. The modern systems' arbitration has largely eliminated the problem, but it is worth understanding: a DMA transfer consumes memory bandwidth, and the bandwidth is a shared resource.

4.3 The DMA transfer cycle

The DMA transfer cycle has three phases: setup, transfer, and completion.

Setup. The processor's driver prepares the transfer:

  1. Allocate the buffer. The driver allocates a memory buffer for the transfer (or uses a buffer that the file system or the network stack has already allocated).
  2. Map the buffer. The driver translates the buffer's virtual address to the physical addresses that the DMA engine uses. The mapping may be a single contiguous range (if the buffer is physically contiguous) or a list of ranges (a scatter-gather list). The mapping's details are the subject of sections 4.4 and 4.5.
  3. Write the descriptor. The driver writes the transfer's description — the source or destination addresses, the length, the direction, and the device's control bits — to the DMA engine's registers or to a descriptor in memory.
  4. Start the transfer. The driver writes the engine's "start" bit (or, for a descriptor-based device, the doorbell register), and the engine begins the transfer.

Transfer. The DMA engine performs the transfer. It reads the descriptor (if it is in memory), then reads or writes the data one word at a time, arbitrating with the processor for the memory bus. The engine's accesses are physical: it does not use the processor's MMU, so it cannot fault, cannot be preempted, and cannot follow a page table. The constraint is the source of the physical-address problem.

Completion. When the transfer is complete, the engine updates its status register and raises an interrupt. The driver's handler (the top half, Lesson 11.2) reads the status register, confirms the transfer's success, unmaps the buffer (releasing the physical addresses' reservation), and schedules the deferred work (the bottom half) that processes the completed request.

The three phases' durations differ enormously. The setup and completion take a few hundred to a few thousand cycles each, and the transfer takes as long as the data's movement requires. For a 1 MB transfer at 1 GB/s, the transfer takes about 1 ms (about 3 million cycles at 3 GHz), while the setup and completion together take about 2,000 cycles. The observation is why the DMA's overhead is negligible for large transfers and significant for small ones: a 64-byte transfer's setup and completion may take longer than the transfer itself, and the driver may prefer programmed I/O for the smallest transfers (Lesson 11.1, section 4.4).

4.4 The physical-address problem

The physical-address problem is the mismatch between the addresses that the processor's programs use and the addresses that the DMA engine uses. A program's buffer is specified by a virtual address, which the processor's MMU translates to a physical address through the page table. A DMA engine, however, does not use the processor's MMU: it addresses memory by physical address, and it cannot fault, cannot be preempted, and cannot follow a page table. The engine can therefore transfer only to or from the physical addresses that the driver has programmed, and the driver must supply them.

The translation is complicated by two facts:

  • The buffer's virtual pages may be scattered across physical memory. A 64 KB buffer may occupy 16 physically non-contiguous 4 KB pages, because the virtual-memory system does not guarantee that a process's pages are physically contiguous. A DMA engine that expects a single contiguous range cannot transfer the buffer directly.
  • The buffer's pages may be moved or evicted during the transfer. The virtual-memory system may page out a page, migrate it for compaction or NUMA balancing, or map a different page in its place. A DMA engine that is transferring to or from the page would then read or write the wrong memory. The driver must therefore pin the buffer's pages — mark them as unmovable and unevictable — for the transfer's duration.

The operating system solves the problem in one of three ways, each with its own cost and its own applicability: contiguous allocation, scatter-gather DMA, and the IOMMU.

4.5 The three solutions

Contiguous allocation (the bounce buffer)
The kernel allocates a physically contiguous buffer for the transfer, copies the data between the buffer and the program's scattered pages, and performs the DMA on the contiguous buffer. For a write (memory to device), the kernel copies the program's data into the bounce buffer before the transfer; for a read (device to memory), the kernel copies the buffer's data into the program's pages after the transfer. The approach works for any device — even one that supports only a single contiguous range — but it costs an extra copy, which is significant for high-bandwidth transfers. For a 1 GB/s transfer, the extra copy consumes memory bandwidth and the processor's time, and it can halve the effective throughput. The approach is used as a fallback: the kernel uses it when the device cannot handle a scatter-gather list, or when the buffer's addresses are outside the range that the device can address (a DMA zone constraint on some 32-bit devices).
Scatter-gather DMA
The device's DMA engine accepts a scatter-gather list (SG list), a list of (physical address, length) pairs that describe the buffer's pages. The engine reads the list and transfers each page separately, so no copy is needed. The approach is the standard for modern storage and network controllers: an NVMe SSD, a SATA controller with AHCI, a SCSI controller, and a modern network adapter all support scatter-gather. The engine's descriptor format varies — the AHCI controller uses a command list with a scatter- gather table, the NVMe controller uses a command with a physical region page (PRP) list or a scatter-gather list, the network adapter uses a ring of buffer descriptors — but the principle is the same: the driver describes the buffer's pages, and the engine transfers them without a copy.
IOMMU (I/O memory management unit)
The IOMMU is a hardware component that translates the device's addresses the way the processor's MMU translates the program's addresses. The kernel programs the IOMMU with a mapping from the device's address space to physical memory, and the device uses virtual addresses (from its own perspective) that the IOMMU translates. The device can then transfer to or from a buffer whose pages are scattered, because the IOMMU performs the translation that the engine cannot. The approach has three advantages: it removes the need for a contiguous buffer (the device sees a contiguous address space even when the physical memory is scattered), it removes the need for a bounce buffer (the device can address any of the system's memory), and it provides isolation (a device can access only the memory that the kernel has mapped for it, so a buggy or malicious device cannot corrupt arbitrary memory). The IOMMU is standard on modern x86 systems (Intel VT-d, AMD Vi) and on ARM systems (the SMMU), and it is essential for virtualization (section 4.9).

The three solutions are not mutually exclusive. A modern system uses the IOMMU where it is available, scatter-gather lists where the device supports them, and bounce buffers as a fallback. The driver's choice depends on the device's capabilities, the buffer's characteristics, and the kernel's configuration.

The DMA transfer cycle and the three address-mapping solutions The diagram has two parts. The top part shows the DMA transfer cycle: the CPU and the device are connected to the system bus; the CPU programs the DMA engine, the DMA engine reads the descriptors from memory and transfers the data between the device and memory, and the engine raises an interrupt when the transfer is complete. The bottom part compares the three address-mapping solutions: contiguous allocation uses a bounce buffer and a copy; scatter-gather DMA describes the scattered pages in a list; and the IOMMU translates the device's addresses to physical memory and provides isolation. THE DMA TRANSFER CYCLE CPU sets up the transfer Memory descriptors + data buffer (virtual pages, physical scatter) DMA engine reads descriptors transfers data Device data source or destination (controller is the bus master) System bus (PCI Express, memory controller) 1 Setup The driver allocates the buffer, maps it to physical addresses, writes the descriptor, and starts the engine. 2 Transfer The engine reads the descriptor and moves the data between the device and memory. The CPU is free. 3 Completion The engine updates its status and raises an interrupt. The driver's handler unmaps the buffer and finishes. THE THREE ADDRESS-MAPPING SOLUTIONS Contiguous (bounce buffer) page 1 page 2 page 3 The program's pages are scattered. copy one contiguous bounce buffer Works for any device. Costs an extra copy of the data. Used as a fallback when the device cannot handle a scatter-gather list. Scatter-gather list Scatter-gather list [phys 0x1A00, 4 KB] [phys 0x3C00, 4 KB] [phys 0x0F00, 4 KB] DMA engine transfers each page No extra copy. The standard for modern storage and network controllers (AHCI, NVMe). IOMMU Device uses virtual address IOMMU translates physical memory (scattered) No bounce buffer, no SG list needed. Provides isolation. Standard on modern x86 (VT-d, AMD-Vi) and ARM (SMMU). Essential for virtualization and device passthrough.
Figure 11.3 — The DMA transfer cycle and the three address-mapping solutions. The top diagram shows the transfer's three phases: the driver sets up the transfer, the DMA engine performs it, and the engine raises an interrupt when it is complete. The bottom diagram compares the three ways the operating system handles the physical-address problem: contiguous allocation (a bounce buffer and an extra copy), scatter-gather DMA (a list of the buffer's physical pages), and the IOMMU (a hardware translation that removes the need for both).

4.6 Cache coherency and DMA

The DMA engine and the processor both access memory, and both may cache the data they access. The processor's caches (L1, L2, L3) hold copies of the memory's contents, and the DMA engine's accesses may not be visible to them. The result is a cache-coherency problem with two directions:

  • The device writes memory, but the processor's cache holds a stale copy. If the processor has recently read the buffer, its cache may hold the buffer's old contents. The DMA engine writes the new contents to memory, but the processor's subsequent read is served from the cache, and it sees the old contents. The processor must invalidate the cache lines before reading the new data.
  • The processor writes memory, but the DMA engine reads the old contents. If the processor has recently written the buffer, its write may still be in the cache and not yet in memory. The DMA engine reads the memory and sees the old contents. The processor must flush (write back) the cache lines before the transfer begins.

The two operations are the cache maintenance that the driver must perform, and their necessity depends on the hardware's cache-coherency model:

Hardware-coherent DMA
The hardware maintains the caches' coherency with the DMA engine's accesses. The bus's protocol snoops the processor's caches during the DMA transfer: when the engine writes memory, the protocol invalidates the corresponding cache lines; when the engine reads memory, the protocol flushes the processor's dirty cache lines first. The hardware's coherency means the driver does not need to perform any cache maintenance, and the DMA transfer's correctness is automatic. x86 systems are hardware-coherent for the devices on the PCIe bus, and most modern ARM systems are coherent for the devices on the coherent interconnect (the CCI or CCN). The coherence is a property of the bus and the platform, not of the device.
Software-managed coherency
The hardware does not maintain the caches' coherency, and the driver must perform the cache maintenance explicitly. The driver invalidates the cache lines before reading the device's data and flushes the cache lines before the device reads the processor's data. The software-managed approach is used by some embedded systems, by some ARM systems without a coherent interconnect, and by some devices (typically the older or the simpler ones) on otherwise-coherent systems. The maintenance's cost is the cache operations' overhead, and its correctness requires the driver to get the operations' order and extent right — a subtle and error-prone task.

The Linux kernel's DMA API abstracts the two models. The driver uses the same API regardless of the hardware's coherency, and the kernel's implementation performs the cache maintenance when it is needed:

Coherent DMA mappings
A mapping that the driver creates once and uses repeatedly, with the guarantee that the processor and the device always see the same data. The kernel allocates the buffer (through dma_alloc_coherent) and maps it in a way that is coherent on all platforms: on a hardware-coherent system, the mapping is a simple address translation; on a software-managed system, the kernel allocates the buffer from a region that is not cached (or manages the cache explicitly). Coherent mappings are used for the long-lived structures that the driver and the device share — the descriptor rings, the command queues, the status blocks — where the coherence's guarantee is essential and the buffer's reuse justifies the allocation's cost.
Streaming DMA mappings
A mapping that the driver creates for a single transfer and destroys when the transfer is complete. The driver maps the buffer (through dma_map_single for a single range or dma_map_sg for a scatter-gather list), performs the transfer, and unmaps the buffer (through dma_unmap_single or dma_unmap_sg). The kernel performs the cache maintenance at the map and unmap operations: on a hardware-coherent system, the operations are mostly no-ops; on a software-managed system, the kernel flushes or invalidates the cache lines as needed. Streaming mappings are used for the transient buffers that the driver passes to the device — the file system's pages, the network stack's packet buffers — where the buffer's lifetime is short and the coherence's guarantee is needed only for the transfer's duration.

The distinction between the two mappings is the driver author's most important DMA decision. A coherent mapping's buffer is always safe to access from the processor and the device, but its allocation is expensive and its region may be limited (the coherent pool is a fixed-size region, and its size is a boot-time parameter). A streaming mapping's buffer is cheap to create, but the driver must not access it between the map and the unmap (the access would race with the device), and the driver must perform the mapping and unmapping in the correct order. A driver that uses the wrong kind of mapping — a coherent mapping for a transient buffer, a streaming mapping for a long-lived structure — is a driver with poor performance or subtle bugs.

4.7 DMA descriptors and ring buffers

A modern high-performance device does not perform one transfer at a time; it processes a stream of transfers, and it needs a way to describe them to the DMA engine and to report their completion. The mechanism is the descriptor ring (or queue): a circular array of descriptors in memory, each describing one transfer, with a pair of producer and consumer indices that the driver and the device use to coordinate.

The ring's operation is a classic producer-consumer protocol:

  1. The driver writes a descriptor. The driver fills the next free descriptor with the transfer's parameters: the buffer's physical addresses (or the scatter-gather list's address), the length, the operation (read or write), and any device-specific flags. The driver then advances the producer index.
  2. The driver notifies the device. The driver writes to the device's doorbell register (or, for NVMe, to the submission queue's tail doorbell), which tells the device that new descriptors are available.
  3. The device reads the descriptors. The device's DMA engine reads the descriptors from the ring (using DMA, of course), processes the transfers, and advances the consumer index. It may process several descriptors in one batch, and it may use its own scheduling to order them.
  4. The device reports the completion. The device writes a completion entry to a separate ring (the completion queue) and, if the driver has requested it, raises an interrupt. The completion entry identifies the completed transfer and reports its status.
  5. The driver processes the completions. The driver's bottom half (Lesson 11.2) reads the completion entries, frees the buffers, and completes the requests. It advances the ring's consumer index (or, for NVMe, the completion queue's head doorbell), which tells the device that the entries have been processed and the slots can be reused.

The ring's design has three important properties. First, it is lock-free (or nearly so): the driver and the device each own one index, and the coordination is through the indices' values rather than through locks. Second, it is batched: the device can process several descriptors in one pass, and the driver can submit several descriptors before notifying the device, so the per-transfer overhead is amortized. Third, it is parallel: a device can have several rings (one per queue, one per processor core), and the driver can submit to different rings from different cores without contention. The parallelism is the source of the modern devices' throughput: an NVMe SSD with 64 queues, each with 1,024 entries, can have 65,536 transfers in flight at once, and the driver can submit them from 64 cores concurrently.

The NVMe protocol's queue pairs are the canonical modern example. Each queue pair consists of a submission queue (the driver writes the commands) and a completion queue (the device writes the completions), and each queue pair is associated with a separate interrupt (an MSI-X vector, Lesson 11.2). The driver can create one queue pair per processor core, submit the I/O requests from the core that originated them, and receive the completions on the same core — the per-core queue design that eliminates the cross-core synchronization that a single queue would require. The design is the reason NVMe SSDs achieve millions of IOPS with low latency, and it is the reason the storage stack has been redesigned around it.

4.8 DMA and virtualization

The IOMMU's translation has a second role beyond the physical-address problem: it provides isolation. A device's DMA engine can access only the memory that the kernel has mapped for it in the IOMMU, so a buggy or malicious device cannot corrupt arbitrary memory. The isolation is the basis for the modern virtualization features:

  • Device passthrough. A virtual machine can be given direct access to a physical device (a network adapter, a storage controller), and the IOMMU translates the device's DMA to the virtual machine's memory. The virtual machine's driver talks to the device directly, without the hypervisor's mediation, and the device's performance is near-native. The IOMMU's isolation ensures that the device can access only the virtual machine's memory, not the hypervisor's or another virtual machine's.
  • SR-IOV (Single Root I/O Virtualization). A physical device can present several virtual functions, each of which can be assigned to a different virtual machine. Each virtual function has its own DMA engine and its own IOMMU mapping, so the virtual machines' traffic is isolated and the device's performance is shared.
  • VFIO (Virtual Function I/O). The Linux framework that exposes a device's IOMMU mapping to user space, so a user-space driver (a virtual machine's emulator, a DPDK application) can program the device's DMA directly. VFIO is the basis for the high-performance user-space I/O frameworks, and it relies on the IOMMU for its safety.

Without the IOMMU, the device passthrough would be unsafe: a device could DMA to any physical address, including the hypervisor's memory, and a malicious virtual machine could use the device to attack the host. The IOMMU's isolation is what makes the modern virtualization's performance possible, and it is one of the reasons the IOMMU is standard on modern systems.

4.9 DMA latency and throughput

DMA's performance characteristics are its latency and its throughput:

  • The setup and completion latency. The driver's setup takes a few hundred to a few thousand cycles (the buffer's allocation, the mapping, the descriptor's write, and the doorbell's write). The completion's handling takes a similar time (the interrupt, the status read, the buffer's unmap, and the deferred work's scheduling). For a small transfer, the setup and completion's latency dominates the transfer's cost, and the driver may prefer programmed I/O or interrupt-driven I/O. For a large transfer, the latency is negligible.
  • The transfer's throughput. The engine's throughput is limited by the device's rate, the bus's bandwidth, and the memory's bandwidth. The three are usually matched: a PCIe 4.0 ×4 device can transfer about 8 GB/s, the memory's bandwidth is tens of gigabytes per second, and the device's own rate (an NVMe SSD's 7 GB/s, a 100 Gb/s network adapter's 12.5 GB/s) determines which one is the bottleneck. The engine's descriptor processing adds overhead — the engine must read each descriptor, translate its addresses (if an IOMMU is present), and update its status — so the effective throughput is lower than the bus's raw bandwidth. The overhead is small for large descriptors (a 1 MB transfer's descriptor is processed once) and significant for small ones (a 4 KB transfer's descriptor is processed once, and the descriptor's processing may take longer than the transfer).
  • The memory bandwidth's contention. The DMA engine's accesses and the processor's accesses share the memory's bandwidth. A high-throughput DMA transfer can consume a significant fraction of the memory's bandwidth, and the processor's accesses may slow as a result. The contention is usually small on modern systems (the memory's bandwidth is much larger than the devices' aggregate rate), but it can become significant on a system with many high-speed devices (a server with several NVMe SSDs and a 100 Gb/s network adapter), and the system's design must account for it.

The characteristics explain the modern stack's design. The large transfers are the DMA's domain, and the drivers use large descriptors, scatter-gather lists, and descriptor rings to amortize the per-transfer overhead. The small transfers are the processor's domain, and the drivers use programmed I/O or interrupt-driven I/O for them. The boundary between the two is a tunable parameter, and the drivers expose it (the network adapter's tx-usecs and rx-usecs coalescing parameters, the block layer's max_sectors_kb) so the administrator can tune it for the workload.

4.10 Common misconceptions

"DMA is a device."

DMA is a mechanism, not a device. The mechanism's hardware is the DMA engine, which may be a separate chip (the classical 8237) or a part of the device's own controller (the modern bus-mastering device). The engine's location determines the mechanism's capabilities, but the mechanism itself — the transfer of data without the processor's per-byte involvement — is the same.

"DMA is always faster than programmed I/O."

DMA frees the processor, but it has a setup and a completion cost. For a small transfer, the setup and completion can exceed the transfer's cost, and programmed I/O (or interrupt-driven I/O) is faster. The choice depends on the transfer's size, and the driver's threshold is a tunable parameter.

"The DMA engine can use virtual addresses."

A classical DMA engine uses physical addresses only: it does not use the processor's MMU, cannot fault, and cannot follow a page table. The constraint is the physical-address problem, and the operating system solves it with contiguous allocation, scatter-gather lists, or an IOMMU. An engine behind an IOMMU can use the device's virtual addresses, but the addresses are the IOMMU's, not the processor's.

"Cache coherency is automatic."

Coherency depends on the hardware. x86 systems and most modern ARM systems are hardware-coherent for their PCIe devices, but some embedded systems and some devices are not. The Linux kernel's DMA API abstracts the difference, but the driver must use the API correctly: a coherent mapping for the long-lived structures, a streaming mapping for the transient buffers.

"A bounce buffer is always necessary."

A bounce buffer is a fallback. Modern devices support scatter-gather lists, and modern systems have an IOMMU, so the bounce buffer is used only when the device cannot handle a scatter-gather list or when the buffer's addresses are outside the device's addressable range. The bounce buffer's extra copy is a performance cost, and the modern stack avoids it wherever possible.

"The IOMMU is only for virtualization."

The IOMMU's translation also solves the physical-address problem for the native (non- virtualized) system: a device can transfer to or from a buffer whose pages are scattered, because the IOMMU performs the translation. The translation's isolation is what makes the virtualization features possible, but the translation itself is useful on any system.

"DMA transfers are instantaneous."

A DMA transfer takes as long as the data's movement requires: a 1 MB transfer at 1 GB/s takes about 1 ms, and a 1 GB transfer takes about a second. The engine's accesses consume memory bandwidth, and the processor's accesses may slow during the transfer. The transfer's duration is not zero, and the driver's design must account for it.

"The descriptor ring is a queue."

The descriptor ring is a circular array, not a queue: the driver and the device each own one index, and the ring's slots are reused as the indices advance. The distinction matters because the ring's capacity is fixed (the number of descriptors it holds), and the driver must not overwrite a descriptor that the device has not yet processed. The producer-consumer protocol is the ring's coordination mechanism, and the doorbell is its notification.

05 Worked example

The following example traces a 1 MB read from an NVMe SSD through the DMA machinery, computes the transfer's cost, compares the three address-mapping solutions, and shows how the descriptor ring's design affects the device's throughput.

Tracing a 1 MB read from an NVMe SSD

Consider a program that reads 1 MB from a file on an NVMe SSD. The file system's block size is 4 KB, so the read spans 256 blocks. The SSD's controller uses bus-mastering DMA with a descriptor ring, and the system has an IOMMU.

  1. The application calls read(fd, buf, 1048576). The kernel's VFS layer receives the call, looks up the file's inode, and determines that the read spans 256 blocks.
  2. The VFS checks the page cache. If the blocks are cached, the read is served from memory. If not, the VFS submits a read request to the block layer.
  3. The block layer builds the request. The block layer merges the 256 blocks into a single request (the blocks are contiguous on the file system) and passes it to the NVMe driver.
  4. The driver maps the buffers. The driver maps the request's pages for DMA. The pages are in the page cache, and they are physically scattered (the page cache does not guarantee physical contiguity). The driver calls dma_map_sg to build a scatter-gather list: the kernel translates each page's virtual address to its physical address (or, with the IOMMU, to the device's address), pins the pages, and returns the list.
  5. The driver writes the command. The driver fills the next free slot in the NVMe submission queue with a read command: the starting logical block, the number of blocks, the scatter-gather list's address, and the command's identifier. The driver advances the queue's tail index and writes to the queue's tail doorbell register.
  6. The SSD's controller reads the command. The controller's DMA engine reads the command from the submission queue (in memory) and the scatter-gather list's entries. It then reads the requested blocks from the flash chips into the scatter-gather list's pages — one page at a time, using its DMA engine — and advances the queue's head index.
  7. The controller writes the completion. When the 256 blocks are transferred, the controller writes a completion entry to the completion queue and raises an MSI-X interrupt (Lesson 11.2).
  8. The driver's top half runs. The interrupt handler reads the completion entry, confirms the command's success, and schedules the bottom half.
  9. The driver's bottom half runs. The bottom half processes the completion: it unmaps the scatter-gather list (dma_unmap_sg), releases the pages' pinning, updates the page cache, and completes the request. The waiting process is woken.
  10. The application's read returns. The 1 MB is in the application's buffer, and the read returns.

The trace shows the DMA machinery in action: the driver sets up the transfer (the mapping and the command), the engine performs it (the command's read and the data's transfer), and the completion interrupt finishes it. The processor is involved only at the setup and the completion; the transfer itself is the engine's work.

The transfer's cost

The 1 MB transfer's cost has five components:

Component Time Notes
Driver setup ~5 µs The scatter-gather mapping, the command's write, the doorbell
Command's transfer ~1 µs The controller reads the command and the SG list from memory
Data transfer ~143 µs 1 MB at 7 GB/s (the SSD's sequential rate)
Completion's interrupt ~2 µs The interrupt's delivery and the handler's entry
Driver completion ~5 µs The unmap, the page cache's update, the process's wakeup
Total ~156 µs Dominated by the data's transfer

The table is illustrative — the actual numbers depend on the SSD, the bus, and the kernel — but the pattern is what matters: the data's transfer dominates, and the DMA's setup and completion are a small fraction. The observation is the reason DMA is the right choice for large transfers: the per-transfer overhead is amortized over the data's transfer, and the processor is free for the duration.

The comparison with the alternatives makes the saving concrete. If the driver used programmed I/O, the processor would execute about 250,000,000 cycles moving the 1 MB (one load/store per 4-byte word, plus the loop's overhead), and the transfer would take the processor's full attention for the 143 µs of the data's transfer. With DMA, the processor's involvement is about 10 µs (the setup and the completion), and the remaining 146 µs are available for other work. The difference is the reason DMA is used by every high-throughput device.

Comparing the address-mapping solutions

Suppose the buffer's pages are physically scattered, as they usually are. The three solutions handle the scatter differently:

Solution Extra copies Driver overhead Applicability
Contiguous (bounce buffer) 2 (in and out) ~50 µs Devices without scatter-gather support; buffers outside the device's addressable range
Scatter-gather list 0 ~5 µs Modern storage and network controllers
IOMMU 0 ~3 µs Modern x86 and ARM systems; essential for virtualization

The bounce buffer's extra copies are its principal disadvantage. For a 1 MB transfer, the two copies consume about 50 µs of the processor's time and the memory's bandwidth, and they can halve the effective throughput. The scatter-gather list and the IOMMU avoid the copies, and their driver overhead is small. The observation is why the modern stack prefers them: the copies are the bottleneck, and the modern devices and systems can avoid them.

The scatter-gather list and the IOMMU differ in their requirements. The scatter-gather list requires the device to support the list's format (the AHCI controller's command list, the NVMe controller's PRP list or SGL), and it does not provide isolation. The IOMMU requires the system to have the hardware (the Intel VT-d, the AMD Vi, or the ARM SMMU), and it provides both the translation and the isolation. A system with both uses the scatter-gather list where the device supports it and the IOMMU for the translation and the isolation; a system with neither falls back to the bounce buffer.

The descriptor ring's effect on throughput

The descriptor ring's design determines the device's throughput for the small transfers. Suppose the SSD performs 4 KB reads (the database's typical access). Each read requires one command in the submission queue, one transfer of 4 KB, and one completion entry. The per-command overhead is:

Component Time Notes
Driver setup ~2 µs The 4 KB page's mapping, the command's write, the doorbell
Data transfer ~0.6 µs 4 KB at 7 GB/s
Completion ~2 µs The interrupt's delivery, the handler, the unmap
Total ~4.6 µs Dominated by the per-command overhead

The per-command overhead dominates the 4 KB transfer, and the device's throughput is limited to about 217,000 IOPS (1 ÷ 4.6 µs). The limitation is not the SSD's flash (which can perform millions of reads per second) but the software's per-command overhead.

The descriptor ring's design reduces the overhead in three ways. First, it batches: the driver can write several commands before ringing the doorbell, so the doorbell's cost is amortized. Second, it parallelizes: the device can have several queue pairs, and the driver can submit from several cores concurrently, so the per-core overhead is multiplied by the number of cores. Third, it eliminates the interrupt's cost: the driver can disable the completion's interrupt and poll the completion queue, so the interrupt's overhead is avoided for the high-throughput workloads. The three techniques together are the reason an NVMe SSD can achieve millions of IOPS with a modest processor, and they are the design's central contributions.

Observing DMA on a live system

The Linux tools let an administrator observe the DMA's configuration and its effect on the system's performance.

# Show the PCI devices' DMA capabilities
lspci -vv 2>/dev/null | grep -E 'DMA|MSI|Capabilities' | head -30

# The output shows whether the device supports bus-mastering
# DMA, scatter-gather, MSI/MSI-X, and 64-bit addressing.

# Show the network adapter's DMA and offload capabilities
sudo ethtool -k eth0 2>/dev/null | grep -E 'scatter|dma|sg'

# Show the block device's queue parameters
cat /sys/block/nvme0n1/queue/max_sectors_kb
cat /sys/block/nvme0n1/queue/max_segments
cat /sys/block/nvme0n1/queue/nr_requests
cat /sys/block/nvme0n1/queue/scheduler

# The max_segments parameter is the maximum number of
# scatter-gather entries the device supports.

# Show the IOMMU's status
dmesg | grep -iE 'iommu|vt-d|dmar' | head -20

# Show the system's DMA-related kernel parameters
cat /proc/cmdline | tr ' ' '\n' | grep -iE 'iommu|dma'

The commands' outputs show the DMA's configuration: whether the device supports bus-mastering DMA and scatter-gather, the maximum number of scatter- gather entries, the IOMMU's status, and the kernel's DMA-related parameters. The max_segments parameter is particularly important: it determines the largest number of pages the device can transfer in one command, and the driver must respect it when it builds the scatter-gather list.

06 Interactive activity

Activity · explore DMA on a live system

The goal of this activity is to use the Linux command line to inspect the system's DMA capabilities, to measure the effect of the DMA's configuration, and to observe the descriptor ring's behaviour. Work through the steps in your course VM.

  1. Inventory the system's DMA-capable devices. Use lspci to list the devices and their DMA capabilities.
    # List the PCI devices and their capabilities
    lspci -v 2>/dev/null | grep -E '^[0-9a-f]|Capabilities|DMA' | head -40
    
    # Show the devices that support bus-mastering DMA
    lspci -vv 2>/dev/null | grep -B1 'BusMaster' | head -20
    Which devices support bus-mastering DMA? Which support 64-bit addressing? Which support MSI or MSI-X? Write down what you find.
  2. Examine a device's DMA capabilities. Use lspci -vv to examine a specific device's DMA configuration.
    # Find the NVMe or SATA controller's PCI address
    lspci | grep -iE 'nvme|sata|raid'
    
    # Show its configuration (replace 00:1f.2 with the
    # device's address from the previous output)
    lspci -vv -s 00:1f.2 2>/dev/null | grep -A5 -E 'Capabilities|Control'
    What DMA-related capabilities does the device have? Does it support 64-bit addressing? What interrupt mechanism does it use (legacy, MSI, or MSI-X)? How many MSI-X vectors does it support?
  3. Examine the block device's DMA parameters. Use the sysfs interfaces to see the block device's queue parameters.
    # Identify the block devices
    lsblk -d -o NAME,SIZE,ROTA,TRAN,MODEL
    
    # Show the NVMe or SATA device's queue parameters
    cat /sys/block/nvme0n1/queue/max_sectors_kb
    cat /sys/block/nvme0n1/queue/max_segments
    cat /sys/block/nvme0n1/queue/max_segment_size
    cat /sys/block/nvme0n1/queue/nr_requests
    cat /sys/block/nvme0n1/queue/rotational
    cat /sys/block/nvme0n1/queue/scheduler
    What is the maximum number of scatter-gather segments the device supports? What is the maximum transfer size? What is the maximum number of requests in the queue? How do the parameters relate to the device's DMA capabilities?
  4. Check the IOMMU's status. Use dmesg and the kernel's parameters to see whether the IOMMU is enabled.
    # Show the IOMMU-related kernel messages
    dmesg | grep -iE 'iommu|vt-d|dmar|smmu' | head -20
    
    # Show the kernel's IOMMU-related parameters
    cat /proc/cmdline | tr ' ' '\n' | grep -iE 'iommu|dma'
    
    # Check whether the IOMMU groups are present
    ls /sys/kernel/iommu_groups/ 2>/dev/null | head -10
    
    # Show the IOMMU groups and their devices
    for g in /sys/kernel/iommu_groups/*/devices/*; do
        echo "$g: $(lspci -s $(basename $g) 2>/dev/null)"
    done 2>/dev/null | head -10
    Is the IOMMU enabled? How many IOMMU groups are there? Which devices are in which groups? What does the grouping tell you about the devices' isolation?
  5. Measure the transfer size's effect. Use dd with different block sizes to measure the effect of the transfer's size on the throughput.
    # Create a large file
    dd if=/dev/zero of=/tmp/dma-test.bin bs=1M count=500 2>/dev/null
    sync
    echo 3 | sudo tee /proc/sys/vm/drop_caches
    
    # Measure the throughput at different block sizes
    for bs in 4k 16k 64k 256k 1M; do
        echo "=== bs=$bs ==="
        dd if=/tmp/dma-test.bin of=/dev/null bs=$bs iflag=direct 2>&1 | tail -1
    done
    
    # Clean up
    rm /tmp/dma-test.bin
    How does the throughput change with the block size? At which block size does the throughput saturate? Explain the result in terms of the per-transfer overhead and the DMA's setup cost.
  6. Observe the interrupt rate at different transfer sizes. Use /proc/interrupts to measure the interrupt rate for the large and small transfers.
    # Record the counts before the small transfers
    grep -E 'nvme|ahci' /proc/interrupts
    
    # Small transfers
    dd if=/dev/zero of=/tmp/small.bin bs=4k count=10000 iflag=direct 2>/dev/null
    grep -E 'nvme|ahci' /proc/interrupts
    
    # Large transfers
    dd if=/dev/zero of=/tmp/large.bin bs=1M count=100 iflag=direct 2>/dev/null
    grep -E 'nvme|ahci' /proc/interrupts
    
    # Clean up
    rm /tmp/small.bin /tmp/large.bin
    How many interrupts did each workload generate? Which generated more interrupts per byte? Explain the difference in terms of the transfer sizes and the driver's interrupt coalescing.
  7. Observe the effect of the queue depth. If the system has an NVMe SSD, use the fio tool (if available) to measure the effect of the queue depth on the throughput.
    # Check whether fio is available
    which fio
    
    # If it is, run a random-read workload at different
    # queue depths
    for qd in 1 4 16 64; do
        echo "=== queue depth $qd ==="
        sudo fio --name=test --filename=/tmp/fio-test.bin \
            --size=100M --rw=randread --bs=4k --iodepth=$qd \
            --ioengine=libaio --direct=1 --runtime=5 --time_based \
            2>/dev/null | grep -E 'IOPS|bw'
    done
    
    # Clean up
    rm /tmp/fio-test.bin 2>/dev/null
    How does the throughput change with the queue depth? At which depth does the throughput saturate? Explain the result in terms of the device's parallelism and the descriptor ring's design.
  8. Read the man pages. Read the man pages that document the DMA-related interfaces.
    man 1 lspci
    man 8 ethtool
    man 5 proc
    man 1 iostat
    man 1 perf
    man 8 nvme
    For each, write a sentence describing its main contribution to your understanding of DMA. The man 1 lspci page documents the PCI device listing; the man 8 ethtool page documents the network adapter's capabilities; the man 1 perf page documents the performance-analysis tool; the man 8 nvme page documents the NVMe tool.

Reflect: DMA is the mechanism that makes modern I/O possible. Without it, the processor would spend its time moving bytes, and the high-throughput devices — NVMe SSDs, 100 Gb/s network adapters — would be unusable. The mechanism is invisible to the user: the application reads a file, and the bytes appear. The invisibility is the result of the DMA engine, the descriptors, the address translation, and the cache-coherency management — decades of engineering that the user never sees. Which of the mechanisms from this lesson matter most for the systems you use? What would you change about the DMA's design if you could? How would you explain the physical-address problem to someone who has never thought about it? What is the right balance between the hardware's DMA capabilities and the operating system's abstractions?

07 Knowledge check

Check your understanding

Question 1

Define direct memory access and explain why it exists. What does the processor do during a DMA transfer, and how does the mechanism compare with programmed I/O and interrupt-driven I/O?

Show answer
Direct memory access (DMA) is the mechanism by which a device transfers data to or from memory without the processor's per-byte involvement. The processor sets up the transfer — it writes the memory address, the length, and the direction to the DMA engine — and the engine performs the transfer, reading from or writing to memory directly. When the transfer is complete, the engine raises an interrupt, and the processor handles the completion. The mechanism exists because the processor's time is too valuable to spend on moving bytes. In programmed I/O, the processor executes a load or a store for every byte or word, so its time is proportional to the amount of data transferred: a 1 MB transfer would consume about 250 million cycles, and the processor would be fully occupied for the transfer's duration. In interrupt-driven I/O, the processor is freed during the device's operation, but it still moves the data, and each transfer costs an interrupt; a 1 MB transfer in 4 KB blocks would cost about 256 interrupts and the same per-byte movement. With DMA, the processor is involved only at the transfer's setup (the buffer's allocation, the address mapping, the descriptor's write, and the doorbell) and its completion (the interrupt, the status read, the buffer's unmap, and the deferred work's scheduling). For a 1 MB transfer, the processor's involvement falls from hundreds of millions of cycles to a few thousand — a reduction of five orders of magnitude. During the transfer itself, the processor is free to execute other instructions, although its memory accesses may be delayed by the DMA engine's use of the bus (the cycle stealing of the classical systems; the modern systems' arbitration makes the contention small). The observation is why DMA is used by every high-throughput device: hard disks, solid-state drives, network adapters, and graphics cards.

Question 2

Describe the DMA transfer cycle. What are the three phases, and what role do descriptors and descriptor rings play?

Show answer
The DMA transfer cycle has three phases. Setup. The processor's driver prepares the transfer: it allocates a memory buffer, maps the buffer's virtual address to the physical addresses that the DMA engine uses (a single contiguous range or a scatter-gather list), writes the transfer's description — the source or destination addresses, the length, the direction, and the device's control bits — to the engine's registers or to a descriptor in memory, and starts the transfer by writing the engine's start bit or the doorbell register. Transfer. The DMA engine performs the transfer. It reads the descriptor (if it is in memory), then reads or writes the data one word at a time, arbitrating with the processor for the memory bus. The engine's accesses are physical: it does not use the processor's MMU, so it cannot fault, cannot be preempted, and cannot follow a page table. Completion. When the transfer is complete, the engine updates its status register and raises an interrupt. The driver's handler reads the status register, confirms the transfer's success, unmaps the buffer, and schedules the deferred work that processes the completed request. A descriptor is the data structure that describes one transfer: the buffer's physical addresses (or the scatter-gather list's address), the length, the operation (read or write), and any device-specific flags. A descriptor ring (or queue) is a circular array of descriptors in memory, with a pair of producer and consumer indices that the driver and the device use to coordinate. The ring's operation is a producer-consumer protocol: the driver writes a descriptor and advances the producer index; it notifies the device by writing to the doorbell register; the device reads the descriptors, processes the transfers, and advances the consumer index; it writes a completion entry to a separate ring and raises an interrupt; the driver's bottom half processes the completions and advances the completion ring's consumer index. The ring's design has three important properties: it is lock-free (the driver and the device each own one index), it is batched (the device can process several descriptors in one pass, and the driver can submit several before notifying), and it is parallel (a device can have several rings, and the driver can submit to different rings from different cores). The properties are the source of the modern devices' throughput: an NVMe SSD with 64 queues, each with 1,024 entries, can have 65,536 transfers in flight at once.

Question 3

Explain the physical-address problem. Why does it arise, and what are the three solutions? Compare the solutions' costs and their applicability.

Show answer
The physical-address problem is the mismatch between the addresses that the processor's programs use and the addresses that the DMA engine uses. A program's buffer is specified by a virtual address, which the processor's MMU translates to a physical address through the page table. A DMA engine, however, does not use the processor's MMU: it addresses memory by physical address, and it cannot fault, cannot be preempted, and cannot follow a page table. The problem is complicated by two facts: the buffer's virtual pages may be scattered across physical memory (a 64 KB buffer may occupy 16 physically non-contiguous 4 KB pages), and the buffer's pages may be moved or evicted during the transfer (the virtual-memory system may page out a page, migrate it for compaction or NUMA balancing, or map a different page in its place). The operating system solves the problem in one of three ways. Contiguous allocation (the bounce buffer): the kernel allocates a physically contiguous buffer for the transfer, copies the data between the buffer and the program's scattered pages, and performs the DMA on the contiguous buffer. The approach works for any device, but it costs an extra copy (or two, for a read), which is significant for high-bandwidth transfers and can halve the effective throughput. It is used as a fallback: when the device cannot handle a scatter-gather list, or when the buffer's addresses are outside the range the device can address. Scatter-gather DMA: the device's DMA engine accepts a scatter-gather list, a list of (physical address, length) pairs that describe the buffer's pages. The engine reads the list and transfers each page separately, so no copy is needed. The approach is the standard for modern storage and network controllers, but it requires the device to support the list's format and it does not provide isolation. IOMMU: the I/O memory management unit translates the device's addresses the way the processor's MMU translates the program's addresses. The kernel programs the IOMMU with a mapping from the device's address space to physical memory, and the device uses virtual addresses (from its own perspective) that the IOMMU translates. The approach removes the need for a contiguous buffer and a bounce buffer, and it provides isolation (a device can access only the memory the kernel has mapped for it). It requires the hardware (Intel VT-d, AMD Vi, ARM SMMU) and is essential for virtualization. The three solutions are not mutually exclusive: a modern system uses the IOMMU where it is available, scatter-gather lists where the device supports them, and bounce buffers as a fallback. The driver's choice depends on the device's capabilities, the buffer's characteristics, and the kernel's configuration.

Question 4

Explain cache coherency in the context of DMA. What are the two directions of the problem, and how do coherent and streaming DMA mappings address them?

Show answer
The DMA engine and the processor both access memory, and both may cache the data they access. The result is a cache-coherency problem with two directions. First, the device writes memory, but the processor's cache holds a stale copy: if the processor has recently read the buffer, its cache may hold the buffer's old contents, and the processor's subsequent read is served from the cache, so it sees the old contents even though the DMA engine wrote the new contents to memory. The processor must invalidate the cache lines before reading the new data. Second, the processor writes memory, but the DMA engine reads the old contents: if the processor has recently written the buffer, its write may still be in the cache and not yet in memory, and the DMA engine reads the memory and sees the old contents. The processor must flush (write back) the cache lines before the transfer begins. Whether the driver must perform these operations depends on the hardware's cache-coherency model. In a hardware-coherent system, the bus's protocol snoops the processor's caches during the DMA transfer: when the engine writes memory, the protocol invalidates the corresponding cache lines; when the engine reads memory, the protocol flushes the processor's dirty cache lines first. x86 systems and most modern ARM systems are hardware-coherent for their PCIe devices, so the driver does not need to perform any cache maintenance. In a software-managed system, the hardware does not maintain the caches' coherency, and the driver must perform the cache maintenance explicitly: it invalidates the cache lines before reading the device's data and flushes the cache lines before the device reads the processor's data. The Linux kernel's DMA API abstracts the two models. A coherent DMA mapping is created once and used repeatedly, with the guarantee that the processor and the device always see the same data; it is used for the long-lived structures the driver and the device share (the descriptor rings, the command queues, the status blocks). A streaming DMA mapping is created for a single transfer and destroyed when the transfer is complete; it is used for the transient buffers the driver passes to the device (the file system's pages, the network stack's packet buffers). The kernel performs the cache maintenance at the map and unmap operations: on a hardware-coherent system, the operations are mostly no-ops; on a software-managed system, the kernel flushes or invalidates the cache lines as needed. The driver must use the correct kind of mapping: a coherent mapping for a transient buffer wastes the coherent pool's space, and a streaming mapping for a long-lived structure risks a coherency bug.

Question 5

Describe the descriptor ring and explain how NVMe uses it. What properties make the ring suitable for high-performance devices, and how does the IOMMU support virtualization?

Show answer
A descriptor ring (or queue) is a circular array of descriptors in memory, each describing one transfer, with a pair of producer and consumer indices that the driver and the device use to coordinate. The ring's operation is a producer-consumer protocol: the driver writes a descriptor and advances the producer index; it notifies the device by writing to the doorbell register; the device's DMA engine reads the descriptors, processes the transfers, and advances the consumer index; it writes a completion entry to a separate ring and raises an interrupt; the driver's bottom half processes the completions and advances the completion ring's consumer index. The ring's design has three properties that make it suitable for high-performance devices. It is lock-free (or nearly so): the driver and the device each own one index, and the coordination is through the indices' values rather than through locks. It is batched: the device can process several descriptors in one pass, and the driver can submit several descriptors before notifying the device, so the per-transfer overhead is amortized. It is parallel: a device can have several rings, and the driver can submit to different rings from different cores without contention. NVMe uses the ring as its queue pair: each pair consists of a submission queue (the driver writes the commands) and a completion queue (the device writes the completions), and each pair is associated with a separate MSI-X interrupt vector. The driver can create one queue pair per processor core, submit the I/O requests from the core that originated them, and receive the completions on the same core — the per-core queue design that eliminates the cross-core synchronization that a single queue would require. An NVMe SSD with 64 queues, each with 1,024 entries, can have 65,536 transfers in flight at once, and it can achieve millions of IOPS with a modest processor. The IOMMU supports virtualization through its address translation and its isolation. A virtual machine can be given direct access to a physical device (device passthrough), and the IOMMU translates the device's DMA to the virtual machine's memory, so the virtual machine's driver talks to the device directly without the hypervisor's mediation and the device's performance is near-native. The IOMMU's isolation ensures that the device can access only the virtual machine's memory, not the hypervisor's or another virtual machine's; without it, a malicious virtual machine could use the device to attack the host. SR-IOV extends the mechanism: a physical device presents several virtual functions, each with its own DMA engine and its own IOMMU mapping, and each can be assigned to a different virtual machine. VFIO exposes the device's IOMMU mapping to user space, so a user-space driver (a virtual machine's emulator, a DPDK application) can program the device's DMA directly. The IOMMU's isolation is what makes the modern virtualization's performance possible.

08 Unit discussion

Discussion prompt

DMA is one of the operating system's oldest mechanisms, and its design has evolved through several distinct generations. The first DMA controllers were separate chips (the IBM 709's channel, the 8237 in the IBM PC) that mediated between the devices and memory. The bus-mastering devices replaced the shared controllers with per-device engines, so each device could transfer at its own rate without contending for a shared controller. The descriptor ring replaced the single-transfer setup with a queue of transfers, so the per-transfer overhead was amortized and the device could process several transfers in one pass. The scatter-gather list and the IOMMU removed the bounce buffer's extra copy, so the transfer's throughput was limited by the device and the bus rather than by the memory's bandwidth. The MSI-X interrupt and the per-core queue pair removed the cross-core synchronization, so the device's parallelism was matched by the processors' parallelism. Each generation was a response to the previous one's limitations, and each was shaped by the technology of its time.

The trend is toward autonomy and parallelism. The device's controller is becoming a computer in its own right: an NVMe SSD has multiple processor cores, its own memory, and its own scheduler, and it makes its own decisions about how to order and batch the transfers it receives. The operating system's role is shifting from driving the device to advising it: the driver submits the descriptors, and the device's controller decides how to execute them. The shift raises a question about the division of labour. If the device's controller is smart enough to schedule the transfers, should the operating system still try to sort and merge them? If the device's DMA engine can handle a scatter-gather list, should the file system still try to allocate contiguous blocks? If the device can reorder the requests, does the I/O scheduler still have a role? The modern answers are mixed: the NVMe protocol's multiple queues assume that the operating system will submit many concurrent requests, but the device's controller schedules them; the file system's allocation policies still try to write sequentially, but the drive's wear leveling may scatter the writes anyway; the I/O scheduler's role has shrunk on SSDs, but it has not disappeared.

The trend also raises a question about the DMA's future. Some systems have proposed removing the DMA's address translation entirely: the cache-coherent interconnect (CCIX, CXL) lets a device access the processor's caches directly, without the memory's round trip, and the gen-z and opencapi standards extend the idea to the memory itself. If the device can access the caches directly, the cache-coherency problem disappears, and the DMA's design simplifies. Other systems have proposed moving the DMA's scheduling to the device entirely: the smart NIC and the DPU (data processing unit) contain their own processors and can perform the network stack's and the storage stack's work without the host's involvement. If the device can do the work, the host's DMA engine is less important, and the operating system's role shrinks further.

Where should the boundary between the operating system and the device's controller be? Should the operating system expose the device's DMA details to the application, or should it hide them behind the uniform interface? What is the right balance between the hardware's autonomy and the operating system's control? Post your response to the Unit 11 discussion forum and reply to at least two classmates. Supports CLO1, CLO5, and CLO6.

09 Practice exercise

This ungraded exercise consolidates the lesson and prepares you for Lesson 11.4. Aim to spend 60–90 minutes on it.

  1. Trace a DMA transfer. A program reads 64 KB from a file on a SATA SSD. Trace the read through the DMA machinery, from the application's read system call to the driver's completion handler. For each step, describe:
    • What the driver does.
    • What the DMA engine does.
    • What the device's controller does.
    • What the processor does (if anything).
    Assume the buffer's pages are physically scattered and the system has an IOMMU.
  2. Compare the transfer mechanisms. Write a short paragraph (200–250 words) comparing programmed I/O, interrupt-driven I/O, and DMA. Your paragraph should address: (a) the processor's involvement in each; (b) the cost of each; (c) the workloads for which each is best suited; (d) the DMA's setup and completion cost and the transfer size at which DMA becomes worthwhile. Conclude with a rule of thumb for choosing among the three.
  3. Compare the address-mapping solutions. Write a short paragraph (200–250 words) comparing contiguous allocation (the bounce buffer), scatter-gather DMA, and the IOMMU. Your paragraph should address: (a) how each handles a buffer whose pages are physically scattered; (b) each solution's cost; (c) each solution's applicability; (d) the situations in which each is the right choice.
  4. Measure the transfer size's effect. Use dd with different block sizes to measure the effect of the transfer's size on the throughput, and relate the result to the DMA's setup and completion costs.
    # Create a large file
    dd if=/dev/zero of=/tmp/dma-size.bin bs=1M count=500 2>/dev/null
    sync
    echo 3 | sudo tee /proc/sys/vm/drop_caches
    
    # Measure the throughput at different block sizes
    for bs in 4k 16k 64k 256k 1M; do
        echo "=== bs=$bs ==="
        dd if=/tmp/dma-size.bin of=/dev/null bs=$bs iflag=direct 2>&1 | tail -1
    done
    
    # Clean up
    rm /tmp/dma-size.bin
    At which block size does the throughput saturate? What is the per-transfer overhead (in microseconds) implied by the small block sizes' throughput? Does the overhead match the lesson's estimate?
  5. Observe the descriptor ring's effect. If the system has fio, measure the effect of the queue depth on the throughput.
    # Check whether fio is available
    which fio
    
    # If it is, run a random-read workload at different
    # queue depths
    for qd in 1 2 4 8 16 32 64; do
        echo "=== queue depth $qd ==="
        sudo fio --name=test --filename=/tmp/fio-qd.bin \
            --size=100M --rw=randread --bs=4k --iodepth=$qd \
            --ioengine=libaio --direct=1 --runtime=3 --time_based \
            2>/dev/null | grep -E 'IOPS|bw'
    done
    
    # Clean up
    rm /tmp/fio-qd.bin 2>/dev/null
    How does the throughput change with the queue depth? At which depth does it saturate? What does the saturation point tell you about the device's parallelism and the descriptor ring's design?
  6. Investigate the IOMMU's grouping. Use the sysfs interfaces to examine the IOMMU's groups and their devices.
    # List the IOMMU groups
    ls /sys/kernel/iommu_groups/ 2>/dev/null
    
    # For each group, list its devices
    for g in /sys/kernel/iommu_groups/*/devices/*; do
        echo "Group $(basename $(dirname $(dirname $g))): $(lspci -s $(basename $g) 2>/dev/null)"
    done 2>/dev/null | head -20
    
    # Show the IOMMU's kernel messages
    dmesg | grep -iE 'iommu|vt-d|dmar' | head -10
    How many IOMMU groups are there? Which devices are in which groups? Why are some devices grouped together (because they cannot be isolated from each other)? What does the grouping tell you about the system's virtualization capabilities?
  7. Design a DMA configuration. A server is being built for a database workload. The database performs many small random reads (4 KB each) and many small writes, and it requires low latency and high throughput. The server has an NVMe SSD with 64 queue pairs and an IOMMU. Design the DMA configuration. Consider:
    • How would you configure the queue pairs (how many, and how would you assign them to the processor cores)?
    • How would you configure the IOMMU (enabled or disabled, and why)?
    • How would you configure the interrupt coalescing?
    • How would you tune the kernel's DMA-related parameters (the queue depth, the maximum transfer size, the scheduler)?
    • How would you measure the system's performance, and what metrics would you watch?
    Justify your design.
  8. Reflect. In your own words, explain why DMA is necessary for modern I/O, and describe the three phases of the DMA transfer cycle and the three address- mapping solutions. Then describe one situation in your own experience — a program you have written or a system you have used — where the DMA's design affected performance. What was the workload, and how would a different configuration have changed the outcome?

10 Summary

  • Direct memory access (DMA) is the mechanism by which a device transfers data to or from memory without the processor's per-byte involvement. The processor sets up the transfer, the DMA engine performs it, and the engine raises an interrupt when it is complete. DMA's purpose is to free the processor from the data movement, and its saving grows with the transfer's size.
  • The DMA engine is the hardware that performs the transfer. It may be an external DMA controller (the classical 8237, shared among the devices) or a bus-mastering device controller (the modern NVMe SSD, network adapter, and graphics card, each with its own engine). Modern systems use the bus-mastering design almost exclusively.
  • The DMA transfer cycle has three phases: setup (the driver allocates the buffer, maps it, writes the descriptor, and starts the engine), transfer (the engine moves the data), and completion (the engine raises an interrupt, and the driver handles it). The setup and completion take a few hundred to a few thousand cycles; the transfer takes as long as the data's movement requires.
  • A descriptor describes one transfer; a descriptor ring (or queue) is a circular array of descriptors with a producer-consumer protocol. The ring is lock-free, batched, and parallel, which makes it suitable for high-performance devices. NVMe's queue pairs are the canonical modern example: each pair has a submission queue and a completion queue, and each is associated with a separate MSI-X interrupt.
  • The physical-address problem is the mismatch between the processor's virtual addresses and the DMA engine's physical addresses. The operating system solves it with contiguous allocation (a bounce buffer and an extra copy), scatter-gather DMA (a list of the buffer's pages), or an IOMMU (a hardware translation that removes the need for both). The three solutions are not mutually exclusive: a modern system uses the IOMMU where it is available, scatter-gather lists where the device supports them, and bounce buffers as a fallback.
  • Cache coherency is the interaction between the DMA and the processor's caches: the device's writes may not be visible to the processor's cache, and the processor's writes may not be visible to the device. A hardware-coherent system maintains the coherency automatically; a software-managed system requires the driver to perform the cache maintenance explicitly. The Linux kernel's DMA API abstracts the difference with coherent mappings (for long-lived structures) and streaming mappings (for transient buffers).
  • The IOMMU provides address translation and isolation. The isolation is the basis for the modern virtualization features: device passthrough (a virtual machine gets direct access to a physical device), SR-IOV (a physical device presents several virtual functions), and VFIO (the Linux framework that exposes a device's IOMMU mapping to user space). Without the IOMMU, the device passthrough would be unsafe.
  • DMA's performance characteristics are its setup and completion latency (a few hundred to a few thousand cycles) and its transfer throughput (limited by the device, the bus, and the memory's bandwidth). The per-transfer overhead is amortized over the data's transfer for large transfers and dominates for small ones; the driver's threshold is a tunable parameter.
  • The modern stack's design reflects the DMA's characteristics: large descriptors, scatter-gather lists, and descriptor rings for the large transfers; programmed I/O or interrupt-driven I/O for the small ones; the IOMMU for the address translation and the isolation; and the per-core queue pairs for the parallelism. The design is the reason the modern devices achieve their throughput and their low latency.

Looking ahead

This lesson presented direct memory access: the DMA engine, the transfer cycle, the descriptors and descriptor rings, the physical-address problem and its three solutions, the cache coherency, the IOMMU's role in virtualization, and the DMA's performance characteristics. It referred several times to the device driver — the kernel layer that translates the file system's requests into the device's commands and manages the DMA transfers. Lesson 11.4 presents device drivers in detail: the driver's structure, the character and block device interfaces, the driver's initialization and teardown, the interrupt handling and the deferred work, the DMA's use in the driver, and the kernel's driver model. The driver is the bridge between the hardware of this lesson and the software of the file system, and it is the component that the application's I/O ultimately depends on.

11 References

  • Silberschatz, A., Galvin, P. B., & Gagne, G. Operating System Concepts, 10th ed., Chapter 12, Sections 12.2.4 and 12.7.
  • Arpaci-Dusseau, R. H., & Arpaci-Dusseau, A. C. Operating Systems: Three Easy Pieces, Chapter 36, I/O Devices. Available online at ostep.org.
  • Tanenbaum, A. S., & Bos, H. Modern Operating Systems, 4th ed., Chapter 5, Section 5.1.3.
  • Corbet, J., Rubini, A., & Kroah-Hartman, G. Linux Device Drivers, 3rd ed. O'Reilly, 2005. Chapter 15, Memory Mapping and DMA.
  • Bovet, D. P., & Cesati, M. Understanding the Linux Kernel, 3rd ed. O'Reilly, 2005. Chapter 13, I/O Architecture and Device Drivers.
  • Love, R. Linux Kernel Development, 3rd ed., Chapters 14 and 16.
  • Intel Corporation. Intel Virtualization Technology for Directed I/O Architecture Specification. The authoritative reference for the IOMMU.
  • Linux man-pages: man 1 lspci, man 8 ethtool, man 5 proc, man 1 iostat, man 1 perf, man 8 smartctl, man 8 nvme, man 1 trace-cmd.
👤