arthrop0d

Brief articles on varied topics.

TLB Shootdowns and Deferred Invalidation

operating systemsconcurrencyperformance

A TLB shootdown is the process of telling other CPUs to invalidate cached virtual-to-physical address translations after an operating system changes a page-table mapping. The notification usually arrives as an inter-processor interrupt, or IPI, so each affected CPU can discard its stale translation before using it again.

This exists because page tables and TLBs are separate things. A page table records which physical memory a virtual address refers to and what access is allowed. A translation lookaside buffer (TLB) is a small, fast cache of those page-table results inside each CPU core. The TLB lets the CPU translate addresses without walking the page table on every load and store.

The problem: page tables are not the whole story

Suppose a process uses virtual address 0x4000, which currently maps to physical page A. A CPU looks up that mapping, caches it in its TLB, and continues running. Later, the operating system changes the page table so 0x4000 maps to physical page B—or becomes inaccessible.

Updating the page-table memory does not automatically remove the old entry from every CPU's private TLB. The next access on a CPU with the cached entry can still go to page A, even though the page table now says otherwise.

That can cause more than an out-of-date read. The operating system might have removed page A from the process and returned it to an allocator. A stale translation could then let the process read or write memory that now belongs to another object or process. If permissions changed, a stale TLB entry could also allow a write that should fault.

The page table is the source of truth, but a TLB may continue using an older cached answer until it is explicitly invalidated.

This is why changing a mapping is a concurrency operation, not merely a memory write. The operating system must coordinate with CPUs that could still use the old translation.

How a shootdown works

A typical sequence looks like this:

  1. The operating system updates the page table, or prepares to remove a mapping.
  2. It identifies the CPUs currently running the affected address space. An address space is the set of mappings belonging to one process or kernel context.
  3. It sends those CPUs an IPI requesting a TLB invalidation.
  4. Each receiving CPU invalidates the relevant virtual address, a range of addresses, or its entire TLB for that address space.
  5. Each CPU acknowledges completion.
  6. The initiating CPU can safely continue with work that depends on the old translation being gone—for example, reusing the physical page.

The interrupt matters because a CPU might be executing ordinary user code and might not naturally enter the kernel soon. Waiting for a future context switch is unsafe if the CPU can immediately access the changed address. The IPI forces it to run a small kernel handler now.

The operating system does not necessarily contact every CPU. CPUs running unrelated address spaces cannot normally use a process's user-space mappings, so they can be excluded. Kernel mappings require more care because they may be visible across many or all address spaces.

The CPU also invalidates its own cached entry as part of the change. Hardware provides instructions for invalidating one translation or flushing a larger set, but the exact instruction and behavior depend on the processor architecture. Features such as address-space identifiers—often called ASIDs or PCIDs—let a TLB retain entries belonging to different address spaces, reducing unnecessary flushes. They do not remove the need to invalidate an entry whose meaning has changed.

Why shootdowns are expensive

An IPI costs more than an ordinary function call. The target CPU must stop its current work, save enough state to handle the interrupt, perform the invalidation, and send an acknowledgment. The initiating CPU may have to wait for several acknowledgments. On a machine with many cores, the cost grows with the number of participants and can introduce latency spikes.

The cost is especially visible in workloads that frequently change mappings: creating and destroying processes, modifying memory protections, growing or shrinking memory regions, unmapping files, or reclaiming memory. A log mentioning a “TLB flush” or “shootdown,” or a performance profile showing time in TLB invalidation, usually points to this coordination overhead rather than to a slow page-table write itself.

Batching several changes

Operating systems reduce the cost by combining work. If a thread is about to unmap 1,000 adjacent pages, it need not send 1,000 IPIs and wait 1,000 times. It can change the page tables for the whole region, then request one range invalidation—or choose one broader flush if that is cheaper than managing many individual entries.

The same idea applies across operations. A kernel can record several pending mapping changes while holding the relevant memory-management lock, then perform one shootdown before releasing the lock. This amortizes the interrupt, handler, and acknowledgment costs.

There is a trade-off: invalidating a whole TLB is simpler but may discard useful translations, causing later page-table walks. Invalidating a precise range preserves more cache entries but can require more bookkeeping. The best choice depends on the number and shape of changed mappings and on the processor's invalidation costs.

Deferring invalidation safely

Some invalidations can be postponed. A CPU may be marked as having a stale view of an address space rather than being interrupted immediately. Before that CPU runs the address space again—or before it performs an operation that could use the changed mapping—it checks the pending state and invalidates its TLB. This is often called lazy or deferred TLB invalidation.

Deferral is safe only when the old translation cannot cause harm in the meantime. For example, if a CPU has stopped running that address space, the kernel can wait until the next switch back to it. The kernel must not reuse a removed physical page while another CPU could still access it through a stale TLB entry. It may therefore wait for a suitable grace period, or keep the physical page quarantined until every relevant CPU has either flushed or stopped using the address space.

Generation counters are one way to track this. The address space records a newer mapping-generation number; each CPU records the generation it has observed. On a later switch or entry into that address space, a mismatch tells the CPU to invalidate before continuing.

Batching reduces how often shootdowns happen. Deferred invalidation reduces how often they must interrupt an actively running CPU. Together they preserve the correctness rule—no CPU may keep using a dangerous stale translation—while avoiding needless cross-core interruptions.

← All articles