CMSC 412 Project #4

Virtual Memory and Paging
Due Sunday, April 11, at 6:00pm

 

Introduction

The purpose of this project is to add paging and virtual memory to your GeekOS kernel. This will require many small, but difficult changes. More than any previous project, it will be important to implement one thing, test it, and then move to the next one. A successful implementation of earlier projects is not required for this project. You can implement Project 4 on top of any earlier version of the kernel (or directly on the provided distribution).

Background

In this project you will add paging to the GeekOS kernel, combined with the segmentation that is already part of the standard GeekOS kernel. For detailed information on paging, segmentation, and how they are combined on the Intel architecture, please refer to sections 8.4-8.7 in the text (pps. 288-309), and the Intel documentation specified above.

In a system that combines paging and segmentation, there are three kinds of addresses: logical addresses, linear addresses, and physical addresses. Logical addresses are those issued by a process. Logical addresses are mapped, via segmentation, to linear addresses by merely adding the base address of the relevant process segment to the logical address. A linear address is mapped to a physical address, which will be used by the processor to actually read from main memory, by using page tables set up and maintained by the operating system.  Paging is similar to segmentation because it allows each process to run in its own memory space. However, paging allows a much finer granularity of control by specifiying per-page mappings rather than a constant value offset.

In GeekOS, your paging system will use a page directory and page tables which form what as known as a two-level page table scheme (see Section 8.5.1, and notably Fig. 8.14, in the text).  Each memory address in the x86 system is 32 bits long.  To translate a linear address to a physical one, the processor will refer to the current page directory (this is analagous to finding an LDT for a process). It will then use the most significant 10 bits to index into the page directory (10 bits allows you to write numbers between 0 and 1023 thus the page directory has 1024 entries). The page directory entry is used to find the appropriate page table. The next 10 bits are used to index the page table (again 10 bits allows you to write numbers between 0 and 1023 thus the page table has 1024 entries). Finally, the page table entry is used to find the base of the physical memory page. The last 12 bits of the linear memory address is used to index into the physical page (12 bits allows you to write numbers between 0 and 4095 thus a page is 4096 bytes in size).

Each page directory and page table are themselves pages in the system. Each page directory/page table entry is 4 bytes and each table contains 1024 entries (4096 bytes / 4 bytes). Each page directory contains a pointer to a page table which in turn contains pointers to the physical memory.

Part I

The first step is to modify your project to use page tables and segmentation rather than just segments to provide memory protection.

Kernel Memory Mapping

The kernel starts running without paging enabled, and the segmentation hardware maps logical addresses directly to physical addresses.  Adding paging introduces a level in between: a logical address is mapped by segmentation to a linear address, which paging then maps to a physical address.  As a first step, we will introduce a page table that maps all linear addresses to themselves; that is each linear address X is mapped to physical address X.

To set up page tables, you will need to allocate a page directory (via Alloc_Page) and then allocate page tables for the entire region that will be mapped into this memory context. You will need to fill out the appropriate fields in the page tables and page directories. The definition of paging tables and directories are to be found in paging.h (structs pte_t and pde_t). Finally, to enable paging for the first time, you will need to call the routine Enable_Paging(pdbr) which is already defined for you in lowlevel.asm. It takes the base address of your page directory as a parameter.

The final step of this function is to add a handler for page faults. A default one named Page_Fault_Handler in paging.c has been provided for you. You should install it by calling Install_Interrupt_Handler. You need to register this as a handler for interrupt 14. You should then add a call the Init_VM function from your main.c (after Init_Interrupts).

You should be able to do this step and test it by itself by temporarily giving user mode access to these pages - set the flags fields in the page table entries to include VM_USER.  Refer to item 1 in the grading criteria to understand what you should have running at this point.  Once you have this running, you can submit it as your intermediate submission.

Part II

The next step is to make user processes have their own linear address space.

User Memory Mapping

We will define a single page directory for all kernel-only threads, and a page directory for each user process.  The page directory and page tables you set up for part I will be used by the kernel.  As such, you should change the flags fields to not include VM_USER.

The page directory for user processes will contain entries mapping user logical memory to linear memory, and will also contain entries to address the kernel memory.  This is not so user processes can access kernel memory directly (we will set the access flags of the memory to prevent them from doing so), but rather so that when an interrupt occurs the page table does not need to be changed for the kernel to access its own memory---it will simply use the page table of the user process that was running when the interrupt occurred.

The memory layout for a user process is shown below, where the addresses on the left are linear addresses (which are mapped to physical addresses by the paging system).  User processes still refer to logical addresses, which are mapped by the segmentation system to the linear addresses shown here.  In particular, a user process will still have the same addresses just as it has in prior projects.

VA 0x0000 0000

Kernel Memory

Start of kernel memory

(map all physical memory here)

VA 0x8000 0000

User Memory

Data/Text start here

 


<gap>

 

VA 0xFFFF E000

User Memory

Initial stack at top of this page

VA 0xFFFF F000

User Memory

Args in this page

VA 0xFFFF FFFF

 

Memory space ends here

 

The next step is to modify your user processes to use pages in the user range of memory. To help you do this, there is a new file called uservm.c that will replace your userseg.c from previous projects. You should start by taking the implementations from userseg.c and copying them to uservm.c and then modifying them for paging. There is a line in the Makefile in the build directory which specifies whether it should use userseg.c or uservm.c. You can switch between them by modifying USER_IMP_C := uservm.c to be USER_IMP_C := userseg.c and vice versa.

Setting up paging for user processes occurs in Load_User_Program, and takes two steps.  First, you need to allocate a page directory for the process. You should copy all of the entries from the kernel page directory for the bottom 2 GB of linear memory.

