In this project you will implement a recoverable virtual memory system like the ones described in the LRVM and Rio Vista papers. Users of your library can create persistent segments of memory and then access them in a sequence of transactions.
You may work in groups of two.
Making the memory persistent is simple: simply mirror each segment of memory to a backing file on disk. The difficult part is implementing transactions. If the client crashes, or if the client explicitly requests an abort, then the memory should be returned to the state it was in before the transaction started.
To implement a recoverable virtual memory system you should use one or more log files. Before writing changes directly to the backing file, you can first write the intended changes to a log file. Then, if the program crashes, it is possible to read the log and see what changes were in progress.
More information is available in the above-mentioned papers. It is up to you how many log files to use and what specific information to write to them.
Your library should implement the following functions:
rvm_t rvm_init(const char *directory) - Initialize the
library with the specified directory as backing store.
void *rvm_map(rvm_t rvm, const char *segname, int
size_to_create) - map a segment from disk into memory. If
the segment does not already exist, then create it and give it
size size_to_create. If the segment exists but
is shorter than size_to_create, then extend it
until it is long enough. It is an error to try to map
the same segment twice.
void rvm_unmap(rvm_t rvm, void *segbase) - unmap a segment
from memory.
void rvm_destroy(rvm_t rvm, const char *segname) -
destroy a segment
completely, erasing its backing store. This function should not
be called on a segment that is currently mapped.
trans_t rvm_begin_trans(rvm_t rvm, int numsegs, void
**segbases) - begin a transaction that will modify the segments
listed in segbases. If any of the specified
segments is already being modified by a transaction, then
the call should fail and return (trans_t) -1.
void rvm_about_to_modify(trans_t tid, void *segbase, int offset,
int size) - declare that the library is about to modify a
specified range of memory in the specified segment. The segment
must be one of the segments specified in the call to
rvm_begin_trans. Your library needs to ensure
that the old memory has been saved, in case an abort is executed.
It is legal call rvm_about_to_modify multiple times
on the same memory area.
void rvm_commit_trans(trans_t tid) - commit all
changes that have been made within the specified transaction.
When the call returns, then enough information should have been
saved to disk so that, even if the program crashes, the changes
will be seen by the program when it restarts.
void rvm_abort_trans(trans_t tid) - undo all changes
that have happened within the specified transaction.
void rvm_truncate_log(rvm_t rvm) - play through any
committed or aborted items in the log file(s) and shrink the
log file(s) as much as possible.
In order to get a feel for how the above API is used, you should
write some test cases that use the above functions and check
whether they worked correctly. To implement your test cases, you will
probably want to use multiple processes, started either with
fork() or by starting programs from a shell script. You
may also want to simulate crashes within the program; the
exit() and abort() functions are useful for
this.
Each test case should print out either "OK" or "ERROR"; if it prints "ERROR" then it should give some description of what went wrong.
I won't grade your test cases, but you are very likely to have fewer bugs in your program the more tests you write. Thus more tests have an indirect effect of increasing your score.
Here are some tests the TA will use:README file including: