Page Fault

Access to an un-mapped virtual address causes a page fault:

  • which is a hardware interrupt
  • we may kill the process; ‘segmentation fault’
  • the OS may handle the page fault and do something interesting:
    • lazily allocate physical memory
    • allocate and lazily load files into memory
    • provide virtual memory via page swapping
    • provide memory-mapped files; create the illusion that files on the disk are resident in the address space
    • emulate the access
Link to original

The OS may choose to reserve some virtual address space without allocating physical memory, we then can run a process as if there is memory there. Accessing this memory causes a page fault.

‘abort’ page fault handler

Here is a simple ‘abort’ page fault handler on x86:

static void handle_page_fault(const IRQ *irq, void *priv) {
	// Retrieve fault address from control reigster
	uint64_t fault_address;
	asm volatile("mov %%cr2, %0" : "=r" (fault_address));
	
	if (current_thread == NULL) {
		// If there is no current thread, then this fault happened early.
		// We must abort.
		log("PAGE FAULT @ vaddr = %p", fault_address);
	}
	
	// If there is a current thread, we abort it.
	log("PAGE FAULT @ vaddr = %p, rip = %p, proc = %s",
		fault_address, current_thread->rip, current_thread->owner().name);
	
	current_thread->owner().terminate(-1);
}

Lazy allocation

Lazy allocation

Lazy allocation is where we allocate, grow, and copy memory as it is needed.

  • stack: allocate / grow as used
  • heap: allocate / grow as used
  • program segments: copy as needed from disk
Link to original

This is quite similar to how it actually works however heap mapping doesn’t work like this, instead it is made and grown explicitly by system calls and not grown lazily by page fault.

Lazy zeroing

We can create the illusion of a zero-initialised region by mapping the same ‘read-only zero frame’ to all virtual addresses.

On first write, the fault handler maps a fresh frame. This means that:

  • allocation appears fast
  • we can make spare use of memory zeroes fit in memory and zeroes are readable at all times

Handling page faults with lazy zeroing

Let’s see what happens when we execute:

mov $0x00120348,(%rax)   ; %rax points to zero-initialised memory area

This triggers a page fault, after which the OS:

  1. Considers the faulting address and the address space (process).
  2. Determines how the area was allocated: ‘copy on write’
  3. Allocated a fresh frame
  4. Mapped it at the faulting page (replacing previous mapping of shared zero frame)
  5. Copied the shared contents (zeroes) into the new frame
  6. Resumed the process at the faulting instruction which now succeeds

Here’s how it could be implemented in code:

static void handle_page_fault(const IRQ *irq, void *priv) {
	// Retrieve fault address from control reigster
	uint64_t fault_address;
	asm volatile("mov %%cr2, %0" : "=r" (fault_address));
	
	if (current_thread == NULL) {
		// If there is no current thread, then this fault happened early.
		// We must abort.
		log("PAGE FAULT @ vaddr = %p", fault_address);
	}
	
	// If there is a current thread, we abort it.
	log("PAGE FAULT @ vaddr = %p, rip = %p, proc = %s",
		fault_address, current_thread->rip, current_thread->owner().name);
	
	// Is there a non-zero cookie in the PTE?
	VMA & vma = current_thread->owner().vma();
	uint32_t cookie;
	bool success = vma.get_pte_cookie(fault_address, cookie);
	if (success && cookie) {
		// perform lazy zeroing
		return;
	}
	
	current_thread->owner().terminate(-1);
}

Copy on Write

Copy on Write

Lazy zeroing is actually a special case of lazy copying known as Copy on Write. Any shared data, not just zeros, can be shared by ‘many-to-one’ read-only mappings, then at the time of write (if it ever happens), we copy said data.

Link to original

Often this is used for text segments in executable files, programs rarely write to their own text so all processes that are instances of the same program can share it, hence saving memory.