Project #3: Virtual Memory Management and File-System

Due June 4, 2003 (That is for you to preview. We make make minor changes by May 15.)

Contents for Part I (Virtual Memory)

Overview
Helpful Information
What to Submit
Additional Information

Overview

For this part of the assignment, you will be working in the vm directory of NACHOS. Your goal is to implement demand-paged virtual memory. A big difference between this assignment and the previous assignments is that you are given absolutely no code to start with, so you have to design and implement it yourself. The main things that you have to do are summarized as follows.

For this assignment, you will need to read and write data to the backing store, which should be a file called SWAP. The swap file should be accessed using the NACHOS OpenFile class. You can use the filesystem stub routines (the default), so that the SWAP file is created as a Unix file.

Implement a page fault handler and a page replacement policy of your choice. You should test your code under various conditions of system load, including one process with an address space larger than physical memory, and several concurrently running processes with combined address spaces larger than physical memory. The sort program in the test directory is an example of a program designed to stress the virtual memory system.

Helpful Information

Demand-paged virtual memory makes it possible to run a process with only a fraction of its address space resident in physical memory. This allows the address space of a user process to be potentially much larger than physical memory, and it also affords a greater opportunity for concurrent execution by allowing more processes to be running at a time. Demand-paged virtual memory is supported by address translation hardware that checks each access to main memory to see whether the referenced page is currently resident in physical main memory. If the page is resident, the access proceeds in the normal way. If the page is not resident, then a page fault is generated, and the CPU leaves user mode and begins executing a kernel routine whose purpose is to handle such exceptions. This routine, called the page fault handler, must perform the following tasks:
  1. A page of physical memory must be allocated to hold the referenced data, which must be read in from backing store (the disk).
  2. If a page of physical memory is not available in step (1), then page replacement must be performed. This consists of (a) selecting a victim page to be written to backing store, (b) allocating space on the backing store to receive the contents of this page, (c) initiating I/O to write the contents of the page to the backing store, and (d) waiting for this I/O to complete before proceeding. Page tables of any processes that had the victim page mapped into their address space must be adjusted to reflect the fact that the page is no longer resident. In particular, the ``valid'' or "residence" bit in the corresponding page table entries must be set to ``false''.
  3. The desired data must be located on the backing store, I/O must be initiated to bring the data into physical memory, and the completion of this I/O awaited before proceeding.
  4. The page tables of the faulting process must be adjusted to reflect the fact that the desired data is now resident in main memory. In particular, the ``valid'' bit in the corresponding page table entries must be set to ``true.''
  5. The page fault handler returns to user mode and restarts the instruction that caused the fault.

The page fault handler requires some auxiliary data structures to accomplish its task:

  1. A swap map is needed to keep track of the allocation of space on the backing store. This can be a bitmap with one bit for each sector of backing store.
  2. A system-wide table that has one entry for each page of virtual memory in the system, giving information about that page, such as a list of threads that have that page in their address space, a flag telling whether or not the page is resident, the location of the page on the backing store (if non-resident), the physical page that currently contains the data (if resident), whether or not I/O is currently being performed on it, etc.
  3. A table that has one entry for each page of physical memory, giving information about that page, such as the number of processes using the page, and a pointer to the entry in the table described in (2) above, telling which page of virtual memory (if any) is currently resident in that physical page.
  4. A table in the address space of each process that, for each page of virtual address space of that process, points to the appropriate entry in the table described in (2) above.
Other designs are possible; the above data structures are just to give you an idea of how it can be done.

You will also need to consider synchronization issues. You may need a lock (or at least a ``busy'' flag) for each physical memory page, and possibly also for each virtual memory page. These would be to ensure, for example, that two processes don't try to initiate I/O on the same page at the same time. Keep in mind that any time a process sleeps in the kernel, another process can run, so that when the sleeping process wakes up again things might look very different then when it went to sleep. The bottom line is that two system calls may be processed concurrently and the synchronization is needed.

What to Submit

The sample test programs for this project are under ~cs170/Spr03-Section1.test/proj3_p1. You can provide your own test programs.

As usual, commit all relevant files via the turnin procedure. You should also include a file HW3_WRITEUP in the "vm" subdirectory that explains how your virtual memory code works, and how to run tests. You should also indicate what does not work and explain your efforts in order to get partial credits.

Additional Information

Page tables were used in assignment 2 to simplify memory allocation and to isolate failures from one address space from affecting other programs. In this assignment we will use page tables to tell the hardware which pages are resident in physical memory and which are only resident on the disk. If the valid bit in a particular page table entry is set, the hardware assumes that the corresponding virtual page is loaded into physical memory in the physical page frame specified by the physicalPage field. Whenever the program generates an access to that virtual page, the hardware accesses the physical page. If the valid bit is not set, the hardware generates a page fault exception whenever the program accesses that virtual page. Your exception handler will then find a free physical page frame, read the page in from the backing store (typically a paging file) to that physical page frame, update the page table to reflect the new virtual to physical mapping, and restart the program. The hardware will then resume the execution of the program at the instruction that generated the fault. This time the access should go through, since the page has been loaded into physical memory.

