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.
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.Additional Information
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.
Recommended Implementation Stages
Adapted from Christopher Kruegel's Notes on cs170-2 Project 3.
For this project, you will primarily be working with files in
userprog
and machine
directories although new files should be
placed in teh vm
directory. We
suggest the following steps and urge you to keep an eye at all
times on the data structures you have chosen to store and access
information. The hope is that after each stage, your design shall
become more concrete and perhaps you shall have a better perspective
of what you need to do and how you would like to do it.
Stage 1:
Make sure that you understand the
TranslationEntry
class as given in
machine/translate.h
. Ask yourself why each component
of a page-table has been included. See the method
Machine::Translate
in machine/translate.cc
to
understand how these fields are used.
Stage 2:
Trace how the MIPS simulator (in
machine/mipssim.cc
) executes instructions. As a part
of executing an instruction, the machine accesses main memory for
loading the instruction itself (code segment) and possibly
for its operands (data/stack segment). Figure out how memory
access is implemented and where the virtual to physical translation is
performed. It is during the translation process that the machine can
determine if the virtual address it is trying to access belongs to a
page which does not reside in physical memory. Figure out how, when
and where the PageFaultException is thrown.
Stage 3:
In stage 2, you would have figured out how,
when and where the PageFaultException is raised. This Exception
results in control being passed back to the kernel and is supposed to
be handled in a manner similar to system calls. Currently, this
exception is not handled. Add code to exception.cc
to
call a stub routine when this Exception is raised. This will be your
page fault handler. As a part of raising the Exception, the processor
saves the faulting virtual address in a register that will be used by
the kernel to handle the Exception. Note that, at this point, this
mechanism is not exercised by program execution as processes are
loaded in their entirety and no page faults are generated.
Stage 4:
Figure out how to start a process with
none of its pages in memory. For this, you will need to change the
code that you wrote in Project 2 for process creation. You may also
need to modify the pagetable structure (in the
TranslationEntry
class) to keep track of pages that are
not in memory. Note that you are free to add fields to the this
structure (as long as you don't disturb the existing
fields). You will need to keep track of the location from which
disk-resident pages are to be loaded. Remember that, initially the
pages of a process are all in the executable file. Once a page has
been brought in to memory, any subsequent flush of this page to disk
(during page replacement) should be to backing store (this storage
will be created in Stage 6) and not to the executable file. You will
also need to allocate space in the backing store for the pages of this
process. You can choose to be conservative and allocate space for the
entire virtual address space of the process on the backing store at
creation time. You can be even more conservative and choose to copy
the entire executable file into the allocated space at startup. If you
did this, you would need to only concern yourself with moving pages
between backing store and the memory during page fault handling.
Stage 5
At this point, you are all set to receive a page
fault. We suggest that you make your dummy page fault handler (set up
in stage 3) simply print some debugging information and return. Using
this scheme, you should make sure that control actually flows to your
page fault handler during program execution. You don't service the
page fault at this stage. Therefore, you should run into an infinite
loop where the machine keeps raising page faults.
Stage 6
Now you are all set to implement a page
replacement algorithm.
Page Fault Handling
The page fault handler gets control as a result of the machine raising a page fault exception. The handling of this exception involves the following tasks.Use a single file called SWAP with 512 sectors to implement the backing store. The size of the swap sectors is the same as that of a physical page frame. You should use the stub implementation of the file system already provided with Nachos (look intoDemand-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: If necessary, allocate space on the backing store to receive the contents of the victim page (assuming that it is dirty and needs flushing). Initiate I/O to write the contents of the victim page to the backing store. Adjust the pagetable for the process to which the victim page belongs to reflect the fact that it is no longer resident in memory. Locate the page for which the fault was generated on the backing store; initiate I/O to load the page into the page frame selected in the previous steps. Adjust the pagetable for the faulting process to reflect the fact that the desired page is now resident in memory. Return to user mode and restart the instruction that caused the fault. Make sure to re-execute the instruction for which the page-fault was generated in the first place. filesys/filesys.h
andfilesys/openfile.h
). You will need a mechanism to keep track of the used and free sectors in the swap file (similar to the mechanism that keeps track of the allocation of the physical page frames in the previous assignment).Recommended Data Structures
The page fault handler requires some auxiliary data structures to accomplish its task. The following data structures may be useful.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.
- 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.
- A table that has one entry for each page of physical memory, giving information about that page, such as the last time it was referenced and and a pointer to the Thread or AddrSpace which has data resident in that page.
- Additional field in the TranslationEntry class that makes up an AddrSpace's page table to represent wether or not the page is on disk or in Memory.
Other designs are possible; the above data structures are just to give you an idea of how it can be done.
Once you get this working, you should be able to execute programs
normally. Launch multiple processes in Nachos simultaneously (using
exec, fork) and test your code under various conditions of
system load. Include a test case using one process with an address space larger than
physical memory and a test case using 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.
vm
directory Optional Stage 7
Bonus (10% of project grade) Awarded for Sucessfull Completion
Partial credit recieved if attempted
In this stage, you will extend your memory structures and page fault handler to support
copy-on-write. This stage will require extensive changes to your existing code, so it is recommended that
you completly finsh stages 1-6 before attempting the bonus stage.
In order to prevent a partialy working implementation from dis-abling your existing working implementation
achieved in steps 1-6, it is especially important that you wrap _all_ modified code in #define COPYONWRITE
statements
that will allow the grader to give you credit for stages 1-6 if your copy-on-write scheme is not finished or working by the time the
code is turned in.
A memory manager implementing copy-on-write does not use extra memory space by duplicating a page (as in a fork system call), instead, you:
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.
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.
What to Extend
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.
Here are some things you should be sure to get right:
Issues to Consider
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.
Adapted from Christopher Kruegel's Notes on cs170-2 Project 4.Recommended Implementation
Stages
For this project, you will primarily be working with files in the
machine
and the filesys
directories and using the built in -f
debug flag. We
suggest the following steps and urge you to keep an eye at all times
on the data structures you have chosen to store and access
information. The hope is that after each step, your design shall
become more concrete and perhaps you shall have a better idea of what
you need to do and how you would like to do it.
Step 1: Understand how the Nachos filesystem operates
Take a look at the Create and Open functions in filesys.cc
. You will later
modify your Create
and Open system calls to use these functions. See how OpenFile objects are created and
opened.
Step 2: Understand how Nachos implements files
Take a look at openfile.cc
. The OpenFile
class uses the
FileHeader
class. Examine the FileHeader
class and see why the file size is limited.
Look at the
implementation of FileHeader
and see what each part is doing.
Step 4: Modify your file system calls to use Nachos file system
In project 2, you wrote system calls for file system related operations
(Create
,
Open
, Read
,
Write
, and Close
). These calls invoked the file system stubs
located in filesys.h
that were #defined under FILES_STUB. The Nachos that is built under the
filesys/
directory
uses the calls defined in filesys.h under the #defines for FILESYS.
Make the necessary modifications to your system calls to allow them to use the
FILESYS
version
of these calls.
Step 5: Modify Nachos so that you can create larger (up to 128KB) files. This
is the size of the
virtual DISK (file) formatted and used in Nachos
This will require you to implement doubly indirect blocks.
The use of
indirect and doubly indirect blocks to keep track of sectors used for large files is
described in
class/section Make sure your
file header is not larger than one disk sector. It is not acceptable to allocate all the
sector pointers in
the FileHeader all at once. In other words, each header should only have the capcity to
handle the current
file size and will need to be allocate more single indirection blocks (and later, double
indirection blocks) when the file is extended
to a size that requires the exta header block pointers.
FileHeader
object to contain a sector number
that points to a block of pointers (single indirect).
The single indirect block can be implemented as a new class called
IndirectPointerBlock
which will
take up an entire sector (just like FileHeader), but contain nothing but pointers to
sectors. The
IndirectPointerBlock
should support the following methods:
WriteBack
in FileHeader
, this will write the
object (this *) to the sector of the disk passed
in as an argument.
FetchFrom
in FileHeader
, this method will
de-serialize the object that was written to the
sector
argument.
FileHeader
will perform all the allocations
and pass the sector numbers to the IndirectPointerBlock
via this
method. This will be easier than having each pointer
block manage sectors with a single free map.
Deallocate
in FileHeader
, this method will loop
through the sectors allocated and un-mark
them from the free map to signify thier deletion.
IndirectPointerBlock
. So if an
IndirectPointerBlock
covers blocks 60-90 and I pass in an offset of
SectorSize*10, I know that the sector number can be found
in the 10th entry.
FileHeader
's Deallocate
and Allocate
methods to create this single indirect
pointer block when necessary and deallocate it when it's no longer needed. You
will only deallocate once when the whole
file is destroyed, so it is OK to just call Deallocate
recursivly
Modify the FileHeader
's FetchFrom
and WriteBack
methods to recursivly put and get the
IndirectPointerBlock
. Modify the ByteToSector
method to
query the single indirect pointer block for
the location of a byte using the IndirectPointerBlock
's
ByteToSector
method.
FileHeader
object to contain a sector number
that points to a block of
IndirectPointerBlock
pointers. This will be a double indirect pointer
block. Each pointer to doubly indirected block
gives you 900 available blocks, so just one should do it. Use single indirection and
direct access to get the other 124 block capacity.
There should be a few modifications
necessary to make the IndirectPointerBlock
point to more
IndirectPointerBlock
rather than to sectors
of data. You can create a new class if you really want, but a couple of
()?:
(question colon) statements or if-then-elses
should be all that is necessary. Some of these changes will be:
FileHeader
's Deallocate
and
Allocate
methods to create this double indirect
pointer block when necessary and deallocate it when it's no longer needed.
Also, the IndirectPointerBlock
PutSector
method should allocate a new IndirectPointerBlock
(if the block is doubly indirected) when necessary.
IndirectPointerBlock
.
ByteToSector
method to query a single indirected block
regarding the location of a byte. It should look like
FileHeader
's ByteToSector
method when it is querying single
indirected blocks.
ByteToSector
in the FileHeader
class
to query the double indirect pointer block for the location of a byte if that byte is
determined to be within it's range.
IndirectPointerBlock
and 1 double
IndirectPointerBlock
how many sectors
can your file contain now? Good, that's enough to have a file as large as the whole
disk (1024) sectors. Plus the whole thing can be accessed from the file header.
Step 6: Allow the Write
system call to extend the size of a file.
Currently, if the user tries to write beyond the end of the file,Nachos returns an error.
Modify the current implementation so that a write beyond the end of the file extends the
size of the file. The exception is that if the file is created to be
a given size, your implementation should allocate as many sectors (and block pointers) as
required to hold that amount of data. For now this function can allocate all pages that
are needed in order to fill the gap between the previous end sector and the new end
sector. Gracefully handle the case of the disk being full - the user process should not
crash. Instead an error should be returned.
ReadAt()
method alone. If a user tries to read
beyond the end of a file they should get back nothing. If they start the read in a valid
position, but try to read too much, they should only get back the amount that existed
between the starting position and the end of the file. That's the way the function works
now and that's the way it should work.
WriteAt()
method in openfile.cc
to
check only for writes that go beyond the size of the disk (not the current size of the
file). Modify the reading of the first and last sector to handle the case when you are
writing beyond the current bounds of the file. I would switch() the cases that can occur
as such:
ByteToSector()
Make sure
you don't attempt to extend the file beyond 1024 bytes.
FileHeader
that extends the file by
N sectors. Name it something like: ExtendFile
. Call this file with the
proper argument before writing the bytes back to disk (in the WriteAt()
function).
Step 7: Allow the Directory file to be extended beyond it's static limit of 10 entries. Only the disk size should limit to the number of files it can hold. It is acceptable to just extend the array (by 1/2 or 1/3 it's current size) each time more DirectoryEntries are needed.
Step 8:
Bonus (10% of project grade) Awarded for Sucessfull Completion
Allow the files to allocate sectors on a demand basis. Extension of
the file should support direct file access with "holes" in the file. When the file is
extended, it should not allocate all the blocks in between the old last block and the one
being written to. In other words, your index data structure should only point to sectors
that have actually had data written into them by some file system operation. It is during
this file extension that your fileHeader may need to be extended to point to new blocks of
block pointers.
You can do this without adding any additional data structures to your FileHeader
implementation by
simply allocating sectors and storing them in the list of sector pointers that corresponds
to that offset.
Examples:
14*SectorSize
in a file with only
2 sectors currently allocated you would You would store the freshly allocated sector in
the 14th element direct sector pointer array.
(NumDirectPointers+65)*SectorSize
in a file with only 2 sectors currently allocated you would You would store the freshly
allocated sector in the 3rd element of the first singly indirect
IndirectPointerBlock
Make sure it is clear why before moving on.
IndirectPointerBlock
In the only
Doubly Indirect pointer block. This will only hold true if you have one doubly
indirect pointer block that gives you the last 900 sector capacity and use some other
means to store the first 124 sectors.
IndirectPointerBlock
In the only
Doubly Indirect pointer block.
ExtendFile
function to take another argument
such as the byte offset that allows a newly allocated sector to be placed in the proper
sector pointer element (see 1-4 above).
FileHeader
ByteToSector
method to
account for fragmented files. The method
should return some "special" value when it encounters a byte offset with no data.
ReadAt()
method in openfile.cc
to
fill a read buffer with '\0' NULL bytes if
the ByteToSector
method tells us there is no data in that sector.
For the following outputs, [pid] is the id of the process on which behalf the operation is performed. [virtualPage] is the involved virtual page number (i.e. the page index into the process virtual address space) and [physicalPage] is the involved physical page number (i.e., the page index into the physical memory of the Nachos virtual machine).
L [pid]: [virtualPage] -> [physicalPage]
S [pid]: [physicalPage]
E [pid]: [physicalPage]
D [pid]: [virtualPage]
Z [pid]: [virtualPage]
For the following outputs, [pid] is the id of the process on which behalf the operation is performed. [fileID] is the ID of the file upon which the operation is performed. [oldSize] previous size of the object (in bytes or entries) and [newSize] is the size it was extended to.
F [pid][fileID]: [oldSize] -> [newSize]
D [pid][dirID]: [oldSize] -> [newSize]
This project is a little more complicated than the previous in it's
dependencies and build requirements. Please read the following
carefully and and follow it closley when attempting to test your code.
Testing Strategy
usage:
csil$ pwd cs170/nachos/code/filesys csil$ ./nachos -f -cp ../test/myFirstProg myFirstProg -x myFirstProg
If there are more than one file referenced in myFirstProg (via Exec), then I will need to pass each on the command line as such:
csil$ ./nachos -f -cp ../test/myFirstProg myFirstProg -cp ../test/myExecedProg myExecedProg -x myFirstProg
Now the Open and close calls will need to work in a _flat_ namespace there is no "mkdir" in nachos so all files on the Nachos disk are in the same un-named directory. This might mean changing the .c files you are testing part 2 with by removing all references to ../test
You will be able to test part 1 without having part 2 implemented. You will be able to test part 2 without having part 1 implemented. The test programs are meant to allow you to get maximal credit for your working code by isolating the functionality and testing each individually. The part 1 test programs don't require large file support and the part 2 programs don't require working VM.
You can wrap your part 1 and part 2 implementations in #defines to allow each to be "removed" allowing the other to be tested independently.
You have to provide a writeup in a file
HW3_WRITEUP
in which you have to mention
which portions of the assignment you have been able to complete. And for the ones
that are incomplete, what is their current status. The
description of status for completed exercises or those that you
did not start should not be more than 1 line. However, when you
submit code that is not working properly (partial solution), make
sure that your documentation is very verbose so that the
TAs can understand what you have achieved and which parts are
missing. This is necessary to receive at least partial
credit. When you just submit something that does not work and give
no explanations, expect to receive no credit. Also include both group members' names
and a short note listing all office hours that both group
members can attend. Make sure that the writeup file is in the top
level code directory. You may or may not be asked to arrive during that
hour for a 10 minute interview (See