** Paging ** Remember our goal: to virtualize memory. Segmentation (a generalization of dynamic relocation) helped us do this, but has some problems; in particular, managing free space becomes quite a pain as memory becomes fragmented. Thus, we'd like to find a different solution. Thus comes along the idea of *paging*. Instead of splitting up our address space into three logical segments (each of variable size), we will split up our address space into fixed-sized units we call a *page*. Here is an example of a tiny address space, 64 bytes total in size, with 16 byte pages (real address spaces are much bigger, of course, commonly 32 bits and thus 4-GB of address space): 0 |---------------------| | | (let's call this page 0 of this address space) | | 16 |---------------------| | | (page 1) | | 32 |---------------------| | | (page 2) | | 48 |---------------------| | | (page 3) | | 64 |---------------------| [FIGURE: SIMPLE 64-BYTE ADDRESS SPACE] Thus, we have an address space that is split into four pages (0 through 3). With paging, physical memory is also split into some number of pages as well. For example: 0 |---------------------| | | (page 0 of physical memory) | | 16 |---------------------| | | (page 1) | | 32 |---------------------| | [page 3 of AS] | (page 2) | | 48 |---------------------| | [page 0 of AS] | (page 3) | | 64 |---------------------| | | (page 4) | | 80 |---------------------| | [page 2 of AS] | (page 5) | | 96 |---------------------| | | (page 6) | | 112 |---------------------| | [page 1 of AS] | (page 7) | | 128 |---------------------| [FIGURE: 64-BYTE AS IN PHYSICAL MEMORY] The beauty of paging is the simplicity and ease the OS has when it is managing free physical memory. For example, when the OS wishes to place our tiny 64-byte address space from above into our 8-page physical memory, it simply finds four free pages; perhaps the OS keeps a *free list* of all free pages for this, and just grabs the first four free pages off of the list. In the example above, the OS has placed virtual page 0 of the address space (AS) in physical page 3, virtual page 1 of the AS on physical page 7, page 2 on page 5, and page 3 on page 2. To record where each virtual page of the operating system is placed in physical memory, the operating system keeps a *per-process* data structure known as a *page table*. The major role of the page table is to store *address translations* for each of the virtual pages of the address space, thus letting us know where in physical memory they live. For our simple example above, the page table would thus have the following entries: Virtual Page | Physical Page ------------------------------ 0 | 3 1 | 7 2 | 5 3 | 2 As we said before, it is important to remember that this is a *per-process* data structure. If another process were to run in our example above, the OS would have to manage a different page table for it, as its virtual pages obviously map to *different* physical pages (modulo any sharing going on). Now, we know enough to perform an address-translation example. Let's imagine the process with that tiny address space (64 bytes) is performing a memory access: load R1, Specifically, let's pay attention to the explicit load of the data at into the register R1 (and thus ignore the instruction fetch that must have happened prior). To *translate* this virtual address that the process generated, we have to first split it into two components: the *virtual page number*, and the *offset* within the page. For this example, because the virtual address space of the process is 64 bytes, we need *6 bits* total for our virtual address (2^6 = 64). Thus, our virtual address looks like this: [ Va5 Va4 Va3 Va2 Va1 Va0 ] where Va5 is the highest-order bit of the virtual address, and Va0 the lowest order bit. Because we know the *page size* (16 bytes), we can further divide the virtual address as follows: virtual | page | offset number | [ Va5 Va4 | Va3 Va2 Va1 Va0 ] The page size is 16 bytes in a 64-byte address space; thus we need to be able to select 4 pages, and the top 2 bits of the address do just that. Thus, we have a 2-bit *virtual page number* (VPN). The remaining bits tell us which byte of the page we are interested in, 4 bits in this case; we call this the *offset*. When a process generates a virtual address, the OS and hardware must combine to translate this virtual address into a meaningful physical address. For example, let us assume the load above was to virtual address 21: load R1, 20 Turning "20" into binary form, we get "010100", and thus we can examine this virtual address and see how it breaks down into a virtual page number (VPN) and offset: vpn | offset 01 | 0100 Thus, the virtual address "20" is on the 4th ("0100"th) byte of virtual page "01" (or 1). With our virtual page number, we can now index our page table and find which physical page that virtual page 1 resides within. In the page table above the physical page number (or PPN) is 5 (binary 101). Thus, we can translate this virtual address by simply replacing the VPN with the correct PPN and then finally issue the load to physical memory: vpn | offset 01 | 0100 becomes ppn | offset 101 | 0100 Note the offset stays the same (it is not translated), because the offset just tells us which byte *within* the page we want. Thus, our final physical address is 1010100, which is 84 in decimal, and is exactly where we want our load to fetch data from (see [FIGURE: 64-BYTE AS IN PHYSICAL MEMORY]). [WHERE ARE PAGE TABLES STORED?] Page tables can get awfully large, much bigger than the small segment table or base/bounds pair we have discussed previously. For example, imagine a typical 32-bit address space, with 4-KB pages. This virtual address splits into a 20-bit VPN and 12-bit offset (recall that 10 bits would be needed for a 1-KB page size, and just add two more to get to 4 KB). A 20-bit VPN implies that there are 2^20 translations that the OS would have to manage for each process (that's roughly a million); assuming we need 4 bytes per *page table entry* (PTE) to hold the physical translation plus any other useful stuff, we get an immense 4MB of memory needed for each page table! That is pretty big. Because they are so big, we don't keep any special on-chip hardware in the MMU to store the page table of the currently-running process; it is simply too big. Thus, we store the page table for each process in *memory* somewhere. Let's assume for now that the page tables live in physical memory that the OS manages. Here is a picture of what that might look like: 0 |---------------------| | page table: | (page 0: OS physical memory) | 3 7 5 2 | 16 |---------------------| | | (page 1) | | 32 |---------------------| | [page 3 of AS] | (page 2) | | 48 |---------------------| | [page 0 of AS] | (page 3) | | 64 |---------------------| | | (page 4) | | 80 |---------------------| | [page 2 of AS] | (page 5) | | 96 |---------------------| | | (page 6) | | 112 |---------------------| | [page 1 of AS] | (page 7) | | 128 |---------------------| [FIGURE: PHYSICAL MEMORY WITH PAGE TABLE IN KERNEL PHYSICAL MEMORY] [WHAT'S IN THE PAGE TABLE?] A small aside about page table organization. The page table is just a data structure that is used to map virtual addresses (or really, virtual page numbers) to physical addresses (physical page numbers). Thus, any data structure could work. The simplest form is called a *linear page table*. It is just an array. The OS indexes the array by the VPN, and thus looks up the *page-table entry* (PTE) at that index in order to find the desired PPN. For now, we will assume this linear page table structure; below we will make use of more advanced data structures to help solve some problems with paging. As for the contents of each PTE, we have a number of different bits in there worth understanding at some level. A *valid* bit is common to indicate whether the particular translation is valid; for example, when a program starts running, it will have code and heap at one end of its address space, and the stack at the other. All the unused space in-between will be marked *invalid*, and if the process tries to access such memory, it will generate a trap to the OS which will likely terminate the process. We also might have *protection* bits, indicating whether the page could be read from, written to, or executed from (e.g., a code page). Again, accessing a page in a way not matched allowed by these bits will generate a trap to the OS. There are a couple of other bits that are important but we won't talk about much for now. A *present* bit indicates whether this page is in physical memory or on disk (swapped out); we will understand this in more detail when we study how to move parts of the address space to disk and back in order to support address spaces that are larger than physical memory and allow for the pages of processes that aren't actively being run to be swapped out. A *dirty* bit is also common, indicating whether the page has been modified since it was brought into memory. We will come back to this bit as well. [SO AREN'T WE DONE?] With page tables in memory, we already know that they might be too big. Turns out they can slow things down too. For example, take our simple instruction: load R1, 20 Again, let's just examine the explicit reference to address 20 and not worry about the instruction fetch. To fetch the desired data, the system must first *translate* the virtual address (20) into the correct physical address (84). Thus, before issuing the load to address 84, the system must first fetch the proper page table entry from the process's page table, perform the translation, and then finally get the desired data from physical memory. Simply put, for every memory reference (whether an instruction fetch or an explicit load or store), paging now requires us to perform one *extra* memory reference in order to first fetch the translation from the page table. Wow, that is a lot of work(!), and will likely slow down the process by a factor of two or more. And now we can see what the real problems are that we must solve. [THE CRUX OF THE PROBLEM] Page tables, as we've describe them, have two big problems: they are *too big*, and they are *too slow*. The rest of this note introduces a number of techniques (both hardware and software) to make the amount of memory taken up by page tables smaller and the speed of address translation faster. We start with speed.