Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Atlas, an elephant, draws a map divided into numbered pages with a pencil held in its trunk. Most squares are blank, a few are filled in, and one is being drawn now that it is needed.

Virtual Memory

The address space of a process is the set of memory addresses the process can use and the meaning the kernel has given to each. The addresses are virtual: they are translated to locations in physical memory by the processor, through a table that the kernel maintains for each process. Different processes can use the same virtual address for different memory, and neither can reach the memory of the other.

In this chapter, we will learn how the kernel fills an address space. We will first measure with vmfacts when a mapped page receives physical memory and where a write through a file mapping ends up. Then we will write vmmodel, a model with mappings, a page table and a page fault handler, and use it to follow copy-on-write after fork.

This chapter covers

  • the layout of a process’s address space, read from /proc/self/maps
  • pages, frames, and how the processor translates an address
  • the kernel structures: struct mm_struct, struct vm_area_struct, struct page
  • mappings, and what mmap does and leaves undone
  • demand paging: a page receives memory when it is first used
  • the shared zero page
  • private and shared file mappings
  • copy-on-write, and why fork does not copy the memory of a process
  • which accesses end in SIGSEGV
  • how much memory a process uses: virtual size, resident size, and overcommit

Why addresses are virtual

Suppose programs used the addresses of physical memory directly. A program compiled to keep a variable at address 0x1000 could run only when that memory was free, and 2 copies of it could not run together. Any program could read or overwrite the memory of any other by using its addresses. And a program could use no more memory than was physically free at the moment it started.

Virtual addresses remove all 3 problems with a single idea: the addresses a program uses are private to it, and a table per process says which physical memory, if any, stands behind each of them.

Process A and process B both read address 0x1000. A's page table maps page 1 to frame 5, and B's page table maps page 1 to frame 7. The same address leads to different frames of physical memory, because each process has its own page table.
Figure 7.1 Both processes use address 0x1000, and each reaches its own memory.

With this arrangement every program can be built as if it had the machine to itself. The kernel decides which physical memory each address reaches, and it can decide late, at the moment the address is first used. The rest of the chapter is about how the table is organised and when it is filled in.

The layout of an address space

Before the mechanism, we look at what an address space contains. The kernel lists the address ranges a process can use in /proc/<pid>/maps. This is the listing of a cat process on our machine, with 3 of its columns removed.

$ cat /proc/self/maps
aaaab7c80000-aaaab7c86000 r-xp 00000000 /usr/bin/cat
aaaab7c95000-aaaab7c96000 r--p 00005000 /usr/bin/cat
aaaab7c96000-aaaab7c97000 rw-p 00006000 /usr/bin/cat
aaaacae31000-aaaacae52000 rw-p 00000000 [heap]
ffffaa650000-ffffaa7d8000 r-xp 00000000 /usr/lib/aarch64-linux-gnu/libc.so.6
ffffaa7e7000-ffffaa7eb000 r--p 00187000 /usr/lib/aarch64-linux-gnu/libc.so.6
ffffaa7eb000-ffffaa7ed000 rw-p 0018b000 /usr/lib/aarch64-linux-gnu/libc.so.6
ffffaa80b000-ffffaa82d000 rw-p 00000000
ffffaa82d000-ffffaa858000 r-xp 00000000 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
ffffaa866000-ffffaa867000 r-xp 00000000 [vdso]
fffff2ebe000-fffff2ee0000 rw-p 00000000 [stack]

The numbers are addresses in hexadecimal, and they are less forbidding than they look. A line gives a start and an end, and the size of the range is the difference. For the first line, 0xaaaab7c86000 - 0xaaaab7c80000 = 0x6000, which is 6 × 4096 bytes: the code of cat occupies 6 pages. Only the differences carry information. The absolute values change on every run.

4 lines of /proc/pid/maps explained. The line for /usr/bin/cat covers 0x6000 bytes, 6 pages, readable and executable: the program's code. The heap line covers 0x21000 bytes, 33 pages, readable and writable: memory from malloc. The libc line covers 0x188000 bytes, 392 pages of library code shared by every program that uses libc. The stack line covers 0x22000 bytes, 34 pages, for local variables and return addresses.
Figure 7.2 4 lines of the listing, with the size of each range worked out.

Each line is a mapping: a range of addresses with the same permissions and the same source.

ColumnMeaning
aaaab7c80000-aaaab7c86000the first address of the range, and the first address after it
r-xpread, write and execute permission, then p for a private mapping or s for a shared one
00005000for a mapping of a file: the offset in the file at which the range starts
the last columnthe file that is mapped, a name in brackets for a special region, or nothing for anonymous memory
RegionContents
/usr/bin/cat, r-xpthe instructions of the program, read from the program file; executable and read-only
/usr/bin/cat, rw-pthe global variables of the program
[heap]memory for small allocations by malloc, grown with the brk system call
libc.so.6, ld-linux-aarch64.so.1shared libraries, each with its own code and data ranges
no nameanonymous memory: not backed by a file, and filled with zeros when first used
[vdso]a small library that the kernel maps into every process, which lets calls such as clock_gettime run without entering the kernel
[stack]the stack of the main thread: local variables and return addresses
The address space of a cat process from high addresses to low. The stack, read-write, holds local variables and grows downward. Below it are the shared libraries libc and ld-linux, mapped from files. Then anonymous mappings for large allocations and thread stacks. Then the heap, read-write, used by malloc and grown upward with brk. Then the data of /usr/bin/cat, read-write, with its global variables. At the lowest addresses is the code of /usr/bin/cat, readable and executable.
Figure 7.3 The regions of a small process, from high addresses to low.

The ranges are far apart, and most of the space between them is unmapped. An access to an unmapped address ends the process with SIGSEGV. The addresses differ from run to run, because the kernel chooses the positions of the regions at random for each new program, which makes attacks that depend on known addresses harder.

Pages, mappings and the page table

Virtual and physical memory are both divided into units of the same size, 4096 bytes on this machine. A unit of virtual memory is a page, and a unit of physical memory is a frame. The address space is described at 2 levels.

LevelKept byContents
Mappingsthe kernela list of address ranges; for each, the permitted access and where its contents come from: a file, or nothing (anonymous memory, initially zero)
Page tablethe kernel, read by the processorfor each page that is in use: the frame that holds it, and whether writing is allowed

Translating an address

Translation is easiest to follow on a machine small enough to work by hand. Imagine addresses of 16 bits and pages of 256 bytes. An address then has 2 hexadecimal digits for the page number and 2 for the offset within the page. To translate 0x1234, split it into page 0x12 and offset 0x34, look up entry 0x12 of the page table, find frame 0x7A there, and attach the offset: the physical address is 0x7A34.

The translation of address 0x1234 on a small machine with 16-bit addresses and pages of 256 bytes. The address splits into page 0x12 and offset 0x34. Entry 0x12 of the page table holds frame 0x7A. The physical address is frame 0x7A followed by the unchanged offset 0x34: 0x7A34.
Figure 7.4 Address 0x1234 translated by hand on a machine with pages of 256 bytes.

A real machine does exactly this, with larger numbers and with a table that is split into several levels to save space.

The processor translates every address a program uses, in hardware. An address of this machine has 48 significant bits. The low 12 bits are the offset within a page, since a page has 4096 = 212 bytes. The remaining 36 bits are the page number, and they are used in 4 groups of 9 bits.

A virtual address of 48 bits is divided into 5 fields: 4 index fields of 9 bits each, for levels 0 to 3 of the page table, and an offset of 12 bits. Each index selects 1 of 512 entries of a table, and the offset selects a byte within the frame.
Figure 7.5 The fields of a virtual address on arm64 with pages of 4096 bytes.

The page table is a tree of tables, each a page of 512 entries. The processor starts at the root table of the process, uses the first 9 bits to select an entry, follows it to a table of the next level, and repeats this 4 times. The last entry holds the number of the frame and the permission bits. The frame number and the offset together are the physical address.

The translation of a virtual address. The address is split into 4 indexes and an offset. The level 0 table, the root that mm->pgd points to, is indexed with index 0, and the entry leads to a level 1 table. Index 1 leads to a level 2 table, index 2 to a level 3 table, and index 3 selects the page table entry, which gives the frame. The frame and the offset form the physical address.
Figure 7.6 The processor walks 4 levels of tables to find the frame of a page.