Next you need to allocate page table entries for the user process' text, data, and stack regions.  Each of these regions will consist of some number of pages allocated by the routine Alloc_Pageable_Page.  This routine differs from Alloc_Page in that the allocated page it returns will have a special flag PAGE_PAGEABLE set in the flags field of its entry in the struct Page data structure (see mem.h).  All pages (but not page directories and page tables) for a user space process should be allocated using this routine.

Contrast this with current implementation: for segmentation, one big chunk of memory was allocated for the entire user process. Paging allows per-page mappings for user memory so that each page of the user process is now allocated and mapped individually, and so it need not be contiguous.

As a suggestion, do this as follows: calculate the size of the text and data segment of the process as is done now, without including the size of the stack (but do round to PAGE_SIZE, though), and Malloc it.  Load the program into this memory, as now.  Then copy the image, page by page into the newly allocated pages.  Then allocate two pages of memory at the end of the virtual address range (i.e. the last two entries in the last page table). One is for the arguments, the other one is for stack.  Make sure the flags bits in both the page directory and page table entries allow user mode access (contain VM_USER flag).

Finally, you will need to tweak some aspects of the current segmentation implementation so that it works with paging. The base linear address for the user mode process (that is, the base address for the code and data segments set in Create_User_Context) should be 0x80000000 (USER_VM_START in paging.h), and the limit should be (0xFFFF FFFF (USER_VM_END in paging.h) - USER_VM_START) / PAGE_SIZE . This will allow the user space process to think that its virtual location 0 is the 2GB point in the page layout and will greatly simplify your kernel compared to traditional paged systems. You will also need to add code to switch the PDBR (cr3) register as part of a context switch. For this, in Switch_To_Address_Space you should add a call to Set_PDBR (provided for you in lowlevel.asm), after you load the LDT. You will use the pageDir field in the userContext structure that will store the address of the process's page directory.

At this point, you should test your memory mapping by running qemu. If you are able to load and run shell, you have completed this correctly.

Demand Paging

A nice benefit of paging is that it is straightforward to add to the memory allocation of a process by simply allocating a new (physical) page and mapping it into a free region in the process's address space.  For example, this can be done to allow the process's stack to grow beyond its initial allocation.  To implement stack growth, you must modify the default page fault handler.  This handler is called whenever the process tries to access an illegal address, and currently it just terminates the process in this case, printing out the address that caused the fault.  The fault handler reads register cr2 to determine the faulting address. It also prints the errorCode defined in InterruptState and the fault defined in the struct faultcode_t in paging.h.

You will modify this page fault handler to determine an appropriate action to take based on the address.  In the case that the address is within one page of the current stack limit, you should allocate a new page, map it into the appropriate address, and then return normally from the handler.  The program will now be able to use the memory that you just allocated for it.  In order to test your new page fault handler, run the provided program rec.c.

Virtual Memory

While paging is useful for efficiently managing memory, the processes that can be active are still limited by the amount of physical memory.  To remedy this, you will implement virtual memory as part of GeekOS, in which memory pages can be temporarily stored on disk so that the freed physical memory can be used by another process.  To do this, the OS will need to pick a page to evict from memory and write it to the paging file, stored on disk. You should implement a version of pseudo-LRU (see section 9.4.5 in text, pp. 336). Use the accessed bit in the page tables to keep track of how frequently pages are accessed. To do this, use the clock field in the struct Page in mem.h. You should update the clock on every page fault for all pages that were accessed since the last fault. Alternatively, you can update the clocks in the body of the Idle thread, i.e. walk thru the allocated pages and for ones that were accessed, update their clocks. This will avoid a heavy-weight page fault handler. Note: the previous section described a moronic version of a clock algorithm that the struct Page "clock" field supports. Instead, maintain a global (static) clock value that points to a page in memory (a struct Page in the array of pages), increment that clock, and clear the accessed bit or claim the page if the accessed bit was already clear. Easy. (Fall 2009.)

The paging file consists of a group of consecutive 512 disk blocks of size SECTOR_SIZE bytes.  Calling the routine Get_Paging_Device in vfs.h will return a Paging_Device structure containing the first disk block number of the paging file and the number of disk blocks in the paging file.  Each page will consume 8 consecutive disk blocks (PAGE_SIZE/SECTOR_SIZE). To read/write the paging file, use the functions Block_Read and Block_Write provided in blockdev.h. These functions write SECTOR_SIZE bytes at a time. How you manage your paging file is completely up to you. A good idea would be to write a Init_Pagefile function in paging.c and call it from main.c

The code to page out a page is implemented for you in Alloc_Pageable_Page in mem.c, and works as follows:

Eventually, the page that was put on disk will be needed by some process again. At this point you will have to read it back off disk into memory (possibly while paging out another page to fit it into memory). Since the page that is paged out has its present bit set to 0 in the page table, an access to it will cause a page fault. Your page fault handler should then realize that this page is actually stored on disk and bring it back from disk (the kernelInfo field in the page table entry). When you bring a page in off disk, you may free the disk space used by the page. This will simplify your paging system, but will require that when a page is removed from memory it must always be written to the backing store (since even pages that haven't been written since they were last brought into memory from disk are not already on disk). You will rely on the information stored when a page is paged out to find it on disk and page it back in.

The following table summarizes the actions of your page fault handler.

Cause

Indication

Action

Stack growing to new page

Fault is within one page of the current stack limit

Allocate a new page and continue.

Fault for paged out page

Bits in page table indicate page is on disk

Read page from paging device (sector indicated in PTE) and continue.

Fault for invalid address

None of the other conditions apply

Terminate user process

Process Termination

As part of process termination, you will need to free the memory associated with a process. This includes freeing the pages used by the process, freeing the page tables and page directories. In addition, you will need to release the backing store space used by any pages of the terminating process. You should modify your Destroy_User_Context function in uservm.c to do this.