The other function of virtual memory is translation. In early computers, all pointers referred to locations in physical memory. When you issued an instruction to load the value from address 512 into register 1, it would load the value from the specified address.
On a single-user, single-tasking system, this isn’t a huge problem. The user loads a program, and it takes control over the entire system. The program knows that it can use any memory between address 0 and the top of memory. A bit later, simple operating systems like CP/M or DOS would reserve some memory for themselves, but the running program still had a large contiguous region to use.
When a computer is running two programs at once, it’s somewhat more complicated. First, both programs need to determine which bit of memory they’re allowed to use. The simplest way of doing this is simply to allocate each program half of the memory, or some fixed-size slab. This is what DOS did with .COM files. The program then just needs to add a constant to each address to get the physical address.
When you start running a lot of programs, this process gets more difficult. Finding a region of memory the correct size is much more tricky after a few dozen programs have run. One solution would be for the operating system to shuffle all of the running programs down the address space periodically, leaving a big gap at the top for new programs. This approach is fine if programs never need to grow, but can be problematic if they do.
Of course, there’s no reason why a program needs to be stored in a contiguous region of memory. When a C program calls malloc(), it doesn’t care if the returned pointer is 20MB away from any other memory it owns. But if the program requests a contiguous buffer (for example, for an array or a large structure) and no free block of memory is that big, you have a problem. If every other 4KB of your memory is used and your program requests 4.1KB for a buffer, the operating system won’t be able to satisfy the request, despite the fact that only half of your memory is in use.
The solution is to give each process a virtual address space. Every process believes that it has a contiguous region of memory starting at address 0. In the situation just described, the operating system could take two of the 4KB regions that are unused, and map them into the process’s address space as if they were next to each other. The MMU would translate addresses in the buffer to physical addresses that were noncontiguous, but the program would be unaware of this translation.
The final aspect of translation is swapping. If your program wants to use more memory than you have, it’s possible to copy some of the contents of memory out to disk and mark that memory region as not present. When a program attempts to access this memory, the operating system will be informed via an interrupt and can load it back from disk before allowing the running program to proceed.