A tree is used because the address space is almost empty. A flat table for 236 pages would need 512 GiB for each process. In the tree, a table exists only for the regions that are in use, and the cat process above needs a few dozen kilobytes.

Walking 4 tables for every memory access would be slow, so the processor keeps the results of recent translations in a cache, the translation lookaside buffer or TLB. Most accesses are answered from the TLB. When the kernel changes a page table entry, it tells the processor to discard the cached translation.

mmap adds a mapping. It writes no page table entry and allocates no frame. A mapping is a promise: these addresses are valid, and this is what they contain.

The promise is kept later. When the program uses an address whose page has no entry, or writes where the entry forbids writing, the processor cannot complete the instruction. It raises a page fault, which transfers control to the kernel. The kernel looks up the address in the mappings. If a mapping permits the access, the kernel provides a frame, writes the entry, and returns, and the processor repeats the instruction. If no mapping permits it, the kernel sends the process the signal SIGSEGV.

A program reads or writes a virtual address. The processor looks in the page table for an entry that allows the access. When there is none, it raises a page fault. The kernel checks whether a mapping allows the access. If so, it fills in the page table entry with the zero frame, a new frame or a private copy, and the access is retried. If no mapping allows it, the kernel sends SIGSEGV.
Figure 7.7 The page table is filled in by page faults.

Filling the table on demand means that memory is spent only on pages that are used. A program can map a large range and touch a small part of it, and a large program starts quickly because its code is read from disk page by page as it runs.

The kernel’s structures

The kernel describes an address space with 3 structures. These are the definitions in include/linux/mm_types.h of Linux 5.15, reduced to the fields we use.

struct mm_struct {
        struct vm_area_struct *mmap;           /* the mappings, as a list */
        struct rb_root mm_rb;                  /* the mappings, as a tree by address */
        pgd_t *pgd;                            /* the root of the page table */
        int map_count;                         /* number of mappings */
        unsigned long total_vm;                /* pages mapped */
        unsigned long start_brk, brk;          /* the heap */
        unsigned long start_stack;             /* the stack */
        /* ... */
};

struct vm_area_struct {
        unsigned long vm_start;                /* first address of the mapping */
        unsigned long vm_end;                  /* first address after it */
        struct vm_area_struct *vm_next, *vm_prev;
        struct mm_struct *vm_mm;               /* the address space it belongs to */
        unsigned long vm_flags;                /* VM_READ, VM_WRITE, VM_EXEC, VM_SHARED */
        unsigned long vm_pgoff;                /* offset in the file, in pages */
        struct file *vm_file;                  /* the mapped file, or NULL */
        /* ... */
};

struct page {
        unsigned long flags;                   /* dirty, locked, ... */
        struct address_space *mapping;         /* the file this frame caches, if any */
        pgoff_t index;                         /* its page number in that file */
        atomic_t _mapcount;                    /* page table entries that map it */
        atomic_t _refcount;                    /* users of the frame */
        /* ... */
};
StructureExistsMeaning
struct mm_structonce per processThe address space. task_struct.mm points to it, and the threads of a process share it.
struct vm_area_structonce per mappingA line of /proc/<pid>/maps. The kernel calls it a VMA.
struct pageonce per frame of physical memoryThe kernel’s record of a frame: who uses it and what it contains.
FieldMeaning
mmap, mm_rbThe mappings of the process, as a list in address order and as a tree for fast lookup of the mapping that contains an address.
pgdThe address of the root table that the processor walks. A context switch loads it into the processor (chapter 6).
vm_start, vm_endThe 2 addresses in the first column of maps.
vm_flagsThe permissions in the second column.
vm_file, vm_pgoffThe file and the offset in the last columns. vm_file is a struct file (chapter 2).
_refcount, _mapcountCounts that decide when a frame is free and whether it is shared, used for copy-on-write below.
The memory structures. Each process has a struct mm_struct with the list and tree of mappings, pgd, the root of the page table, map_count, total_vm, and the bounds of the heap and the stack. Each mapping is a struct vm_area_struct with vm_start and vm_end, vm_flags, vm_file and vm_pgoff. Each frame of physical memory has a struct page with _refcount, _mapcount, mapping and flags.
Figure 7.8 The address space, its mappings, and the frames behind them.

There are 2 descriptions of the same address space, and the difference between them is the subject of this chapter. The list of VMAs says what the process is allowed to use. The page table says what has actually been given a frame. mmap changes only the first.

mmap of 1 GiB allocates no memory. The frames arrive page by page, at the first access to each page.

Measuring an address space

vmfacts uses golang.org/x/sys/unix in Go and the memmap2 crate in Rust.

$ cargo new vmfacts
$ cargo add -p vmfacts memmap2

The kernel publishes the page table of a process in /proc/self/pagemap: 8 bytes for each virtual page, in which bit 63 is set when the page has a frame. resident counts those pages for a mapping.

Listing 7.1 The imports of vmfacts, in Go. code/go/vmfacts/main.go

import (
	"bufio"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"strings"

	"golang.org/x/sys/unix"
)

Listing 7.2 The imports of vmfacts, in Rust. code/rust/vmfacts/src/main.rs

use std::fs::{self, File, OpenOptions};
use std::io;
use std::os::unix::fs::FileExt;
use std::path::Path;

use memmap2::{MmapMut, MmapOptions};

Listing 7.3 Counting the pages of a mapping that have a frame, in Go. code/go/vmfacts/main.go

const pageSize = 4096

// address returns the virtual address of the first byte of a mapping.
func address(mapping []byte) uintptr {
	return reflect.ValueOf(mapping).Pointer()
}

// resident counts the pages of a mapping that have physical memory. The
// kernel publishes 8 bytes per virtual page in /proc/self/pagemap, and bit 63
// of an entry is set when the page is present in memory.
func resident(mapping []byte) (int, error) {
	pagemap, err := os.Open("/proc/self/pagemap")

	if err != nil {
		return 0, err
	}

	defer pagemap.Close()

	entries := make([]byte, 8*len(mapping)/pageSize)

	if _, err := pagemap.ReadAt(entries, int64(address(mapping)/pageSize*8)); err != nil {
		return 0, err
	}

	count := 0

	for i := 0; i < len(entries); i += 8 {
		if binary.LittleEndian.Uint64(entries[i:])>>63 == 1 {
			count++
		}
	}

	return count, nil
}

Listing 7.4 Counting the pages of a mapping that have a frame, in Rust. code/rust/vmfacts/src/main.rs

const PAGE_SIZE: usize = 4096;

/// Counts the pages of a mapping that have physical memory. The kernel
/// publishes 8 bytes per virtual page in `/proc/self/pagemap`, and bit 63 of
/// an entry is set when the page is present in memory.
fn resident(mapping: &[u8]) -> io::Result<usize> {
    let pagemap = File::open("/proc/self/pagemap")?;
    let mut entries = vec![0u8; 8 * mapping.len() / PAGE_SIZE];
    let offset = mapping.as_ptr() as usize / PAGE_SIZE * 8;

    pagemap.read_exact_at(&mut entries, offset as u64)?;

    let (entries, _) = entries.as_chunks::<8>();
    let count = entries.iter().filter(|entry| entry[7] >> 7 == 1).count();

    Ok(count)
}

demand maps 64 pages of anonymous memory and counts the resident pages 3 times: after mmap, after writing to 10 pages, and after reading 1 further page.

Listing 7.5 When a page receives memory, in Go. code/go/vmfacts/main.go

// demand maps 64 pages of anonymous memory, writes to 10 of them, reads 1
// more, and counts the resident pages after each step.
func demand() error {
	mapping, err := unix.Mmap(-1, 0, 64*pageSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS)

	if err != nil {
		return err
	}

	defer unix.Munmap(mapping)

	before, err := resident(mapping)

	if err != nil {
		return err
	}

	for page := range 10 {
		mapping[page*pageSize] = 1
	}

	written, err := resident(mapping)

	if err != nil {
		return err
	}

	value := mapping[20*pageSize]
	read, err := resident(mapping)

	if err != nil {
		return err
	}

	fmt.Printf("64 pages mapped: %d resident\n", before)
	fmt.Printf("after writing to 10 pages: %d resident\n", written)
	fmt.Printf("after reading 1 more page: %d resident, the byte read is %d\n", read, value)

	return nil
}

Listing 7.6 When a page receives memory, in Rust. code/rust/vmfacts/src/main.rs

/// Maps 64 pages of anonymous memory, writes to 10 of them, reads 1 more, and
/// counts the resident pages after each step.
fn demand() -> io::Result<()> {
    let mut mapping = MmapMut::map_anon(64 * PAGE_SIZE)?;

    let before = resident(&mapping)?;

    for page in 0..10 {
        mapping[page * PAGE_SIZE] = 1;
    }

    let written = resident(&mapping)?;

    let value = mapping[20 * PAGE_SIZE];
    let read = resident(&mapping)?;

    println!("64 pages mapped: {before} resident");
    println!("after writing to 10 pages: {written} resident");
    println!("after reading 1 more page: {read} resident, the byte read is {value}");

    Ok(())
}

protect reads the permissions of a mapping from /proc/self/maps before and after mprotect removes the permission to write.

Listing 7.7 The permissions of a mapping, in Go. code/go/vmfacts/main.go

// permissions returns the permission column of the line in /proc/self/maps
// that starts at the address of a mapping.
func permissions(mapping []byte) (string, error) {
	maps, err := os.Open("/proc/self/maps")

	if err != nil {
		return "", err
	}

	defer maps.Close()

	start := fmt.Sprintf("%x-", address(mapping))

	for lines := bufio.NewScanner(maps); lines.Scan(); {
		if fields := strings.Fields(lines.Text()); strings.HasPrefix(fields[0], start) {
			return fields[1], nil
		}
	}

	return "", fmt.Errorf("mapping at %s not found", start)
}

// protect reads the permissions of a mapping before and after mprotect
// removes the permission to write.
func protect() error {
	mapping, err := unix.Mmap(-1, 0, pageSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS)

	if err != nil {
		return err
	}

	defer unix.Munmap(mapping)

	before, err := permissions(mapping)

	if err != nil {
		return err
	}

	if err := unix.Mprotect(mapping, unix.PROT_READ); err != nil {
		return err
	}

	after, err := permissions(mapping)

	if err != nil {
		return err
	}

	fmt.Printf("permissions of the mapping: %s, after mprotect: %s\n", before, after)

	return nil
}

Listing 7.8 The permissions of a mapping, in Rust. code/rust/vmfacts/src/main.rs

/// Returns the permission column of the line in `/proc/self/maps` that starts
/// at the address of a mapping.
fn permissions(mapping: &[u8]) -> io::Result<String> {
    let start = format!("{:x}-", mapping.as_ptr() as usize);

    fs::read_to_string("/proc/self/maps")?
        .lines()
        .filter(|line| line.starts_with(&start))
        .find_map(|line| line.split_whitespace().nth(1))
        .map(str::to_owned)
        .ok_or_else(|| io::ErrorKind::NotFound.into())
}

/// Reads the permissions of a mapping before and after mprotect removes the
/// permission to write.
fn protect() -> io::Result<()> {
    let mapping = MmapMut::map_anon(PAGE_SIZE)?;
    let before = permissions(&mapping)?;

    let mapping = mapping.make_read_only()?;
    let after = permissions(&mapping)?;

    println!("permissions of the mapping: {before}, after mprotect: {after}");

    Ok(())
}

throughMapping maps a file that contains hello, writes HELLO into the mapped memory, and reads the file again. It does this with a private mapping and with a shared one. Mapping a file is an unsafe operation in Rust, because another process could change the file while the program holds a slice of it. The comment in the listing states why the use is sound here.

Listing 7.9 Writing through a private and a shared file mapping, in Go. code/go/vmfacts/main.go

// throughMapping maps a file that holds "hello", writes "HELLO" through the
// mapping, unmaps it, and returns the contents of the file.
func throughMapping(path string, flags int) (string, error) {
	if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil {
		return "", err
	}

	file, err := os.OpenFile(path, os.O_RDWR, 0)

	if err != nil {
		return "", err
	}

	defer file.Close()

	mapping, err := unix.Mmap(int(file.Fd()), 0, 5, unix.PROT_READ|unix.PROT_WRITE, flags)

	if err != nil {
		return "", err
	}

	copy(mapping, "HELLO")

	if err := unix.Munmap(mapping); err != nil {
		return "", err
	}

	text, err := os.ReadFile(path)

	return string(text), err
}

// files compares a private file mapping with a shared one.
func files() error {
	dir, err := os.MkdirTemp("", "vmfacts")

	if err != nil {
		return err
	}

	defer os.RemoveAll(dir)

	path := filepath.Join(dir, "file")

	for _, kind := range []struct {
		name  string
		flags int
	}{{"private", unix.MAP_PRIVATE}, {"shared", unix.MAP_SHARED}} {
		text, err := throughMapping(path, kind.flags)

		if err != nil {
			return err
		}

		fmt.Printf("HELLO written through a %s file mapping: the file holds %q\n", kind.name, text)
	}

	return nil
}

Listing 7.10 Writing through a private and a shared file mapping, in Rust. code/rust/vmfacts/src/main.rs

/// Maps a file that holds "hello", writes "HELLO" through the mapping, unmaps
/// it, and returns the contents of the file.
fn through_mapping(path: &Path, shared: bool) -> io::Result<String> {
    fs::write(path, "hello")?;

    let file = OpenOptions::new().read(true).write(true).open(path)?;
    let options = MmapOptions::new();

    // SAFETY: mapping a file is unsafe in Rust because another process could
    // change the file while the slice exists. This file is in a directory the
    // program has just created, and nothing else opens it.
    let mut mapping = unsafe {
        if shared {
            options.map_mut(&file)?
        } else {
            options.map_copy(&file)?
        }
    };

    mapping.copy_from_slice(b"HELLO");
    drop(mapping);

    fs::read_to_string(path)
}

/// Compares a private file mapping with a shared one.
fn files() -> io::Result<()> {
    let dir = std::env::temp_dir().join(format!("vmfacts-{}", std::process::id()));
    let path = dir.join("file");

    fs::create_dir_all(&dir)?;

    for (name, shared) in [("private", false), ("shared", true)] {
        let text = through_mapping(&path, shared)?;
        println!("HELLO written through a {name} file mapping: the file holds {text:?}");
    }

    fs::remove_dir_all(&dir)
}
64 pages mapped: 0 resident
after writing to 10 pages: 10 resident
after reading 1 more page: 11 resident, the byte read is 0
permissions of the mapping: rw-p, after mprotect: r--p
HELLO written through a private file mapping: the file holds "hello"
HELLO written through a shared file mapping: the file holds "HELLO"
OutputConclusion
64 pages mapped, 0 residentmmap allocates no memory.
10 resident after writing to 10 pagesA page receives a frame at the first access to it.
11 resident after a read, and the byte is 0A read is also an access. An untouched anonymous page reads as zeros.
rw-p becomes r--pPermissions are a property of the mapping. The p marks a private mapping.
private mapping: the file holds helloA write to a private mapping changes a copy that belongs to the process.
shared mapping: the file holds HELLOA write to a shared mapping changes the file’s own page.

Modelling an address space

Low-level design: vmmodel

TypeFieldsMeaning
framedata, refsa page of physical memory and the number of entries that refer to it
memoryframes, zerothe count of allocated frames; the shared frame of zeros
mappingstart, pages, writablea range of virtual pages and its permission
entryframe, writablea page table entry
spacemappings, table, faultsan address space
OperationBehaviour
mmap(pages, writable)adds a mapping; returns its address
read(address), write(address, value)use the entry when it permits the access; otherwise call fault and repeat
fault(page, write)fails with SIGSEGV, or installs an entry
fork()returns a space with the same mappings and entries, all read-only on both sides

In the Rust version, a frame is an Rc of an array, and the reference count of the Rc is the refs field of the Go version.

Listing 7.11 The imports of vmmodel, in Go. code/go/vmmodel/main.go

import (
	"errors"
	"fmt"
)

Listing 7.12 The imports of vmmodel, in Rust. code/rust/vmmodel/src/main.rs

use std::cell::Cell;
use std::collections::HashMap;
use std::fmt;
use std::rc::Rc;

Listing 7.13 Frames, mappings, entries and the address space, in Go. code/go/vmmodel/main.go

const pageSize = 4096

