Introduction
In this assignment, we will be augmenting a language virtual machine. In essence, a virtual machine is a program that hosts programs in another programming language, providing a device independent layer for cross platform execution. For example, the Java virtual machine executes Java programs. A virtual machine is actually similar to an operating system in a lot of ways, especially when the hosted language is actually compiled at runtime into assembly language and run directly on the CPU. This is know as Just-In-Time (JIT) compilation and our virtual machine will do something pretty similar.
Learning Objectives
At the end of the this assignment, you should be able to:
- Catch segmentation faults and implement custom handlers for them.
- Manage memory for forth instances (basically processes) by allocating pages for them only upon need.
- Map memory pages onto disk and swap pages back and forth between physical memory and disk.
Getting the Source Code
For this lab, you will be using the native Linux virtual machine (or baremetal machine if you have one) and not the xv6 operation system. Please note that this assignment will NOT run on MacOSX, you should be running a Linux machine using the x86 architecture.
To obtain the starting code for this lab, navigate to the top level directory of
your csse332 class repository and cd
into the labs/lab04
directory as
follows:
$ cd /path/to/csse332/git/repository/
$ git pull
$ cd labs/lab04
Starter Code
Forth
The language our virtual machine will be hosting is known as
Forth.
It is not that important for you to fully understand this language, but you
might find it handy to know a bit. The Wikipedia page is a good introduction,
and if you’d like to try some code you can type make interactive
with the
provided makefile
. This will build an interactive forth that you can issue
commands to and see output:
$ make interative
$ ./jonesforth.bin forth/jonesforth.f
Welcome to forth! Press ^D to quit.
3 2 + .
5
Paged Forth
Using the interactive version of forth is a great way to get introduced to the
language, however, when in this lab, we would like to provide input to forth as
a C string and read outputs from it as a C string. For that, take a look at the
paged_forth.c
file. To run the code, use the following steps:
$ make pagedforth.bin
gcc -no-pie -ggdb -Wall -c paged_forth.c -o paged_forth.o
gcc -no-pie -ggdb -Wall -c forth/forth_embed.c -o forth_embed.o
gcc -no-pie -ggdb -Wall -c forth/myjf.S -o jonesforth.o
gcc -no-pie -ggdb -Wall -o pagedforth.bin forth_embed.o jonesforth.o paged_forth.o
$ ./pagedforth.bin
finished loading starter forth
OUTPUT: 4999 5000 finished successfully
done
The comments in paged_forth.c
should be mostly self-explanatory, but a few
details here might be helpful:
mmap
is mostly a more powerful version ofmalloc
. We will be using a lot of its features in this lab, so you might want to familiarize yourself with its man page.- The
forth_data
struct is a structure that represents the total state of a forth system (similar to the process control block for processes in our operating system discussions). However, it does not allocate space for the forth return stack, stack, or heap. This memory must be allocated by the user and then be passed to theinitialize_forth_data
function, which sets theforth_data
struct up. The existing code does all of that for you. - The second parameter passed to
f_run
is the forth code we would like to run. - The third parameter passed to
f_run
is the buffer where forth’s output should go.
Step 1
Goals
Like most programs, forth expects memory to be a contiguous space with the stack at the top (high memory) and the heap at the bottom (low memory). Forth uses both parts of the memory. It uses the stack to store its data stack, and uses the heap to store its function definitions and some globals. These two memory locations grow towards each other but hopefully never get so large they overlap.
Like an operating system does, we would like to start the stack and the heap
very fat apart to accommodate the largest possible program. However, similar to
modern operating systems, we would prefer if we did not have to allocate all
that memory upfront because most programs won’t use all of it. In the
paged_forth.c
right now, we allocate the entire (20 pages) memory block
upfront. Our first step is to change that so that we only allocate a page of
memory when it is needed.
Here’s a possible plan to achieve that:
- We want to initially pass the forth instance pointers to a memory region that is actually invalid.
- Then we will set up a signal handler that detects segmentation faults.
- We will determine the address that is failing and determine which page that bad address corresponds to.
- We will use
mmap
to allocate memory for that exact page, at the particular address we need it to be. Note thatmmap
must be placed at a page boundary (i.e., allocations must not overlap). - We will then return from the signal handler and allow the erroring code to run again, which should succeed because the requested address is now valid.
- Code will continue until another invalid page is referenced.
- Eventually, we will have loaded only the parts of the stack/heap we actually use, while leaving unreferenced memory unallocated.
Implementation
Take a look at segfault_catch_example.c
, this shows you how to correctly
install a signal handler to catch a segmentation fault, and then resume
operations after the segmentation fault is resolved. Use that code as your
starting point to make changed to paged_forth.c
.
The given code always maps the same page, but you will need to edit it. Your
code will need to use some pointer math to determine in which page (of the
possible 20 pages) the address that causes the segmentation fault resides. Then,
you will compute the beginning of that page, and then mmap
it.
You will want to modify the line in main
so that the stackheap is not actually
allocated. So the line:
void* stackheap = mmap(NULL, stackheap_size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_ANON | MAP_PRIVATE, -1, 0);
becomes
void *stackheap = (void*) STACKHEAP_MEM_START;
Testing
When completing the above steps, your output should look as follows:
in handler with invalid address 0xf9f9fff8
mapping page 19
in handler with invalid address 0xf9f8c000
mapping page 0
in handler with invalid address 0xf9f8d000
mapping page 1
in handler with invalid address 0xf9f9eff8
mapping page 18
in handler with invalid address 0xf9f9dff8
mapping page 17
in handler with invalid address 0xf9f9cff8
mapping page 16
in handler with invalid address 0xf9f9bff8
mapping page 15
in handler with invalid address 0xf9f9aff8
mapping page 14
in handler with invalid address 0xf9f99ff8
mapping page 13
in handler with invalid address 0xf9f98ff8
mapping page 12
in handler with invalid address 0xf9f97ff8
mapping page 11
in handler with invalid address 0xf9f96ff8
mapping page 10
OUTPUT: 4999 5000 finished successfully
done
Make sure your output matches the above before going on to step 2.
Step 2
Goals
In this step, we would like to further improve the memory footprint of our forth instances by writing infrequently used memory pages to files on disk.
Here’s a possible plan of action:
- Our forth instance will have a maximum number of pages we want to keep in
memory at one time, that is defined by the
MAX_PAGES
constant. This can be set to anything that is 3 or higher. - When we catch a segmentation fault indicating we need to load a new page, we will see if we’re already at our max pages.
- If we are, we will first write our oldest page onto disk, then
unmap
it from memory. - Then we will
mmap
the newly required page into memory. If it’s a page we will loaded before, we will load that page from the file instead. - We will probably have to update some metadata to keep track of what pages have been in memory the longest.
mmap
This problem is made much easier by the fact that mmap
has a feature that
allows to map a file into a particular region of memory. If we do this, the
memory region will be populated with the file’s content when it is created.
Also, any modifications to the memory page will automatically be transmitted
back to the file. So mmap
will handle most of the work of file operations.
Take a look at mmap_file_example.c
for details. Note that this example remove
the MAP_ANONYMOUS
option from the mmap
parameters. This is important because
anonymous means not mapping to a disk or file, which was true in the first
part but is no longer true for this step.
Tracking the Oldest
It is important that we select the right page to unmap
when a new page needs
to be swapped in. Otherwise, we can generate an infinite loop. Imagine for
example that we use an algorithm that always swaps out the smallest numbered
page. Imagine our code is attempting to execute an assembly language
instruction stored in page 1 to an address stored in page 2.
If pages 1 and 3 and 4 are loaded, this will cause a segmentation fault and we will replace page 1 with page 2. Now pages 2, 3, and 4 are loaded. This will cause a segmentation fault when accessing page 1, so we will replace page 2 with page 1.
Hopefully, you can see the loop taking place. Alternatively, if we always swap the oldest page, we can avoid this problem. Even better might be to track the most recently used pages, but unfortunately, our system can only detect when blocks are loaded not when an already loaded block is accessed.
You can use any mechanism you like to detect the oldest page (and they should all be equivalent), but here’s my approach:
- Store a
priority
for every page in the system. When a page enter (i.e., is loaded), give it the best priority, which is the max allowed number of pages. - When another page is loaded, loop through all existing pages and drop their priority by 1. If any page drops to 0 priority (only one should be at a time, at most), then unmap it from memory.
Page States
A page in this system can be in 3 states:
- Active.
- Never mapped before.
- Swapped out.
If you need to swap in a page for the first time, you need to open a file descriptor and create a file of the appropriate size.
When a page is unmapped, you don’t have to lost the file descriptor. Instead, keep it saved but unmap the memory, mark the page as swapped out.
When a page is swapped in, reuse the file descriptor but remap the file.
Testing
The file finaloutput.txt
contains my solution’s output when MAX_PAGES = 3
.
To see if yours matches exactly, run your code and pipe the output to a file,
and then use the diff
command as follows:
$ ./pagedforth > testout.txt
$ diff finaloutput.txt testout.txt
There should not be anything printed out on the command line.
Submitting your code
There are no special submission requirements for this lab, you only have to
submit the paged_forth.c
file. Please do not submit any other files than
paged_forth.c
.
Submission Checklist
- My code compiles and generates the right executables.
- I submitted
paged_forth.c
to Gradescope.
Rubric
Part | Points |
---|---|
Step 1: manages to catch segfaults | 20 |
Step 1: works correctly and matches output | 40 |
Step 2: produces some files | 15 |
Step 2: seems to be swapping pages in and out but not with FIFO algorithm | 15 |
Step 2: works correctly and matches output | 50 |