To find a free frame, your exception fault handler may need to eject a cached page from its physical page frame. If the ejected page has been modified in the physical memory, the page fault handling code must write the page out to the backing store before reading the accessed page into its physical page frame. The hardware maintains some information that will help the page fault handler determine which steps need to be taken on a page fault. In addition to the valid bit, every page table entry contains a use and a dirty bit. The hardware sets the use bit every time it accesses the corresponding page; if the access is a write the hardware also sets the dirty bit. Your code may use these bits in its page fault handling code. For example, your code can use the dirty bit to determine if it needs to write an ejected page back to the backing store. When a page is read in from disk, the page fault handler should clear the dirty and use bits in its page table entry. If the page is ever ejected, the page fault handler checks its dirty bit. If the dirty bit is still clear, the copy of the page on disk is identical to the copy in physical memory and there is no need to write the page back to disk before ejecting it.

As with any caching system, performance depends on the policy used to decide which things are kept in memory and which are only stored on disk. On a page fault, the kernel must decide which page to replace; ideally, it will throw out a page that will not be referenced for a long time, keeping pages in memory those that are soon to be referenced. Another consideration is that the operating system may be able to avoid the overhead of writing modified pages to disk inside the page fault handler by writing modified pages to disk in advance. The page fault handler can take advantage of the clean physical page frame to complete subsequent page faults more quickly. FIFO replacement policy is the easiest one to implement, so you may want to start from there to test your page swapping mechanism. But in reality, FIFO replacement policy is rarely used due to its bad performance. You will have to implement some other more efficient replacement policy in your final version, such as FIFO with second chance or LRU.

Part II (Building a File System)

The multiprogramming and virtual memory assignments made use of the Nachos file system with the stub version. The last phase of your project is to use the Nachos's own file system and enhance its functionality. Your implementation should be under subdirectory "filesys".

The first step is to read and understand the partial file system we have written for you under filesys subdirectory. Run the program `nachos -f -cp test/small small' for a simple test case of our code - `-f' formats the emulated physical disk, and `-cp' copies the UNIX file `test/small' onto that disk.

The files to focus on are:

Our file system has a UNIX-like interface, so you may also wish to read the UNIX man pages for creat, open, close, read, write, lseek, and unlink (e.g., type "man creat"). Our file system has calls that are similar (but not identical) to these; the file system translates these calls into physical disk operations. One major difference is that our file system is implemented in C++. Create (like UNIX creat), Open (open), and Remove (unlink) are defined on the FileSystem object, since they involve manipulating file names and directories. FileSystem::Open returns a pointer to an OpenFile object, which is used for direct file operations such as Seek (lseek), Read (read), Write (write). An open file is "closed" by deleting the OpenFile object.

Many of the data structures in our file system are stored both in memory and on disk. To provide some uniformity, all these data structures have a "FetchFrom" procedure that reads the data off disk and into memory, and a "WriteBack" procedure that stores the data back to disk. Note that the in memory and on disk representations do not have to be identical.

What to Extend

Your job is to modify the file system to allow the maximum size of a file to be as large as the disk (128Kbytes). In the basic file system, each file is limited to a file size of just under 4Kbytes. Each file has a header (class FileHeader) that is a table of direct pointers to the disk blocks for that file. Since the header is stored in one disk sector with 128 bytes, the maximum size of a file is limited by the number of pointers that will fit in one disk sector. Increasing the limit to 128KBytes will probably but not necessarily require you to implement doubly indirect blocks.

The second job is to implement extensible files. In the basic file system, the file size is specified when the file is created. One advantage of this is that the FileHeader data structure, once created, never changes. In UNIX and most other file systems, a file is initially created with size 0 and is then expanded every time a write is made off the end of the file. Modify the file system to allow this; as one test case, allow the directory file to expand beyond its current limit of ten files. In doing this part, be careful that concurrent accesses to the file header remain properly synchronized.

Issues to Consider

Here are some things you should be sure to get right:

Notice that for the input program file, you can still use the regular Unix file system to read its content.

Currently, the basic file system code assumes it is accessed by a single thread at a time. You can still keep this assumption for this part of the assignment while in a real system, synchronization is needed to allow multiple threads to use file system concurrently.

What to Submit

The sample test programs for this project are under ~cs170/Spr03-Section1.test/proj3_p2. You can provide your own test programs.

As usual, commit all relevant files via the turnin procedure together when submitting Part I of your code. You should also include a file HW3_P2_WRITEUP in the "filesys" subdirectory that explains how you make this extension and how to run tests. You should also indicate what does not work and explain your efforts in order to get partial credits.

For part of the Nachos program you have modified, you should insert the following lines in your code so TAs can read:

//HW3 Begin of Modification

//HW3 End of Modification