// errSegv is the result of an access that no mapping allows. The kernel
// sends the process the signal SIGSEGV.
var errSegv = errors.New("SIGSEGV")

// A frame is a page of physical memory. refs counts the page table entries
// that refer to it.
type frame struct {
	data [pageSize]byte
	refs int
}

// memory is the physical memory shared by all address spaces: the number of
// frames allocated, and the frame of zeros that every read of an untouched
// page maps.
type memory struct {
	frames int
	zero   *frame
}

func (m *memory) allocate() *frame {
	m.frames++

	return &frame{refs: 1}
}

// A mapping is a range of virtual pages and the access it permits.
type mapping struct {
	start, pages int
	writable     bool
}

// An entry is a page table entry: the frame behind a virtual page, and
// whether the processor may write to it without a fault.
type entry struct {
	frame    *frame
	writable bool
}

// A space is the address space of a process.
type space struct {
	memory   *memory
	mappings []mapping
	table    map[int]entry
	faults   int
	nextPage int
}

func newSpace() *space {
	return &space{
		memory:   &memory{zero: &frame{refs: 1}},
		table:    map[int]entry{},
		nextPage: 16,
	}
}

Listing 7.14 Frames, mappings, entries and the address space, in Rust. code/rust/vmmodel/src/main.rs

const PAGE_SIZE: usize = 4096;

/// The result of an access that no mapping allows. The kernel sends the
/// process the signal `SIGSEGV`.
#[derive(Debug)]
struct Segv;

impl fmt::Display for Segv {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("SIGSEGV")
    }
}

/// A page of physical memory. The reference count of the `Rc` is the number
/// of page table entries that refer to it.
type Frame = Rc<[u8; PAGE_SIZE]>;

/// The physical memory shared by all address spaces: the number of frames
/// allocated, and the frame of zeros that every read of an untouched page
/// maps.
struct Memory {
    frames: Cell<usize>,
    zero: Frame,
}

impl Memory {
    fn allocate(&self, contents: [u8; PAGE_SIZE]) -> Frame {
        self.frames.set(self.frames.get() + 1);

        Rc::new(contents)
    }
}

/// A range of virtual pages and the access it permits.
#[derive(Clone, Copy)]
struct Mapping {
    start: usize,
    pages: usize,
    writable: bool,
}

/// A page table entry: the frame behind a virtual page, and whether the
/// processor may write to it without a fault.
#[derive(Clone)]
struct Entry {
    frame: Frame,
    writable: bool,
}

/// The address space of a process.
struct Space {
    memory: Rc<Memory>,
    mappings: Vec<Mapping>,
    table: HashMap<usize, Entry>,
    faults: usize,
    next_page: usize,
}

impl Space {
    fn new() -> Space {
        let memory = Memory {
            frames: Cell::new(0),
            zero: Rc::new([0; PAGE_SIZE]),
        };

        Space {
            memory: Rc::new(memory),
            mappings: Vec::new(),
            table: HashMap::new(),
            faults: 0,
            next_page: 16,
        }
    }
}

mmap only records the range.

Listing 7.15 Adding a mapping, in Go. code/go/vmmodel/main.go

// mmap records a new mapping and returns its address. It allocates no frame
// and writes no page table entry.
func (s *space) mmap(pages int, writable bool) int {
	start := s.nextPage
	s.mappings = append(s.mappings, mapping{start: start, pages: pages, writable: writable})
	s.nextPage += pages + 1

	return start * pageSize
}

// find returns the mapping that contains a page.
func (s *space) find(page int) (mapping, bool) {
	for _, m := range s.mappings {
		if page >= m.start && page < m.start+m.pages {
			return m, true
		}
	}

	return mapping{}, false
}

Listing 7.16 Adding a mapping, in Rust. code/rust/vmmodel/src/main.rs

impl Space {
    /// Records a new mapping and returns its address. It allocates no frame
    /// and writes no page table entry.
    fn mmap(&mut self, pages: usize, writable: bool) -> usize {
        let start = self.next_page;

        self.mappings.push(Mapping {
            start,
            pages,
            writable,
        });
        self.next_page += pages + 1;

        start * PAGE_SIZE
    }

    /// Returns the mapping that contains a page.
    fn find(&self, page: usize) -> Option<Mapping> {
        self.mappings
            .iter()
            .copied()
            .find(|mapping| (mapping.start..mapping.start + mapping.pages).contains(&page))
    }
}

read and write play the part of the processor. They consult the page table and call fault when the entry is missing or forbids the access.

Listing 7.17 An access through the page table, in Go. code/go/vmmodel/main.go

// read returns the byte at an address. The processor uses the page table
// entry when one exists, and faults when none does.
func (s *space) read(address int) (byte, error) {
	page, offset := address/pageSize, address%pageSize

	if _, present := s.table[page]; !present {
		if err := s.fault(page, false); err != nil {
			return 0, err
		}
	}

	return s.table[page].frame.data[offset], nil
}

// write stores a byte at an address. It faults when the page has no entry or
// the entry does not permit writing.
func (s *space) write(address int, value byte) error {
	page, offset := address/pageSize, address%pageSize

	if !s.table[page].writable {
		if err := s.fault(page, true); err != nil {
			return err
		}
	}

	s.table[page].frame.data[offset] = value

	return nil
}

Listing 7.18 An access through the page table, in Rust. code/rust/vmmodel/src/main.rs

impl Space {
    /// Returns the byte at an address. The processor uses the page table
    /// entry when one exists, and faults when none does.
    fn read(&mut self, address: usize) -> Result<u8, Segv> {
        let (page, offset) = (address / PAGE_SIZE, address % PAGE_SIZE);

        if !self.table.contains_key(&page) {
            self.fault(page, false)?;
        }

        self.table
            .get(&page)
            .map(|entry| entry.frame[offset])
            .ok_or(Segv)
    }

    /// Stores a byte at an address. It faults when the page has no entry or
    /// the entry does not permit writing.
    fn write(&mut self, address: usize, value: u8) -> Result<(), Segv> {
        let (page, offset) = (address / PAGE_SIZE, address % PAGE_SIZE);

        if !self.table.get(&page).is_some_and(|entry| entry.writable) {
            self.fault(page, true)?;
        }

        let entry = self.table.get_mut(&page).ok_or(Segv)?;

        // A writable entry is the only user of its frame, so `make_mut`
        // returns the frame itself.
        Rc::make_mut(&mut entry.frame)[offset] = value;

        Ok(())
    }
}

fault is the kernel’s handler. After the check against the mappings it has 4 cases.

AccessEntryAction
readnonemap the shared zero frame, read-only
writenoneallocate a frame of zeros, writable
writeread-only, and the frame has no other usermake the entry writable
writeread-only, and the frame is sharedallocate a frame, copy the contents, map the copy writable

Case 1 explains the measurement of 11 resident pages. Every read of an untouched anonymous page maps the same frame of zeros, so reading costs no memory. The frame is mapped read-only, and the first write replaces it with a private frame through case 4.

Listing 7.19 The page fault handler, in Go. code/go/vmmodel/main.go

// fault is the kernel's page fault handler. It checks the access against
// the mappings and then repairs the page table entry so that the access can
// be repeated.
func (s *space) fault(page int, write bool) error {
	m, ok := s.find(page)

	if !ok || write && !m.writable {
		return errSegv
	}

	s.faults++
	e, present := s.table[page]

	switch {
	case !write:
		// A read of an untouched page maps the shared zero frame.
		s.memory.zero.refs++
		s.table[page] = entry{frame: s.memory.zero}

	case !present:
		// A write to an untouched page gets a new frame of zeros.
		s.table[page] = entry{frame: s.memory.allocate(), writable: true}

	case e.frame.refs == 1:
		// The frame has no other user. Writing is enabled in place.
		s.table[page] = entry{frame: e.frame, writable: true}

	default:
		// The frame is shared. The writer gets a private copy.
		private := s.memory.allocate()
		private.data = e.frame.data
		e.frame.refs--
		s.table[page] = entry{frame: private, writable: true}
	}

	return nil
}

Listing 7.20 The page fault handler, in Rust. code/rust/vmmodel/src/main.rs

