Skip to content

TLB Shootdown

A TLB shootdown is the mechanism a kernel uses to invalidate a stale translation cached in another CPU’s TLB, something neither invlpg nor a CR3 reload can do on their own, since Paging already covers both as strictly local operations: each only touches the TLB of the core executing it.

Every CPU core maintains its own private TLB, filled independently as that core walks page tables and caches the results. When one core modifies a page table entry, perhaps unmapping a page being freed, its own local invalidation clears that stale entry from its own TLB, but every other core that happens to have cached the same translation keeps using it, completely unaware the underlying mapping changed. On a single-core system this was never a problem worth naming: there was only ever one TLB to keep consistent. Under SMP, it becomes one of the sharper correctness hazards a kernel’s memory management code has to get right, since the failure mode is exactly the kind that’s hard to reproduce: one core reads or writes through a physical address a different core has already reused for something else, an intermittent bug that only shows up once a specific pair of cores race in a specific order.

A shootdown works by having the CPU that changed the mapping send an inter-processor interrupt to every other CPU that might have the stale translation cached, the same Local APIC IPI mechanism already covered for waking a core at boot, repurposed here for a different message: each recipient’s IPI handler runs invlpg (or reloads CR3, if invalidating the entire address space rather than one page) against its own local TLB, then signals back that it’s done.

void tlb_shootdown(void *addr, cpu_mask_t targets) {
invlpg(addr); // invalidate locally first
atomic_set(&shootdown_pending, targets);
send_ipi(targets, IPI_TLB_SHOOTDOWN, (uintptr_t)addr);
while (atomic_read(&shootdown_pending) != 0)
cpu_relax(); // wait for every target to ack
}
// runs on each target CPU when the IPI arrives
void tlb_shootdown_handler(void *addr) {
invlpg(addr);
atomic_and(&shootdown_pending, ~(1 << current_cpu_id()));
}
CPU0 invalidating locally, sending an IPI to CPU1 and CPU2, then waiting for both to invalidate their own TLB and acknowledge before freeing the pageCPU0CPU1CPU2invlpg (local)IPIinvlpg + ackinvlpg + ackspins waitingfor both ackspage safe to free

The initiating CPU has to wait for every target to actually acknowledge completion before it can safely proceed, most commonly by spinning on a shared bitmask each recipient clears its own bit from, since freeing the underlying physical page (or reusing it for something else) before every stale TLB entry pointing at it is gone would let another core’s in-flight access read or write through a translation that no longer means what it used to.

Sending the IPI to every CPU unconditionally is correct but wasteful on a system with many cores, most of which never touched the address space in question at all; a kernel that tracks, per address space, which CPUs have actually run a thread from it recently can narrow the target set to just those cores, skipping the interrupt (and the wait) entirely for cores with nothing to invalidate. This tracking itself has to be conservative rather than exact: a CPU that ran a thread from that address space and then switched away still needs to be in the target set unless its TLB entries for that address space are already known to have been flushed some other way (a subsequent CR3 reload for an unrelated address space, for instance, which flushes every non-global entry as a side effect regardless of which specific mappings changed).

A shootdown is not free even when correctly targeted: every recipient CPU has to stop what it’s doing, take the interrupt, and run the handler, a real interruption to whatever useful work was in progress there, which is why a kernel doing many small mapping changes in a row commonly batches them into a single shootdown covering a range of addresses rather than sending one IPI per individual invlpg. Interrupts have to remain enabled on the initiating CPU while it spins waiting for acknowledgments, specifically because the acknowledgment path itself may depend on that CPU handling its own incoming IPIs from other shootdowns in flight simultaneously; disabling interrupts across the entire wait risks a deadlock where two CPUs are each waiting on an acknowledgment the other can’t deliver until its own interrupts are re-enabled.

  1. ^ Intel, Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 3A, Chapter 4.10: TLB behavior and the invalidation instructions a shootdown is built from.