Before you start#
Read RCU and object lifetime, atomics and memory ordering, and exec and binary loading. This chapter uses upstream v6.12 and a simplified x86-64 four-level, 4 KiB example. The optional Python exercise maps a few anonymous pages in its own process. It does not read /dev/mem, bypass pagemap restrictions or change host VM settings.
Learning goals#
Distinguish an address-space object, VMA, page table and physical page or folio. Trace a VMA lookup and a bounded unmap/TLB-gather path. Explain why translation caches and secondary MMUs require coordinated invalidation, and calculate a four-level address split without treating it as universal.
One address space, several lifetimes#
A process uses virtual addresses in an address space represented by an mm_struct. Threads created with the appropriate sharing flags can share an mm; a fork without shared VM obtains a logically separate address space. Kernel threads commonly have no ordinary userspace mm, though the architecture's active_mm and lazy-TLB mechanisms can involve a borrowed address-space context. Borrowing is a specific lifetime and context protocol, not evidence that a kernel thread owns the userspace mappings or can dereference arbitrary user pointers.
mm_users counts users of the address space under its documented rules, while mm_count counts references to the mm_struct itself; the users collectively contribute a reference to mm_count. mmget/mmput and mmgrab/mmdrop serve different lifetime purposes. An mm_count reference can keep the structure allocated without guaranteeing that its userspace mappings remain intact after the last mm_users reference is released. The field comments in v6.12 include/linux/mm_types.h and helpers in v6.12 include/linux/sched/mm.h are more precise than “the process has a page table.” A pointer obtained without a valid reference or lock can become stale during exit.
In include/linux/sched/mm.h, SPDX GPL-2.0, mmdrop is shown whole (the declarations above it omitted):
static inline void mmdrop(struct mm_struct *mm)
{
/*
* The implicit full barrier implied by atomic_dec_and_test() is
* required by the membarrier system call before returning to
* user-space, after storing to rq->curr.
*/
if (unlikely(atomic_dec_and_test(&mm->mm_count)))
__mmdrop(mm);
}
mmdrop decrements mm_count and calls __mmdrop only at zero, so this count alone decides when the mm_struct itself is freed. The mapping teardown is driven by mm_users through a different path that the excerpt does not show. The comment explains a barrier for membarrier, not any rule about page tables.
A virtual memory area, or VMA, describes a range and its policy within an mm. Its vm_start and vm_end delimit the range; the end is exclusive. vm_flags describe properties and permissions, vm_ops can point to operations for specialized mappings, and vm_file can identify a file backing. Anonymous mappings have different backing relationships from file mappings; an anon_vma is involved in reverse-mapping relationships under applicable paths. A VMA is metadata, not one physical page and not a guarantee that every virtual address in its range currently has a resident page.
The v6.12 mm_struct contains mm_mt, a Maple Tree used for VMA lookup, and map_count, among many other fields. Maple Tree is a range-indexing structure, not a hardware page table. VMA creation can merge or split ranges, and a lookup can return a neighboring VMA rather than a containing one. The exact locking and lifetime contract matters when a concurrent thread maps, unmaps or changes permissions. Read split page-table locking documentation together with the source for this release; not all later documentation describes precisely the same implementation.
A mental model before the source#
Think of a virtual address as a name in a process's private address space. Four layers answer different questions: the VMA says whether a range is mapped and what policy applies; page-table entries describe the current translation; a TLB may cache that translation on a CPU; and a page or folio is the memory object that may back it. These layers can disagree temporarily in controlled ways: a VMA may exist before a page is resident, and a write-protected PTE may trigger copy-on-write even when the VMA allows writes.
A page-table walk splits an address into indices that select entries at successive levels, ending in a physical address or a fault. During unmap, Linux removes translation entries and coordinates invalidation with CPUs and relevant secondary translation users before memory can be safely reused. Keep these four questions separate as you read the implementation.
- VMA
- Policy and range: whether the address may be used and with which permissions.
- Page-table entry
- The translation and permissions installed now; it may be absent or write-protected.
- TLB entry
- A CPU-cached copy of a translation; it can stay stale until invalidated.
- Page or folio
- The memory object backing the mapping; its counts answer a different question.
Source walkthrough#
- Address space
An mm_struct owns VMA and page-table relationships under separate lifetime rules.
- VMA lookup
find_vma searches the Maple Tree under its required lock.
- Translation
Page tables and TLBs map virtual access under architecture-specific permissions.
- Unmap
Notifier, TLB gather and deferred release coordinate invalidation and reuse.
In v6.12 mm/mmap.c, find_vma includes this excerpt (SPDX GPL-2.0-only; declaration and surrounding comments omitted):
mmap_assert_locked(mm);
return mt_find(&mm->mm_mt, &index, ULONG_MAX);
The assertion expresses the required mmap locking context for this helper. mt_find searches the mm's Maple Tree starting from the supplied index. The helper can return the VMA containing the address or the next VMA, or NULL; a caller must check the returned range before assuming the address is covered. The VMA pointer is not an independently owned reference valid forever after the required protection ends. A failed lookup can be an ordinary unmapped-address case rather than corruption.
A separate, deliberately narrow unmap example appears in zap_page_range_single in v6.12 mm/memory.c (SPDX GPL-2.0-only; notifier setup and surrounding calls omitted):
unmap_single_vma(&tlb, vma, address, end, details, false);
mmu_notifier_invalidate_range_end(&range);
tlb_finish_mmu(&tlb);
The preceding code initializes a notifier range, starts invalidation and sets up an mmu_gather. The first shown call removes mappings in the specified range through that path. The next closes the notifier range, and tlb_finish_mmu completes the gather's required TLB and deferred-release work. This is one VMA-zapping path, not a claim that every munmap follows exactly these three statements or has identical locking. Error handling, huge pages, device mappings and architecture hooks add branches. tlb_finish_mmu in v6.12 mm/mmu_gather.c handles nested-flush conditions and calls tlb_flush_mmu; its batching rules matter before resources can be reused safely.
Page tables and physical memory#
A page table is a hierarchy of architecture-defined translation entries. A CPU translates a virtual address using a current address-space context and page-table roots; entries can encode presence, access rights and other architecture-specific bits. A page is a unit used by memory management and translation, while a folio is a kernel memory-management object that can represent one or more contiguous base pages. A physical page reference count tracks references under its API; mapcount tracks mappings under a different accounting scheme. Neither counter alone proves that every DMA user, page-table entry or filesystem reference is gone. Later allocation and page-cache chapters develop those details.
On x86-64 with 4 KiB pages, a common four-level translation uses 48 significant virtual-address bits, with canonical-address rules outside that range. Linux names the generic hierarchy PGD, P4D, PUD, PMD and PTE; an unused level can be folded. With five-level paging, an additional level and wider address handling can be active when hardware, configuration and runtime support permit it. Do not assume that every x86-64 kernel uses five levels merely because the source defines them, or that every mapping descends to a base-page PTE. Huge mappings can terminate at a higher level.
PTE and higher-level entry semantics include present, user/supervisor, writable and execute-disable permissions where applicable, plus accessed and dirty state whose interpretation depends on architecture and kernel handling. A VMA's permission flags and a current PTE are related but not identical: a VMA may allow writing while a particular PTE is write-protected to support copy-on-write. A fault can then trigger kernel work before the write succeeds. The page-table documentation gives the generic hierarchy; architecture headers supply actual formats.
Arm64 uses its own translation-table descriptors, address-size rules and selectable page granules, with supported combinations depending on hardware and configuration. Its permission and accessed/dirty handling must be read from arm64 source and architecture contracts, not inferred from x86 bit names. A guest virtual address, guest physical address, host physical address and DMA address are different namespaces in virtualized or device contexts. An ordinary process virtual address printed in /proc/self/maps is not a DMA mapping or a physical frame number.
A checked four-level address split#
Assume x86-64, four-level paging, 4 KiB base pages, a canonical lower-half address and no huge mapping. The offset uses 12 bits and each of four table indices uses 9 bits: 9 + 9 + 9 + 9 + 12 = 48. For the synthetic address 0x00007f123456789a, the indices are:
| Field | Bit range | Value |
|---|---|---|
| PGD | 47–39 | 0x0fe |
| PUD | 38–30 | 0x048 |
| PMD | 29–21 | 0x1a2 |
| PTE | 20–12 | 0x167 |
| offset | 11–0 | 0x89a |
In generic Linux naming the P4D level is folded in this specific four-level example. These numbers select entries only under the stated assumptions; they do not reveal the contents of any entry or the physical address. If the PMD is a huge mapping, translation can terminate before the PTE index. If five-level paging is active, or arm64 uses another granule and table layout, this split is not the right walk. The exercise below checks the shifts arithmetically; it does not inspect privileged page tables.
In arch/x86/include/asm/pgtable_64_types.h, v6.12, SPDX GPL-2.0, the #else branch used without CONFIG_X86_5LEVEL and the following PUD and PMD shifts read as follows:
/*
* PGDIR_SHIFT determines what a top-level page table entry can map
*/
#define PGDIR_SHIFT 39
#define PTRS_PER_PGD 512
#define MAX_PTRS_PER_P4D 1
#endif /* CONFIG_X86_5LEVEL */
/*
* 3rd level page
*/
#define PUD_SHIFT 30
#define PTRS_PER_PUD 512
/*
* PMD_SHIFT determines the size of the area a middle-level
* page table can map
*/
#define PMD_SHIFT 21
#define PTRS_PER_PMD 512
These defines match the split above for the PGD, PUD and PMD fields: the PGD shift is 39, the PUD shift is 30 and the PMD shift is 21, and MAX_PTRS_PER_P4D is 1, which is the folded P4D. The PTE index's shift comes from the page size and is not in this header. The excerpt covers the non-5-level case only, and it does not show that a particular running kernel uses it.
Locks, faults and invalidation#
mmap_lock protects important address-space and VMA operations under its read/write rules. Per-VMA locking, when CONFIG_PER_VMA_LOCK and the applicable path support it, provides more granular synchronization for certain operations, often with RCU-assisted lookup and validation. It is not a blanket replacement for mmap_lock on every VMA operation. Page-table locks protect modifications to page-table structures at their appropriate granularity; holding an mmap read lock does not automatically make arbitrary PTE updates safe. RCU can protect particular lookup or page-table lifetime paths, but the caller must follow the exact helper's contract.
A translation lookaside buffer, or TLB, caches translations and permissions. Removing a page-table entry in memory does not instantly invalidate every CPU's cached translation. The kernel must coordinate the relevant TLB shootdown before reusing memory in a way that would allow stale access. ASIDs on arm64 and PCIDs on x86 can tag translations by address-space context, but their allocation, generation and invalidation are architecture-specific. They reduce some flush costs; they do not eliminate the need for correct invalidation.
mmu_gather can batch TLB invalidation and deferred freeing while unmapping. The order is an invariant: page-table changes and required invalidation must be coordinated before affected page-table memory or pages are reused in a way that would expose stale translations. The exact batching, RCU page-table freeing and architecture flush requirements depend on configuration and operation. MMU notifiers allow secondary translation users, such as certain device or virtualization subsystems, to coordinate invalidation around relevant mapping changes. A CPU TLB flush alone is not proof that every secondary MMU or DMA user has stopped accessing a page.
Consider an unmap race: thread A is tearing down a mapping while CPU B has cached a translation. If A cleared the PTE and immediately recycled the page before B's stale translation was invalidated, B could access data belonging to a new owner. The kernel's gather, shootdown and lifetime rules prevent that sequence when used correctly. If another thread concurrently changes mappings, the relevant VMA and page-table locks and nested-flush handling determine the legal order. This is a hazard model, not a claim that v6.12 performs an unsafe immediate free.
- Thread APage tables
Clear the PTE under the page-table lock; the range is gathered
- Thread ACPU B TLB
Invalidate the range on CPUs that may cache it
- CPU B TLBThread A
Return. Stale translation dropped; invalidation done
- Thread APage allocator
Release pages for reuse only after the gather completes
Time runs downward. Dashed arrows show returns.
Read the text version
- Thread A → Page tables: Clear the PTE under the page-table lock; the range is gathered
- Thread A → CPU B TLB: Invalidate the range on CPUs that may cache it
- CPU B TLB → Thread A (return): Stale translation dropped; invalidation done
- Thread A → Page allocator: Release pages for reuse only after the gather completes
Mapping does not mean residency#
A successful anonymous mmap normally creates address-space metadata for a virtual range. Physical backing can be supplied later on page fault, and read and write faults can follow different paths. Some configurations may use a shared zero page for reads; transparent huge pages, overcommit policy, prefaulting and architecture behavior affect results. A mapping can fail because of invalid parameters, resource limits, address-space constraints or security policy. On later access, a fault can succeed, deliver a signal or encounter resource failure depending on state. A VMA lookup returning the next VMA is not a successful mapping of a gap.
After fork, a private writable mapping is logically separate between parent and child, often using copy-on-write until a write requires separation. With shared VM clone flags, tasks can instead use the same mm. Neither description implies that every page is physically copied at fork or that every mapping is anonymous. Page faults and COW mechanics receive a full treatment in the next chapter; here the important distinction is between VMA policy, current page-table state and physical backing.
One lookup result, two different questions#
Suppose a VMA covers 0x400000 through but not including 0x404000, and the next VMA starts at 0x500000. A call to find_vma for 0x405000 can return the latter VMA even though the requested address is in the gap. A fault handler or other caller must compare the address with vm_start before treating the result as a containing mapping. Separately, a lookup for 0x401000 can find the first VMA while its PTE is absent; policy metadata and a populated translation are distinct facts. Concurrent unmap can invalidate a previously observed VMA pointer once the required lock or reference protection is lost. This small state example explains both the range check and the lifetime requirement without asserting anything about a particular process's physical pages.
Practice#
Source-only route: calculate each index in the table with (address >> shift) & 0x1ff for shifts 39, 30, 21 and 12; calculate the offset with address & 0xfff. Draw a VMA covering the address, a hypothetical page-table walk and an absent-PTE case. Add CPU B's stale TLB entry to the unmap race and mark the required invalidation before reuse. This is arithmetic and source reasoning, not a page-table dump.
Own-process host route: run this bounded Python 3 example. It creates four anonymous base-page-sized units in the Python process, touches two offsets, reports its own mapping line and minor-fault counter differences, then closes the mapping. It uses no privileged interfaces and does not change host VM settings.
getconf PAGESIZE
python3 - <<'PY'
import ctypes, mmap, os, resource
page = os.sysconf('SC_PAGE_SIZE')
before = resource.getrusage(resource.RUSAGE_SELF).ru_minflt
region = mmap.mmap(-1, page * 4, access=mmap.ACCESS_WRITE)
try:
address = ctypes.addressof(ctypes.c_char.from_buffer(region))
after_map = resource.getrusage(resource.RUSAGE_SELF).ru_minflt
region[0] = 1
region[page * 2] = 2
after_touch = resource.getrusage(resource.RUSAGE_SELF).ru_minflt
print('mapping bytes:', page * 4)
print('minor-fault deltas map/touch:', after_map-before, after_touch-after_map)
with open('/proc/self/maps', encoding='ascii') as maps:
for line in maps:
bounds = line.split(None, 1)[0]
lo, hi = (int(x, 16) for x in bounds.split('-'))
if lo <= address < hi:
print('own mapping:', line.strip())
break
else:
print('mapping line not visible')
finally:
region.close()
PY
Expect a mapping covering the address and a positive-size allocation; the fault deltas are not guaranteed to equal two. Python runtime activity, zero pages, transparent huge pages, prefaulting and accounting can change them. The /proc/self/maps file is read by this same Python process, avoiding the common mistake of asking cat to show its own mappings. Addresses and mappings can be sensitive; review output before sharing. If /proc is unavailable or mapping fails, retain the error and use the source-only exercise; do not attempt a pagemap permission bypass. Closing the mapping happens in finally, and no file cleanup is needed.
As a counterexample, seeing a VMA line does not prove every page has a resident physical page or that its address is usable by a device for DMA. Deliver the address-split table, unmap race diagram and labeled host observation if performed.
Knowledge check#
- What does
find_vmareturn when the requested address lies in a gap before another VMA? - Why can an
mm_countreference outlive usable mappings after the lastmm_usersreference is dropped? - Why can a VMA permit writing while a current PTE is write-protected?
- Why must an unmap coordinate TLB invalidation before relevant memory reuse?
- Which assumptions make the
9+9+9+9+12split applicable?
Answers#
- It can return the next VMA rather than
NULL; the caller must test that the address is actually within the returned range. mm_countkeeps the structure itself alive, whilemm_userstracks address-space users under its contract. Releasing the last user can tear down mappings before the final structure reference disappears.- The VMA expresses policy, while a PTE can temporarily restrict access, for example to trigger a copy-on-write fault. The fault path decides whether the operation can proceed.
- Another CPU may retain a stale translation or permission in its TLB. Reusing memory before the required shootdown and lifetime synchronization could expose the new owner's data.
- A four-level x86-64 walk with 4 KiB base pages and no higher-level huge mapping; five-level paging and arm64 configurations differ.
Checkpoint#
Deliver the checked address split, VMA/translation diagram and unmap race analysis, plus the own-process observation if performed. Pass when you distinguish VMA metadata from residency, mm_users from mm_count, and TLB invalidation from mere PTE clearing. If uncertain, retrace find_vma and zap_page_range_single and recalculate each bit field. This is not certification.