impl Space {
    /// The kernel's page fault handler. It checks the access against the
    /// mappings and then repairs the page table entry so that the access can
    /// be repeated.
    fn fault(&mut self, page: usize, write: bool) -> Result<(), Segv> {
        let mapping = self.find(page).ok_or(Segv)?;

        if write && !mapping.writable {
            return Err(Segv);
        }

        self.faults += 1;

        let entry = match (write, self.table.remove(&page)) {
            // A read of an untouched page maps the shared zero frame.
            (false, _) => Entry {
                frame: Rc::clone(&self.memory.zero),
                writable: false,
            },

            // A write to an untouched page gets a new frame of zeros.
            (true, None) => Entry {
                frame: self.memory.allocate([0; PAGE_SIZE]),
                writable: true,
            },

            // The frame has no other user. Writing is enabled in place.
            (true, Some(entry)) if Rc::strong_count(&entry.frame) == 1 => Entry {
                frame: entry.frame,
                writable: true,
            },

            // The frame is shared. The writer gets a private copy.
            (true, Some(entry)) => Entry {
                frame: self.memory.allocate(*entry.frame),
                writable: true,
            },
        };

        self.table.insert(page, entry);

        Ok(())
    }
}

Copy-on-write

fork gives the child a copy of the parent’s address space. Copying every frame would be slow, and most of the work would be wasted, since the usual next step of the child is exec, which discards the address space. The kernel therefore copies only the page table. Parent and child refer to the same frames, and every entry is made read-only in both.

Listing 7.21 Forking an address space, in Go. code/go/vmmodel/main.go

// fork returns a child with the same mappings and a page table that refers
// to the same frames. Every entry is made read-only in the parent and in the
// child, so that the first write by either causes a fault.
func (s *space) fork() *space {
	child := &space{
		memory:   s.memory,
		mappings: append([]mapping(nil), s.mappings...),
		table:    map[int]entry{},
		nextPage: s.nextPage,
	}

	for page, e := range s.table {
		e.frame.refs++
		e.writable = false
		s.table[page], child.table[page] = e, e
	}

	return child
}

Listing 7.22 Forking an address space, in Rust. code/rust/vmmodel/src/main.rs

impl Space {
    /// Returns a child with the same mappings and a page table that refers to
    /// the same frames. Every entry is made read-only in the parent and in
    /// the child, so that the first write by either causes a fault.
    fn fork(&mut self) -> Space {
        for entry in self.table.values_mut() {
            entry.writable = false;
        }

        Space {
            memory: Rc::clone(&self.memory),
            mappings: self.mappings.clone(),
            table: self.table.clone(),
            faults: 0,
            next_page: self.next_page,
        }
    }
}

Reads continue without faults. The first write to a page by either process faults. The handler sees a read-only entry in a writable mapping, copies the frame for the writer, and the processes diverge on that page only.

After fork, page 0 of the parent and page 0 of the child both refer to frame 1, which holds a, and both entries are read-only. After the child writes b, the child's entry is writable and refers to a new frame 3 that holds b. The parent's entry still refers to frame 1.
Figure 7.9 A frame is shared after fork and copied at the first write.

A private file mapping uses the same mechanism. Its pages initially refer to the file’s frames, read-only, and a write copies the page. The file is unchanged, as vmfacts measured.

Listing 7.23 The trace, in Go. code/go/vmmodel/main.go

// report prints a step with the fault count of a space and the number of
// frames allocated.
func (s *space) report(step string) {
	fmt.Printf("%-34s faults %d, frames %d\n", step, s.faults, s.memory.frames)
}

