How Does the MMU Translate Virtual to Physical Memory? A Deep Dive
This article explains the role of the Memory Management Unit (MMU) and paging in modern operating systems, covering hardware structure, address translation, permission checks, page tables, TLB behavior, virtual memory mechanisms, and practical Linux kernel code examples for memory protection, sharing, and performance optimization.
Memory Management Unit (MMU) Overview
The MMU is a hardware block, typically integrated in the CPU, that translates virtual addresses generated by software into physical addresses used by memory chips and enforces per‑page access permissions (read, write, execute). It enables each process to see a contiguous address space starting at 0 while the underlying physical memory may be fragmented and shared.
Core Functions
Address translation : Uses page tables to map virtual pages to physical page frames.
Permission checking : Each page table entry contains bits that control read/write/execute access, preventing illegal accesses such as writing to kernel code.
Address Translation Process
When the CPU issues a virtual address, the MMU first looks in the Translation Lookaside Buffer (TLB). A TLB hit yields the physical address instantly. On a miss, the MMU walks the page‑table hierarchy (typically four levels on x86‑64: PGD → PUD → PMD → PTE) to locate the mapping. If the required entry is absent, a page‑fault is raised.
TLB Role
The TLB is a small, fully‑ or set‑associative cache that stores recent virtual‑to‑physical translations. It exploits temporal and spatial locality to reduce the latency of address translation. Techniques such as ASID/PCID allow multiple processes to share the TLB without full flushes on context switches.
Paging Mechanism
Virtual and physical memory are divided into fixed‑size pages (commonly 4 KiB). The page table records the mapping and permission bits for each page. The translation steps are:
Split the virtual address into a page number and offset.
Lookup the page number in the TLB or page table.
Combine the physical page frame number with the offset to obtain the final physical address.
If the page is not resident, the operating system loads it from swap or aborts the access, handling the page‑fault by allocating a frame, updating the page table and TLB, and resuming the process.
Multi‑Level Page Tables
Four‑level page tables dramatically reduce memory consumption because only the portions actually used are allocated. For a process that accesses 1 GiB of memory, the total page‑table memory is roughly 2 MiB instead of the terabytes required by a flat table.
MMU‑Based Memory Protection
Each page’s permission bits are checked on every access. A kernel‑only page is marked read‑only/executable for kernel mode; any user‑mode write triggers a fault. The following kernel‑mode example demonstrates a simple permission check:
typedef struct {
unsigned int phys_frame; // physical page‑frame number
unsigned char read : 1; // read permission
unsigned char write : 1; // write permission
unsigned char exec : 1; // execute permission
} PageTableEntry;
int mmu_check_permission(PageTableEntry *pte, unsigned char access_type) {
// access_type: 0=read, 1=write, 2=exec
switch (access_type) {
case 0: return pte->read ? 0 : -1;
case 1: return pte->write ? 0 : -1;
case 2: return pte->exec ? 0 : -1;
default: return -2; // illegal type
}
}
int kernel_mmu_permission_test(void) {
PageTableEntry kernel_code_pte = {0x1000, 1, 0, 1}; // read‑only code page
int ret = mmu_check_permission(&kernel_code_pte, 1); // user tries to write
if (ret == -1) {
printk("MMU permission check failed: attempted write to read‑only kernel page!
");
return -1;
}
return 0;
}Shared Memory via the MMU
Processes can map the same physical page into different virtual addresses, enabling fast inter‑process communication. The kernel module below creates a System V shared memory segment, maps it into parent and child, and demonstrates read/write synchronization.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/shm.h>
#include <linux/sched.h>
#include <linux/pid.h>
#include <linux/wait.h>
int mmu_set_shared_page(int shmid, void **shared_addr) {
*shared_addr = shmat(shmid, NULL, 0);
if (*shared_addr == (void *)-1) {
printk(KERN_ERR "MMU shared page mapping failed
");
return -1;
}
printk(KERN_INFO "MMU shared page mapped at %p
", *shared_addr);
return 0;
}
static int __init mmu_shared_memory_init(void) {
int shmid = shmget(IPC_PRIVATE, sizeof(int), IPC_CREAT | 0666);
if (shmid == -1) return -1;
int *shared_data;
if (mmu_set_shared_page(shmid, (void **)&shared_data) == -1) {
shmctl(shmid, IPC_RMID, NULL);
return -1;
}
*shared_data = 100;
pid_t pid = fork();
if (pid == 0) { // child
int *child_shared;
if (mmu_set_shared_page(shmid, (void **)&child_shared) == -1) return -1;
printk(KERN_INFO "Child reads %d at %p
", *child_shared, child_shared);
*child_shared = 200;
shmdt(child_shared);
return 0;
} else if (pid > 0) {
wait(NULL);
printk(KERN_INFO "Parent reads %d
", *shared_data);
shmdt(shared_data);
shmctl(shmid, IPC_RMID, NULL);
}
return 0;
}
static void __exit mmu_shared_memory_exit(void) {
printk(KERN_INFO "MMU shared memory module unloaded
");
}
module_init(mmu_shared_memory_init);
module_exit(mmu_shared_memory_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("MMU Process Shared Memory Demo");Process Isolation Example (fork)
After fork(), the child receives its own page tables; code pages are shared read‑only, while stack and heap are private. The following program prints the virtual addresses of a variable in parent and child, showing they differ even though the variable name is identical.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
void child_process() {
int local_var = 100;
printf("Child PID %d: local_var virtual address = %p, value = %d
", getpid(), &local_var, local_var);
while (1) sleep(1);
}
int main() {
int local_var = 200;
printf("Parent PID %d: local_var virtual address = %p, value = %d
", getpid(), &local_var, local_var);
pid_t pid = fork();
if (pid == 0) child_process();
else if (pid > 0) wait(NULL);
return 0;
}Memory‑Mapped Files (mmap)
Mapping a file into a process’s virtual address space lets the program read or write the file via ordinary memory accesses, avoiding extra system calls. The example below opens example.txt, maps it read‑only, prints its size and contents, then unmaps the region.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
int main() {
const char *filename = "example.txt";
int fd = open(filename, O_RDONLY);
if (fd == -1) { perror("open"); return EXIT_FAILURE; }
struct stat sb;
if (fstat(fd, &sb) == -1) { perror("fstat"); close(fd); return EXIT_FAILURE; }
char *map = mmap(0, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
if (map == MAP_FAILED) { perror("mmap"); close(fd); return EXIT_FAILURE; }
close(fd);
printf("File size: %ld bytes
", sb.st_size);
printf("Content:
%s
", map);
if (munmap(map, sb.st_size) == -1) perror("munmap");
return EXIT_SUCCESS;
}Huge Pages (Large Pages)
Using 2 MiB huge pages reduces TLB pressure and improves bandwidth. The program creates a file in a hugetlbfs mount, truncates it to the huge‑page size, maps it with MAP_HUGETLB, writes a string, prints the address, then cleans up.
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#define HUGE_PAGE_SIZE (2 * 1024 * 1024)
#define HUGE_PAGE_MOUNT "/mnt/huge"
int main() {
static int page_num = 0;
char filename[128];
snprintf(filename, sizeof(filename), "%s/huge_page_%d", HUGE_PAGE_MOUNT, page_num++);
int fd = open(filename, O_CREAT | O_RDWR, 0666);
if (fd == -1) { perror("open huge page"); return -1; }
if (ftruncate(fd, HUGE_PAGE_SIZE) == -1) { perror("ftruncate"); close(fd); unlink(filename); return -1; }
void *ptr = mmap(0, HUGE_PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_HUGETLB, fd, 0);
if (ptr == MAP_FAILED) { perror("mmap huge page"); close(fd); unlink(filename); return -1; }
memset(ptr, 0x00, HUGE_PAGE_SIZE);
strncpy(ptr, "Linux huge page demo", HUGE_PAGE_SIZE - 1);
printf("Huge page mapped at %p, content: %s
", ptr, (char *)ptr);
if (munmap(ptr, HUGE_PAGE_SIZE) == -1) perror("munmap");
close(fd);
if (unlink(filename) == -1) perror("unlink");
printf("Huge page resources released
");
return 0;
}Performance Optimisation Strategies
TLB Optimisation
Increasing the TLB hit rate—by exploiting temporal and spatial locality, using large pages, and reducing address‑space switches—can cut memory‑access latency dramatically. Benchmarks show that raising the hit rate from 70 % to 90 % reduces query response time by ~35 % and boosts throughput by ~42 % in database workloads.
Page‑Table Optimisation
Multi‑level page tables shrink the memory needed for address translation. For a process using only 1 GiB, a four‑level hierarchy occupies roughly 2 MiB instead of the terabytes required by a flat table. Linux also caches recently used page‑table entries (pte_cache) to avoid repeated memory accesses.
Memory‑Allocation Strategies
Different allocators suit different workloads. First‑Fit offers speed for high‑throughput servers, while the Buddy System or huge pages reduce fragmentation for scientific and high‑frequency‑access applications.
Deepin Linux
Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
