I hope you've started. :) Any questions?
The idea behind paging is simple: we define a constant called the "page size", and we partition our address space (both virtual and physical) into equal-sized pages, where each page holds an amount of data equal to the page size. So if my page size is 1K, every page holds 1K of data.
We create pages starting from address 0, up to the end of the address space. Page 0 always starts at address 0. Suppose my page size is 1K. Where does page 0 end? Where does page 1 start? Where does page N start and end? What page number does address 100 lie on? What about address 5000? Address X?
Pages are nice because it's really easy to determine which page an address lies on. Why are page sizes usually a power of 2?
Abstractly, page tables just map virtual page numbers to physical page numbers. The different types of page tables may seem intimidating, but remember that they're still just translating virtual page numbers to physical page numbers. The additional complexity comes from trying to make the process more efficient.
Let's take a look at some of the optimizations, starting with the basics. Since a page table just maps from virtual page numbers (VPNs) to physical page numbers (PPNs), the most straightforward page table looks like this:
| VPN | PPN |
|---|---|
| 0 | 1 |
| 1 | 3 |
| 2 | 4 |
| 3 | 7 |
| ... | ... |
The first idea is that we don't actually need to store the virtual page numbers in the table. Instead, we can use an array, and use the virtual page numbers as indices into the array. So we could represent the page table as the following:
int pageTable[] = {1, 3, 4, 7, ...};
This new definition of pageTable provides exactly the same information as the earlier definition: pageTable[0] == 7, pageTable[1] == 3 and pageTable[2] == 4, and so on. So we can use this pageTable to translate VPNs to PPNs, just like before, but now we're not explicitly storing the VPNs. As an added bonus, we can very quickly determine the PPN for any VPN. Given a VPN, we can get its PPN in constant time - we don't have to do a linear scan through the page table.
Unfortunately, our page tables are still too big. How big are they? If we have a 32-bit address space, and our pages are 4K, we need 2^32 / 4K mappings, which is 1M mappings. Each mapping in our page table takes 4 bytes (sizeof(int)), so we need 4M for each page table.
The solution is to add another level of indirection (this is a very common trick in computer science). Instead of having one big table that maps from VPN to PPN, we will have two levels of tables. The first level table will point us to a second level table, and the second level table will tell us the PPN.
For a two-level page table, we split the VPN into two pieces: a primary VPN and a secondary VPN. We feed the primary VPN into the first level table, which points us to a second level table, then we feed the secondary VPN into the second level table, which gets us a PPN.
Note that the second level tables work a lot like our simple page tables (input: secondary VPN, output: PPN), but the first level page table is different (input: primary VPN, output: pointer to second level table)
So what does a two-level page table look like? Suppose I have a 1-bit secondary VPN:
int pageTable[][2] = {{1, 3}, {4, 7}, ...};
Things aren't as simple as before, but all the data is still here, and I can still do all the translations I could do before.
For example, suppose I want to look up VPN 2. I split the VPN into a primary VPN and secondary VPN - since my secondary VPN is 1-bit, the secondary VPN is just the low bit of the VPN (which is 0), and the primary VPN is all the remaining bits of the VPN (which is 1). I feed the primary VPN into the first level table, and the secondary VPN into the second level table: pageTable[1][0] and the page table translates VPN 2 to PPN 4 as desired (pageTable[1][0] == 4)
How do I look up VPN 1 in this two level page table? And how are two-level page tables more efficient than single-level page tables? :)
The primary job of page tables is to convert VPNs into PPNs, but page tables also maintain several extra bits about each page, such as:
Why do we bother tracking all these extra bits? When is each one useful?
Because the page table maintains these extra bits, most page tables do not actually map directly from VPNs to PPNs. Instead, they map from VPNs to "page table entries" (PTEs), where a PTE contains the physical page number and lots of extra bits. So a PTE looks something like this:
struct pte {
int ppn;
bool valid;
bool dirty;
bool reference;
}
The corresponding structure in Nachos is TranslationEntry (see machine/translate.h).
With PTEs, we can still do translations just like before (since each PTE contains a physical page number), but now we have extra bits too.
With demand paging, we don't have to have all of a program's pages in memory at the same time. This is good because the amount of code and data a program uses at any time is typically a small fraction of the total code and data available.
Demand paging depends on the valid bit in PTEs. The valid bit indicates whether the virtual page has a corresponding physical page. If a program generates a virtual address that maps to an invalid PTE, a page fault is triggered, and we do the following:
Page faults can occur in any segment (code, data, stack, etc). This means that the page requested by the user program could be in a number of different places. Where might we read the requested page from?