func run() error {
	parent := newSpace()
	base := parent.mmap(4, true)
	parent.report("mmap of 4 pages:")

	if err := parent.write(base, 'a'); err != nil {
		return err
	}

	parent.report("write to page 0:")

	value, err := parent.read(base + pageSize)

	if err != nil {
		return err
	}

	parent.report(fmt.Sprintf("read of page 1, value %d:", value))

	if err := parent.write(base+pageSize, 'x'); err != nil {
		return err
	}

	parent.report("write to page 1:")

	child := parent.fork()
	child.report("fork, in the child:")

	if err := child.write(base, 'b'); err != nil {
		return err
	}

	child.report("child writes page 0:")

	if err := parent.write(base, 'c'); err != nil {
		return err
	}

	parent.report("parent writes page 0:")

	inParent, err := parent.read(base)

	if err != nil {
		return err
	}

	inChild, err := child.read(base)

	if err != nil {
		return err
	}

	fmt.Printf("page 0 holds %q in the parent and %q in the child\n", inParent, inChild)

	fmt.Printf("write outside every mapping: %v\n", parent.write(base+100*pageSize, 1))

	readOnly := parent.mmap(1, false)
	fmt.Printf("write to a read-only mapping: %v\n", parent.write(readOnly, 1))

	return nil
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

Listing 7.24 The trace, in Rust. code/rust/vmmodel/src/main.rs

impl Space {
    /// Prints a step with the fault count of a space and the number of frames
    /// allocated.
    fn report(&self, step: &str) {
        println!(
            "{step:<34} faults {}, frames {}",
            self.faults,
            self.memory.frames.get()
        );
    }
}

fn run() -> Result<(), Segv> {
    let mut parent = Space::new();
    let base = parent.mmap(4, true);
    parent.report("mmap of 4 pages:");

    parent.write(base, b'a')?;
    parent.report("write to page 0:");

    let value = parent.read(base + PAGE_SIZE)?;
    parent.report(&format!("read of page 1, value {value}:"));

    parent.write(base + PAGE_SIZE, b'x')?;
    parent.report("write to page 1:");

    let mut child = parent.fork();
    child.report("fork, in the child:");

    child.write(base, b'b')?;
    child.report("child writes page 0:");

    parent.write(base, b'c')?;
    parent.report("parent writes page 0:");

    println!(
        "page 0 holds {:?} in the parent and {:?} in the child",
        char::from(parent.read(base)?),
        char::from(child.read(base)?)
    );

    if let Err(error) = parent.write(base + 100 * PAGE_SIZE, 1) {
        println!("write outside every mapping: {error}");
    }

    let read_only = parent.mmap(1, false);

    if let Err(error) = parent.write(read_only, 1) {
        println!("write to a read-only mapping: {error}");
    }

    Ok(())
}

fn main() {
    if let Err(error) = run() {
        println!("{error}");
    }
}
mmap of 4 pages:                   faults 0, frames 0
write to page 0:                   faults 1, frames 1
read of page 1, value 0:           faults 2, frames 1
write to page 1:                   faults 3, frames 2
fork, in the child:                faults 0, frames 2
child writes page 0:               faults 1, frames 3
parent writes page 0:              faults 4, frames 3
page 0 holds 'c' in the parent and 'b' in the child
write outside every mapping: SIGSEGV
write to a read-only mapping: SIGSEGV
LineWhat it shows
mmap of 4 pagesNo fault and no frame.
write to page 0A write to an untouched page allocates frame 1.
read of page 1A read maps the zero frame: a fault, and no new frame.
write to page 1The zero frame is shared, so the write copies it into frame 2.
fork, in the childThe child starts with the parent’s 2 frames and none of its own.
child writes page 0The child faults and receives a copy, frame 3.
parent writes page 0The parent also faults, because its entry is read-only. It is now the only user of frame 1, so no copy is made.
the last 2 linesAn address outside every mapping, and a write to a read-only mapping, end in SIGSEGV.

Correspondence with the kernel source. A mapping is struct vm_area_struct in include/linux/mm_types.h, and the list of mappings is in struct mm_struct. On arm64 the page table is a tree of 4 levels that the processor walks itself; the model replaces the tree by a map from page number to entry. The fault handler is handle_mm_fault in mm/memory.c, where do_anonymous_page handles the first 2 cases of the table and do_wp_page the last 2. /proc/self/maps prints a line for each vm_area_struct.

How much memory a process uses

Since mapping and allocating are separate, a process has 2 sizes.

$ ps -o pid,vsz,rss,min_flt,maj_flt,comm -p 580844
    PID    VSZ   RSS  MINFL  MAJFL COMMAND
 580844  11204   776    114      0 sleep
ColumnMeaning
VSZVirtual size: the total of all mappings, 11204 kB. It is total_vm.
RSSResident set size: the pages that have a frame, 776 kB.
MINFLMinor faults: page faults that the kernel resolved without reading from a disk, 114.
MAJFLMajor faults: page faults that had to wait for a page to be read from a disk, 0.
2 gauges for a sleep process. VSZ, the addresses mapped, is 11204 kB. RSS, the pages that have a frame, is 776 kB, 7 percent of the mapped size.
Figure 7.10 The 2 sizes of the sleep process, to scale.

The sleep process has mapped 11 MB and uses under 1 MB, because most of the pages of its libraries have not been touched. It took 114 page faults to start, each bringing in a single page or a few. RSS is the number to look at when asking how much memory a process costs, with the reservation that pages shared with other processes, such as the code of libc, are counted in the RSS of each.

Because memory is given out at the first access, the kernel can promise more than it has. This is overcommit: the sum of the mappings of all processes may exceed physical memory and swap, on the assumption that not all of it will be touched. The policy is in /proc/sys/vm/overcommit_memory, and the value 1 on this machine means that mmap does not refuse a request for lack of memory. When the promises are called in and no frame is free, the kernel first reclaims frames, by discarding page cache pages (chapter 8) and by writing anonymous pages to swap space on disk. When that is not enough, the OOM killer ends a process to free its memory. Chapter 12 shows it at work inside a memory limit.

The heap and the stack

malloc is a library function, and the kernel knows nothing about individual allocations. The allocator asks the kernel for memory in large pieces and divides them itself. It has 2 ways to ask. brk moves the end of the [heap] mapping, which suits many small allocations. For a large request, 128 KiB or more in the allocator of glibc, it calls mmap for an anonymous mapping of its own, which can be returned to the kernel independently. Both give addresses without frames, so memory that a program allocates and does not touch costs nothing.

The stack is a mapping that the kernel extends on demand. An access directly below its current end causes a page fault, and the handler enlarges the mapping, up to the limit shown by ulimit -s.

Inspecting memory on a live system

CommandShows
cat /proc/<pid>/mapsthe mappings of a process
cat /proc/<pid>/smapsfor each mapping: resident, shared, private and swapped pages
ps -o pid,vsz,rss,min_flt,maj_flt,commvirtual size, resident size and page faults
grep Vm /proc/<pid>/statusthe sizes by kind: data, stack, code, libraries, page tables
free -mmemory of the whole system: used, free, and used for the page cache
cat /proc/sys/vm/overcommit_memorythe overcommit policy

The complete programs

Listing 7.25 The complete vmfacts, in Go. code/go/vmfacts/main.go

// Command vmfacts measures how the kernel fills an address space: when a
// mapped page receives physical memory, what an untouched page contains, how
// a mapping's permissions appear in /proc/self/maps, and where a write
// through a private or a shared file mapping ends up.
//
// Run it from code/go with:
//
//	go run ./vmfacts
package main

import (
	"bufio"
	"encoding/binary"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"reflect"
	"strings"

	"golang.org/x/sys/unix"
)


const pageSize = 4096

// address returns the virtual address of the first byte of a mapping.
func address(mapping []byte) uintptr {
	return reflect.ValueOf(mapping).Pointer()
}

// resident counts the pages of a mapping that have physical memory. The
// kernel publishes 8 bytes per virtual page in /proc/self/pagemap, and bit 63
// of an entry is set when the page is present in memory.
func resident(mapping []byte) (int, error) {
	pagemap, err := os.Open("/proc/self/pagemap")

	if err != nil {
		return 0, err
	}

	defer pagemap.Close()

	entries := make([]byte, 8*len(mapping)/pageSize)

	if _, err := pagemap.ReadAt(entries, int64(address(mapping)/pageSize*8)); err != nil {
		return 0, err
	}

	count := 0

	for i := 0; i < len(entries); i += 8 {
		if binary.LittleEndian.Uint64(entries[i:])>>63 == 1 {
			count++
		}
	}

	return count, nil
}


// demand maps 64 pages of anonymous memory, writes to 10 of them, reads 1
// more, and counts the resident pages after each step.
func demand() error {
	mapping, err := unix.Mmap(-1, 0, 64*pageSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS)

	if err != nil {
		return err
	}

	defer unix.Munmap(mapping)

	before, err := resident(mapping)

	if err != nil {
		return err
	}

	for page := range 10 {
		mapping[page*pageSize] = 1
	}

	written, err := resident(mapping)

	if err != nil {
		return err
	}

	value := mapping[20*pageSize]
	read, err := resident(mapping)

	if err != nil {
		return err
	}

	fmt.Printf("64 pages mapped: %d resident\n", before)
	fmt.Printf("after writing to 10 pages: %d resident\n", written)
	fmt.Printf("after reading 1 more page: %d resident, the byte read is %d\n", read, value)

	return nil
}


// permissions returns the permission column of the line in /proc/self/maps
// that starts at the address of a mapping.
func permissions(mapping []byte) (string, error) {
	maps, err := os.Open("/proc/self/maps")

	if err != nil {
		return "", err
	}

	defer maps.Close()

	start := fmt.Sprintf("%x-", address(mapping))

	for lines := bufio.NewScanner(maps); lines.Scan(); {
		if fields := strings.Fields(lines.Text()); strings.HasPrefix(fields[0], start) {
			return fields[1], nil
		}
	}

	return "", fmt.Errorf("mapping at %s not found", start)
}

// protect reads the permissions of a mapping before and after mprotect
// removes the permission to write.
func protect() error {
	mapping, err := unix.Mmap(-1, 0, pageSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_PRIVATE|unix.MAP_ANONYMOUS)

	if err != nil {
		return err
	}

	defer unix.Munmap(mapping)

	before, err := permissions(mapping)

	if err != nil {
		return err
	}

	if err := unix.Mprotect(mapping, unix.PROT_READ); err != nil {
		return err
	}

	after, err := permissions(mapping)

	if err != nil {
		return err
	}

	fmt.Printf("permissions of the mapping: %s, after mprotect: %s\n", before, after)

	return nil
}


// throughMapping maps a file that holds "hello", writes "HELLO" through the
// mapping, unmaps it, and returns the contents of the file.
func throughMapping(path string, flags int) (string, error) {
	if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil {
		return "", err
	}

	file, err := os.OpenFile(path, os.O_RDWR, 0)

	if err != nil {
		return "", err
	}

	defer file.Close()

	mapping, err := unix.Mmap(int(file.Fd()), 0, 5, unix.PROT_READ|unix.PROT_WRITE, flags)

	if err != nil {
		return "", err
	}

	copy(mapping, "HELLO")

	if err := unix.Munmap(mapping); err != nil {
		return "", err
	}

	text, err := os.ReadFile(path)

	return string(text), err
}

// files compares a private file mapping with a shared one.
func files() error {
	dir, err := os.MkdirTemp("", "vmfacts")

	if err != nil {
		return err
	}

	defer os.RemoveAll(dir)

	path := filepath.Join(dir, "file")

	for _, kind := range []struct {
		name  string
		flags int
	}{{"private", unix.MAP_PRIVATE}, {"shared", unix.MAP_SHARED}} {
		text, err := throughMapping(path, kind.flags)

		if err != nil {
			return err
		}

		fmt.Printf("HELLO written through a %s file mapping: the file holds %q\n", kind.name, text)
	}

	return nil
}


func main() {
	for _, experiment := range []func() error{demand, protect, files} {
		if err := experiment(); err != nil {
			log.Fatal(err)
		}
	}
}

Listing 7.26 The complete vmfacts, in Rust. code/rust/vmfacts/src/main.rs

//! Measures how the kernel fills an address space: when a mapped page
//! receives physical memory, what an untouched page contains, how a mapping's
//! permissions appear in `/proc/self/maps`, and where a write through a
//! private or a shared file mapping ends up.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p vmfacts
//! ```

use std::fs::{self, File, OpenOptions};
use std::io;
use std::os::unix::fs::FileExt;
use std::path::Path;

use memmap2::{MmapMut, MmapOptions};

const PAGE_SIZE: usize = 4096;

/// Counts the pages of a mapping that have physical memory. The kernel
/// publishes 8 bytes per virtual page in `/proc/self/pagemap`, and bit 63 of
/// an entry is set when the page is present in memory.
fn resident(mapping: &[u8]) -> io::Result<usize> {
    let pagemap = File::open("/proc/self/pagemap")?;
    let mut entries = vec![0u8; 8 * mapping.len() / PAGE_SIZE];
    let offset = mapping.as_ptr() as usize / PAGE_SIZE * 8;

    pagemap.read_exact_at(&mut entries, offset as u64)?;

    let (entries, _) = entries.as_chunks::<8>();
    let count = entries.iter().filter(|entry| entry[7] >> 7 == 1).count();

    Ok(count)
}

/// Maps 64 pages of anonymous memory, writes to 10 of them, reads 1 more, and
/// counts the resident pages after each step.
fn demand() -> io::Result<()> {
    let mut mapping = MmapMut::map_anon(64 * PAGE_SIZE)?;

    let before = resident(&mapping)?;

    for page in 0..10 {
        mapping[page * PAGE_SIZE] = 1;
    }

    let written = resident(&mapping)?;

    let value = mapping[20 * PAGE_SIZE];
    let read = resident(&mapping)?;

    println!("64 pages mapped: {before} resident");
    println!("after writing to 10 pages: {written} resident");
    println!("after reading 1 more page: {read} resident, the byte read is {value}");

    Ok(())
}

/// Returns the permission column of the line in `/proc/self/maps` that starts
/// at the address of a mapping.
fn permissions(mapping: &[u8]) -> io::Result<String> {
    let start = format!("{:x}-", mapping.as_ptr() as usize);

    fs::read_to_string("/proc/self/maps")?
        .lines()
        .filter(|line| line.starts_with(&start))
        .find_map(|line| line.split_whitespace().nth(1))
        .map(str::to_owned)
        .ok_or_else(|| io::ErrorKind::NotFound.into())
}

/// Reads the permissions of a mapping before and after mprotect removes the
/// permission to write.
fn protect() -> io::Result<()> {
    let mapping = MmapMut::map_anon(PAGE_SIZE)?;
    let before = permissions(&mapping)?;

    let mapping = mapping.make_read_only()?;
    let after = permissions(&mapping)?;

    println!("permissions of the mapping: {before}, after mprotect: {after}");

    Ok(())
}

/// Maps a file that holds "hello", writes "HELLO" through the mapping, unmaps
/// it, and returns the contents of the file.
fn through_mapping(path: &Path, shared: bool) -> io::Result<String> {
    fs::write(path, "hello")?;

    let file = OpenOptions::new().read(true).write(true).open(path)?;
    let options = MmapOptions::new();

    // SAFETY: mapping a file is unsafe in Rust because another process could
    // change the file while the slice exists. This file is in a directory the
    // program has just created, and nothing else opens it.
    let mut mapping = unsafe {
        if shared {
            options.map_mut(&file)?
        } else {
            options.map_copy(&file)?
        }
    };

    mapping.copy_from_slice(b"HELLO");
    drop(mapping);

    fs::read_to_string(path)
}

/// Compares a private file mapping with a shared one.
fn files() -> io::Result<()> {
    let dir = std::env::temp_dir().join(format!("vmfacts-{}", std::process::id()));
    let path = dir.join("file");

    fs::create_dir_all(&dir)?;

    for (name, shared) in [("private", false), ("shared", true)] {
        let text = through_mapping(&path, shared)?;
        println!("HELLO written through a {name} file mapping: the file holds {text:?}");
    }

    fs::remove_dir_all(&dir)
}

fn main() -> io::Result<()> {
    demand()?;
    protect()?;
    files()
}

Listing 7.27 The complete vmmodel, in Go. code/go/vmmodel/main.go

// Command vmmodel models an address space: a list of mappings, a page table
// that is filled on demand by page faults, a shared zero page, and
// copy-on-write after fork.
//
// Run it from code/go with:
//
//	go run ./vmmodel
package main

import (
	"errors"
	"fmt"
)


const pageSize = 4096

// errSegv is the result of an access that no mapping allows. The kernel
// sends the process the signal SIGSEGV.
var errSegv = errors.New("SIGSEGV")

// A frame is a page of physical memory. refs counts the page table entries
// that refer to it.
type frame struct {
	data [pageSize]byte
	refs int
}

// memory is the physical memory shared by all address spaces: the number of
// frames allocated, and the frame of zeros that every read of an untouched
// page maps.
type memory struct {
	frames int
	zero   *frame
}

func (m *memory) allocate() *frame {
	m.frames++

	return &frame{refs: 1}
}

// A mapping is a range of virtual pages and the access it permits.
type mapping struct {
	start, pages int
	writable     bool
}

// An entry is a page table entry: the frame behind a virtual page, and
// whether the processor may write to it without a fault.
type entry struct {
	frame    *frame
	writable bool
}

// A space is the address space of a process.
type space struct {
	memory   *memory
	mappings []mapping
	table    map[int]entry
	faults   int
	nextPage int
}

func newSpace() *space {
	return &space{
		memory:   &memory{zero: &frame{refs: 1}},
		table:    map[int]entry{},
		nextPage: 16,
	}
}


// mmap records a new mapping and returns its address. It allocates no frame
// and writes no page table entry.
func (s *space) mmap(pages int, writable bool) int {
	start := s.nextPage
	s.mappings = append(s.mappings, mapping{start: start, pages: pages, writable: writable})
	s.nextPage += pages + 1

	return start * pageSize
}

// find returns the mapping that contains a page.
func (s *space) find(page int) (mapping, bool) {
	for _, m := range s.mappings {
		if page >= m.start && page < m.start+m.pages {
			return m, true
		}
	}

	return mapping{}, false
}


// read returns the byte at an address. The processor uses the page table
// entry when one exists, and faults when none does.
func (s *space) read(address int) (byte, error) {
	page, offset := address/pageSize, address%pageSize

	if _, present := s.table[page]; !present {
		if err := s.fault(page, false); err != nil {
			return 0, err
		}
	}

	return s.table[page].frame.data[offset], nil
}

// write stores a byte at an address. It faults when the page has no entry or
// the entry does not permit writing.
func (s *space) write(address int, value byte) error {
	page, offset := address/pageSize, address%pageSize

	if !s.table[page].writable {
		if err := s.fault(page, true); err != nil {
			return err
		}
	}

	s.table[page].frame.data[offset] = value

	return nil
}


// fault is the kernel's page fault handler. It checks the access against
// the mappings and then repairs the page table entry so that the access can
// be repeated.
func (s *space) fault(page int, write bool) error {
	m, ok := s.find(page)

	if !ok || write && !m.writable {
		return errSegv
	}

	s.faults++
	e, present := s.table[page]

	switch {
	case !write:
		// A read of an untouched page maps the shared zero frame.
		s.memory.zero.refs++
		s.table[page] = entry{frame: s.memory.zero}

	case !present:
		// A write to an untouched page gets a new frame of zeros.
		s.table[page] = entry{frame: s.memory.allocate(), writable: true}

	case e.frame.refs == 1:
		// The frame has no other user. Writing is enabled in place.
		s.table[page] = entry{frame: e.frame, writable: true}

	default:
		// The frame is shared. The writer gets a private copy.
		private := s.memory.allocate()
		private.data = e.frame.data
		e.frame.refs--
		s.table[page] = entry{frame: private, writable: true}
	}

	return nil
}


// fork returns a child with the same mappings and a page table that refers
// to the same frames. Every entry is made read-only in the parent and in the
// child, so that the first write by either causes a fault.
func (s *space) fork() *space {
	child := &space{
		memory:   s.memory,
		mappings: append([]mapping(nil), s.mappings...),
		table:    map[int]entry{},
		nextPage: s.nextPage,
	}

	for page, e := range s.table {
		e.frame.refs++
		e.writable = false
		s.table[page], child.table[page] = e, e
	}

	return child
}


// report prints a step with the fault count of a space and the number of
// frames allocated.
func (s *space) report(step string) {
	fmt.Printf("%-34s faults %d, frames %d\n", step, s.faults, s.memory.frames)
}

func run() error {
	parent := newSpace()
	base := parent.mmap(4, true)
	parent.report("mmap of 4 pages:")

	if err := parent.write(base, 'a'); err != nil {
		return err
	}

	parent.report("write to page 0:")

	value, err := parent.read(base + pageSize)

	if err != nil {
		return err
	}

	parent.report(fmt.Sprintf("read of page 1, value %d:", value))

	if err := parent.write(base+pageSize, 'x'); err != nil {
		return err
	}

	parent.report("write to page 1:")

	child := parent.fork()
	child.report("fork, in the child:")

	if err := child.write(base, 'b'); err != nil {
		return err
	}

	child.report("child writes page 0:")

	if err := parent.write(base, 'c'); err != nil {
		return err
	}

	parent.report("parent writes page 0:")

	inParent, err := parent.read(base)

	if err != nil {
		return err
	}

	inChild, err := child.read(base)

	if err != nil {
		return err
	}

	fmt.Printf("page 0 holds %q in the parent and %q in the child\n", inParent, inChild)

	fmt.Printf("write outside every mapping: %v\n", parent.write(base+100*pageSize, 1))

	readOnly := parent.mmap(1, false)
	fmt.Printf("write to a read-only mapping: %v\n", parent.write(readOnly, 1))

	return nil
}

func main() {
	if err := run(); err != nil {
		fmt.Println(err)
	}
}

Listing 7.28 The complete vmmodel, in Rust. code/rust/vmmodel/src/main.rs

//! Models an address space: a list of mappings, a page table that is filled
//! on demand by page faults, a shared zero page, and copy-on-write after
//! fork.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p vmmodel
//! ```

use std::cell::Cell;
use std::collections::HashMap;
use std::fmt;
use std::rc::Rc;

const PAGE_SIZE: usize = 4096;

/// The result of an access that no mapping allows. The kernel sends the
/// process the signal `SIGSEGV`.
#[derive(Debug)]
struct Segv;

impl fmt::Display for Segv {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("SIGSEGV")
    }
}

/// A page of physical memory. The reference count of the `Rc` is the number
/// of page table entries that refer to it.
type Frame = Rc<[u8; PAGE_SIZE]>;

/// The physical memory shared by all address spaces: the number of frames
/// allocated, and the frame of zeros that every read of an untouched page
/// maps.
struct Memory {
    frames: Cell<usize>,
    zero: Frame,
}

impl Memory {
    fn allocate(&self, contents: [u8; PAGE_SIZE]) -> Frame {
        self.frames.set(self.frames.get() + 1);

        Rc::new(contents)
    }
}

/// A range of virtual pages and the access it permits.
#[derive(Clone, Copy)]
struct Mapping {
    start: usize,
    pages: usize,
    writable: bool,
}

/// A page table entry: the frame behind a virtual page, and whether the
/// processor may write to it without a fault.
#[derive(Clone)]
struct Entry {
    frame: Frame,
    writable: bool,
}

/// The address space of a process.
struct Space {
    memory: Rc<Memory>,
    mappings: Vec<Mapping>,
    table: HashMap<usize, Entry>,
    faults: usize,
    next_page: usize,
}

impl Space {
    fn new() -> Space {
        let memory = Memory {
            frames: Cell::new(0),
            zero: Rc::new([0; PAGE_SIZE]),
        };

        Space {
            memory: Rc::new(memory),
            mappings: Vec::new(),
            table: HashMap::new(),
            faults: 0,
            next_page: 16,
        }
    }
}

impl Space {
    /// Records a new mapping and returns its address. It allocates no frame
    /// and writes no page table entry.
    fn mmap(&mut self, pages: usize, writable: bool) -> usize {
        let start = self.next_page;

        self.mappings.push(Mapping {
            start,
            pages,
            writable,
        });
        self.next_page += pages + 1;

        start * PAGE_SIZE
    }

    /// Returns the mapping that contains a page.
    fn find(&self, page: usize) -> Option<Mapping> {
        self.mappings
            .iter()
            .copied()
            .find(|mapping| (mapping.start..mapping.start + mapping.pages).contains(&page))
    }
}

impl Space {
    /// Returns the byte at an address. The processor uses the page table
    /// entry when one exists, and faults when none does.
    fn read(&mut self, address: usize) -> Result<u8, Segv> {
        let (page, offset) = (address / PAGE_SIZE, address % PAGE_SIZE);

        if !self.table.contains_key(&page) {
            self.fault(page, false)?;
        }

        self.table
            .get(&page)
            .map(|entry| entry.frame[offset])
            .ok_or(Segv)
    }

    /// Stores a byte at an address. It faults when the page has no entry or
    /// the entry does not permit writing.
    fn write(&mut self, address: usize, value: u8) -> Result<(), Segv> {
        let (page, offset) = (address / PAGE_SIZE, address % PAGE_SIZE);

        if !self.table.get(&page).is_some_and(|entry| entry.writable) {
            self.fault(page, true)?;
        }

        let entry = self.table.get_mut(&page).ok_or(Segv)?;

        // A writable entry is the only user of its frame, so `make_mut`
        // returns the frame itself.
        Rc::make_mut(&mut entry.frame)[offset] = value;

        Ok(())
    }
}

impl Space {
    /// The kernel's page fault handler. It checks the access against the
    /// mappings and then repairs the page table entry so that the access can
    /// be repeated.
    fn fault(&mut self, page: usize, write: bool) -> Result<(), Segv> {
        let mapping = self.find(page).ok_or(Segv)?;

        if write && !mapping.writable {
            return Err(Segv);
        }

        self.faults += 1;

        let entry = match (write, self.table.remove(&page)) {
            // A read of an untouched page maps the shared zero frame.
            (false, _) => Entry {
                frame: Rc::clone(&self.memory.zero),
                writable: false,
            },

            // A write to an untouched page gets a new frame of zeros.
            (true, None) => Entry {
                frame: self.memory.allocate([0; PAGE_SIZE]),
                writable: true,
            },

            // The frame has no other user. Writing is enabled in place.
            (true, Some(entry)) if Rc::strong_count(&entry.frame) == 1 => Entry {
                frame: entry.frame,
                writable: true,
            },

            // The frame is shared. The writer gets a private copy.
            (true, Some(entry)) => Entry {
                frame: self.memory.allocate(*entry.frame),
                writable: true,
            },
        };

        self.table.insert(page, entry);

        Ok(())
    }
}

impl Space {
    /// Returns a child with the same mappings and a page table that refers to
    /// the same frames. Every entry is made read-only in the parent and in
    /// the child, so that the first write by either causes a fault.
    fn fork(&mut self) -> Space {
        for entry in self.table.values_mut() {
            entry.writable = false;
        }

        Space {
            memory: Rc::clone(&self.memory),
            mappings: self.mappings.clone(),
            table: self.table.clone(),
            faults: 0,
            next_page: self.next_page,
        }
    }
}

impl Space {
    /// Prints a step with the fault count of a space and the number of frames
    /// allocated.
    fn report(&self, step: &str) {
        println!(
            "{step:<34} faults {}, frames {}",
            self.faults,
            self.memory.frames.get()
        );
    }
}

fn run() -> Result<(), Segv> {
    let mut parent = Space::new();
    let base = parent.mmap(4, true);
    parent.report("mmap of 4 pages:");

    parent.write(base, b'a')?;
    parent.report("write to page 0:");

    let value = parent.read(base + PAGE_SIZE)?;
    parent.report(&format!("read of page 1, value {value}:"));

    parent.write(base + PAGE_SIZE, b'x')?;
    parent.report("write to page 1:");

    let mut child = parent.fork();
    child.report("fork, in the child:");

    child.write(base, b'b')?;
    child.report("child writes page 0:");

    parent.write(base, b'c')?;
    parent.report("parent writes page 0:");

    println!(
        "page 0 holds {:?} in the parent and {:?} in the child",
        char::from(parent.read(base)?),
        char::from(child.read(base)?)
    );

    if let Err(error) = parent.write(base + 100 * PAGE_SIZE, 1) {
        println!("write outside every mapping: {error}");
    }

    let read_only = parent.mmap(1, false);

    if let Err(error) = parent.write(read_only, 1) {
        println!("write to a read-only mapping: {error}");
    }

    Ok(())
}

fn main() {
    if let Err(error) = run() {
        println!("{error}");
    }
}

Summary

  • Each process has its own virtual addresses, translated to physical frames through a page table.
  • mmap records a mapping and allocates nothing. A page receives a frame at its first access, through a page fault.
  • A read of an untouched anonymous page maps a shared frame of zeros. The first write replaces it with a private frame.
  • A write through a shared file mapping reaches the file. A write through a private mapping changes a copy.
  • fork copies the page table and shares the frames read-only. A frame is copied when one side first writes to it.
  • An access that no mapping permits ends in SIGSEGV.

References