Assigned: | Monday, April 19 |
Groups Due: | Wednesday, April 21, 11:59 pm |
Code Due: | Wednesday, May 12, 11:59 pm |
Writeup Due: | Friday, May 14, 11:59 pm |
Collaboration Policy: | Level 1 |
Group Policy: | Pair-optional (but recommended!) |
In this project, you will implement part of a virtual memory paging subsystem. In particular, you will implement an external pager, which is a process that handles virtual memory requests for other application processes. This program will be analogous to the core virtual memory components of an operating system that uses paging.
Your pager will handle address space creation and destruction, page faults, and simple argument passing between virtual and physical address spaces. The pager will manage a fixed range of the virtual address space (called the arena) of each application that uses it. While running, the pager will handle all access requests to virtual memory addresses located in the arena. Valid pages in the arena will be stored in (simulated) physical memory or in (simulated) disk. Your pager will manage these two resources (physical memory and disk space) on behalf of all applications using the pager.
In addition to handling page faults, your pager will provide two system calls to applications:
vm_extend
and vm_syslog
. An application uses vm_extend
to ask the pager to
make another virtual page of its arena valid. You can think of vm_extend
like a very low-level memory allocation routine, on top of which a higher-level allocation library like malloc
could be built (though your test applications will be calling vm_extend
directly). The vm_syslog
function is used
to ask the pager to print a message in memory that exists in the virtual address space of the calling process
(which may seem trivial at first, but is actually quite tricky)!
As a matter of interest, the vm_extend
function is roughly equivalent to the real-world
sbrk
system call in Linux (which is used internally by malloc
or new
to enlarge the usable size of the heap).
Your external pager works in tandem with the CPU's hardware memory management unit (MMU) and exception-handling mechanism. The hardware MMU is automatically invoked on every virtual memory access and performs the following tasks:
Both faults and system calls invoke the exception mechanism. When a system call instruction is executed, the exception mechanism transfers control to the registered kernel handler for the exception.
The MMU and exception functionality in this project are emulated
through a provided software infrastructure. To use this infrastructure, each application
that uses the external pager includes vm_app.h
and links with
libvm_app.a
, while your external pager itself includes vm_pager.h
and links
with libvm_pager.a
. You do not need to understand the mechanisms used to emulate
the hardware components (but in case you're curious, the infrastructure uses mmap
,
mprotect
, signal handlers, named pipes, and remote procedure calls; suffice to say,
there's a lot going on to simulate the regular hardware features of the system in a relatively seamless way).
Linking with these libraries enables application processes to communicate with the pager process in the same way that applications on real hardware communicate with the operating system. Specifically, applications issue load and store instructions (i.e., reads and writes to memory), and these instructions are translated or faulted by the infrastructure exactly as in the above description of the MMU. For faulting instructions and system calls, the infrastructure transfers control to the external pager via function calls.
The following diagram shows how your pager will interact with applications that
use the pager. An application makes a request to the system via the system (function)
calls vm_extend
, vm_syslog
and vm_yield
, or by trying to load or store an
address that is non-resident or protected.
initialize VM system --> vm_init create process --> vm_create end process --> vm_destroy switches to new process --> vm_switch vm_yield --> may switch to new process vm_extend --> system call handler --> vm_extend vm_syslog --> system call handler --> vm_syslog faulting load --> exception handler --> vm_fault faulting store --> exception handler --> vm_fault +-----------+ +----------------+ +----------------+ |APPLICATION| | INFRASTRUCTURE | | EXTERNAL PAGER | +-----------+ +----------------+ +----------------+
Note in the above diagram that there are two versions of vm_extend
and vm_syslog
: one for
applications and one for the pager. The application-side versions of these functions are
implemented in libvm_app.a
and are called by the application process. The
pager-side versions of these functions are implemented by you in your pager. You can think of
the application versions as system call wrappers, while the pager versions
are the OS code that is invoked by making the system calls. When the application calls the
wrapper functions, the infrastructure takes care of invoking vm_extend
or
vm_syslog
in your page. The actual declarations for these functions are
given in vm_app.h
(for the application) and vm_pager.h
(for the pager)
and are described later in this writeup.
A virtual address is composed of a virtual page number and a page offset, as follows:
bit 63-13 bit 12-0 +----------------------+-------------------+ | virtual page number | page offset | +----------------------+-------------------+
The simulated MMU uses a single-level, fixed-size page table.
A page table is an array of page table entries (PTEs), one
PTE per virtual page in the arena. The MMU locates the active page table through the
page table base register (PTBR). In this case, rather than an actual hardware
register, the PTBR is a global variable that is declared and used by the infrastructure,
but will be controlled by your pager. The following portion of vm_pager.h
details the arena,
page table, PTEs, and PTBR.
/* * *********************** * * Definition of arena * * *********************** */ /* page size (in bytes) for the machine */ #define VM_PAGESIZE 8192 /* virtual address at which application's arena starts */ #define VM_ARENA_BASEADDR ((void*) 0x60000000) /* virtual page number at which application's arena starts */ #define VM_ARENA_BASEPAGE ((uintptr_t) VM_ARENA_BASEADDR / VM_PAGESIZE) /* size (in bytes) of arena */ #define VM_ARENA_SIZE 0x20000000 /* * ************************************** * * Definition of page table structure * * ************************************** */ /* * Format of page table entry. * * ppage: the physical page (frame) for this virtual page, if applicable. * read_enable: bit determining whether loads to this virtual page will fault. * (0 ==> fault, 1 ==> no fault) * write_enable: bit determining whether stores to this virtual page will fault. * (0 ==> fault, 1 ==> no fault) */ typedef struct { unsigned long ppage : 51; /* bits 0-50 of pte */ unsigned int read_enable : 1; /* bit 51 of pte */ unsigned int write_enable : 1; /* bit 52 of pte */ } page_table_entry_t; /* * Format of page table. Entries start at virtual page VM_ARENA_BASEPAGE, * i.e. ptes[0] is the page table entry for virtual page VM_ARENA_BASEPAGE. */ typedef struct { page_table_entry_t ptes[VM_ARENA_SIZE / VM_PAGESIZE]; } page_table_t; /* * MMU's page table base register. This variable is defined by the * infrastructure, but it is controlled completely by the student's pager code. */ extern page_table_t* page_table_base_register;
Make sure you understand the behavior of the fields of
the page table entries. Whenever a page is accessed by the MMU, it is accessed
either as a load (i.e., read) or a store (i.e., write). The two protection bits
read_enable
and write_enable
determine whether reads
or writes (respectively) will call vm_fault
. Importantly, note that a
non-resident page access (i.e., a regular page fault) is not the only time
when you might need an attempted page access to fault. In other words, one or both
protection bits might be sometimes set to 0 even in the case of a resident page. Clearly,
a non-resident page would always need both protection bits to be set to 0.
If the protection bits do not trigger a fault, the MMU will access the ppage
field (the frame/physical page number) and then access the requested memory
address. For faulting accesses, the MMU will automatically retry the access
after handling the fault via vm_fault
.
A physical page (frame) may be associated with at most one virtual page at any given time (i.e., no sharing is allowed).
Applications use three system calls to communicate explicitly with the
simulated operating system: vm_extend
, vm_syslog
,
and vm_yield
. The declarations for these system calls are given
in vm_app.h
:
/* * vm_extend * * Ask for the lowest invalid virtual page in the process's arena to * be declared valid. Returns the lowest-numbered byte of the newly * valid virtual page. For example, if the valid part of the arena * before calling vm_extend is 0x60000000-0x60003FFF, the return value * will be 0x60004000, and the resulting valid part of the arena will * be 0x60000000-0x60005FFF. The newly-allocated page is initialized to * all zero bytes. Returns null if the new page cannot be allocated. */ extern void* vm_extend(); /* * vm_syslog * * Ask external pager to log a message of nonzero length len. Message data * must be in the part of the address space controlled by the pager. * Returns 0 on success or -1 on failure. */ extern int vm_syslog(void* message, unsigned len); /* * vm_yield * * Ask operating system to yield the CPU to another process. The * infrastructure's scheduler is non-preemptive, so a process runs * until it calls vm_yield or exits. */ extern void vm_yield();
The following is a sample application program that uses the external pager.
This application allocates one virtual page within the arena, writes five bytes to it, then asks the pager to log
the five bytes just written. Note that since vm_syslog
is asking the pager
to log the specified message, it doesn't result in any output from the application process.
// sample.cc - a sample application program that uses the external pager #include "vm_app.h" int main() { char* p; p = (char*) vm_extend(); // p is an address in the arena p[0] = 'h'; p[1] = 'e'; p[2] = 'l'; p[3] = 'l'; p[4] = 'o'; vm_syslog(p, 5); // pager logs "hello" return 0; }
The functions that you must implement in your pager are declared in vm_pager.h
. These declarations are shown below.
Note you will not implement a main
function; instead, main
is included in libvm_pager.a
and included in your pager during compilation.
The infrastructure will invoke your pager functions as described previously.
/* * vm_init * * Initializes the pager and any associated data structures. Called automatically * on pager startup. Passed the number of physical memory pages and the number of * disk blocks in the raw disk. */ extern void vm_init(unsigned memory_pages, unsigned disk_blocks); /* * vm_create * * Notifies the pager that a new process with the given process ID has been created. * The new process will only run when it's switched to via vm_switch. */ extern void vm_create(pid_t pid); /* * vm_switch * * Notifies the pager that the kernel is switching to a new process with the * given pid. */ extern void vm_switch(pid_t pid); /* * vm_fault * * Handle a fault that occurred at the given virtual address. The write flag * is 1 if the faulting access was a write or 0 if the faulting access was a * read. Returns -1 if the faulting address corresponds to an invalid page * or 0 otherwise (having handled the fault appropriately). */ extern int vm_fault(void* addr, bool write_flag); /* * vm_destroy * * Notifies the pager that the current process has exited and should be * deallocated. */ extern void vm_destroy(); /* * vm_extend * * Declare as valid the lowest invalid virtual page in the current process's * arena. Returns the lowest-numbered byte of the newly valid virtual page. * For example, if the valid part of the arena before calling vm_extend is * 0x60000000-0x60003FFF, vm_extend will return 0x60004000 and the resulting * valid part of the arena will be 0x60000000-0x60005FFF. The newly-allocated * page is allocated a disk block in swap space and should present a zero-filled * view to the application. Returns null if the new page cannot be allocated. */ extern void* vm_extend(); /* * vm_syslog * * Log (i.e., print) a message in the arena at the given address with the * given nonzero length. Returns 0 on success or -1 if the specified message * address or length is invalid. */ extern int vm_syslog(void* message, unsigned len);
More details on the proper behavior of vm_fault
and vm_syslog
are provided below.
If a fault occurs on a virtual page that is not resident, you must find a physical page (frame) to associate with the virtual page. If there are no free physical pages, you must create a free physical page by evicting a virtual page that is currently resident.
The pager must use the second-chance (clock) algorithm to select a victim. The clock queue is an ordered list of all valid, resident virtual pages in the system (i.e., global replacement). To select a victim, check the next physical page in the queue. If it has been accessed in any way since it was last, continue searching to the next page in the queue (and clear its reference bit).
If the next physical page in the queue has not been accessed, then its virtual page should be evicted. Dirty and clean pages are treated the same when selecting a victim page to evict (i.e., don't continue searching past a dirty eviction candidate in order to locate a clean candidate). Additionally, you should not write out a dirty page to disk unless you're actually evicting that page.
The vm_syslog
routine is called with a pointer to an array of bytes
in the current process's virtual address space and the length of that array. The pager
should first check that the entire message is in valid pages of the arena.
Return -1 (and don't print anything) if any part of the message is not in a
valid arena page, or if the message length is zero.
After checking the message validity, the pager should copy the entire message
into a C++ string in the pager's address space, then print the C++ string
to cout
. You must use exactly the following formatting for your print statement (assuming
your C++ string is named str
):
cout << "syslog \t\t\t" << str << endl;
You must treat access to the message by vm_syslog
exactly the same
as if the application had accessed the message itself (e.g., for purposes of
residency, reference, and so forth). In other words, syslogging a message should produce the
same behavior as if the application had accessed each byte of the message, starting from
the lowest virtual address and proceeding towards the highest virtual address.
The print statement in vm_syslog
should be the only output generated by
your pager code. However, the pager infrastructure generates a significant amount of
additional output itself (detailing the current operation of the pager). You can disable
the infrastructure output during testing by passing the -q
flag when running the pager.
There are many points in this project where you have some freedom over when many types of work occur, such as zero-fills, faults, and disk I/O operations. You must defer such work as far into the future as possible (or even better, avoid it entirely). Performing appropriate work deferral is part of the required project specification, not simply a best practice. Deferring work to the maximum extent possible is one of the trickier parts of the project!
As a simple example of work deferral, if a page that is being evicted does not need to be written to disk, don't do so. However, make sure that you don't modify the page replacement algorithm (or any other aspect of the specification) in order to defer or avoid work.
There are cases where you might need to maintain extra state for the purpose of deferring or avoiding work. Carefully look for these cases!
If you could possibly defer or avoid some action at a cost of performing a different action, keep in mind the relative costs of various operations. Triggering a fault (about 5 microseconds) is cheaper than zero-filling a page (about 30 microseconds), which in turn is much cheaper than a disk I/O operation (about 10,000 microseconds). For instance, if you have a choice between incurring an extra fault or causing an extra disk I/O, you should take the extra fault.
This section describes how the external pager accesses simulated hardware (i.e. physical memory, disk, and the MMU).
Physical memory is structured as a contiguous collection of N phyiscal pages (frames), numbered
from 0 to N-1. The number of physical pages is configurable via the -m
command-line argument when executing the pager (e.g. by running ./pager -m 4
).
The minimum number of physical pages is 2, the maximum is 128, and the default is 4.
The disk is modeled as a single device that is a fixed number of "blocks" long, where each disk block is the same size as a physical memory page.
The pager controls the operation of the MMU by modifying the contents of
the page tables and the page_table_base_register
(PTBR) variable. Remember that
although the MMU accesses the page tables automatically for the purpose of performing regular
address translation, it is your pager's responsibility to allocate and maintain the page tables appropriately.
The following portion of vm_pager.h
describes the variables and utility
functions for accessing the disk and physical memory (the page tables and PTBR were shown previously):
/* * ********************************************* * * Public interface for the disk abstraction * * ********************************************* * * Disk blocks are numbered from 0 to (disk_blocks-1), where disk_blocks * is the parameter passed to vm_init. */ /* * disk_read * * Read the specified disk block into the specified physical memory page. */ extern void disk_read(unsigned block, unsigned ppage); /* * disk_write * * Write the contents of the specified physical memory page onto the specified * disk block. */ extern void disk_write(unsigned block, unsigned ppage); /* * ******************************************************** * * Public interface for the physical memory abstraction * * ******************************************************** * * Physical memory pages are numbered from 0 to (memory_pages-1), where * memory_pages is the parameter passed to vm_init. * * The pager accesses the data in physical memory through the variable * pm_physmem, e.g. ((char*) pm_physmem)[5] is byte 5 in physical memory. */ extern void* pm_physmem;
Note that unlike the page tables and PTBR, the pager is not responsible
for initializing either the disk or physical memory. The pager is given the number of physical pages
and disk blocks via vm_init
, but it should not try to allocate
actual disk blocks or physical memory space. Instead, it will work with disk_read
,
disk_write
, and pm_physmem
, which are already defined and initialized
by the infrastructure. You should not make any assumptions about the starting contents of physical memory or the disk blocks.
As in the previous project, you will submit a suite of test cases that exercises
your pager. Each test case for the pager will be a C++ program that uses the pager
via the client interface defined in vm_app.h
, and should be run without
any arguments and without using any input files.
The filename of each test case must specify the number of physical memory pages to use when
launching the pager that the test case will use. Specifically, the name
of each test case must be of the format anyName.N.cc
,
where N
is the number of physical memory pages. For example,
you might name a test case myTest.4.cc
. Remember that the minimum
number of physical memory pages is 2 and the maximum number is 128.
Your test suite may contain up to 20 test cases. Each test case may cause a correct pager to generate at most 256 KB of output and must take less than 60 seconds to run (these limits are much larger than needed). You will submit your suite of test cases together with your pager to the autograder.
You should test your pager with both single and multiple application processes running. However, your submitted test suite need only consist of single process tests; none of the buggy pagers used to evaluate your test suite require multi-process applications to be exposed.
Finally, note that the autograder will check your test cases against the
buggy pagers according to the pager (NOT the application) output.
In other words, a test case exposes a buggy pager by causing the buggy pager
to generate pager output that differs from the correct pager output. Of course, an
application call to vm_syslog
will directly correspond to
a line of pager output, but the additional infrastructure output provides much more
detail into the behavior of the pager. Since the only output your own pager code
ever produces is the single line in vm_syslog
, most of the output
that is checked to expose the buggy pagers will be the infrastructure output
that is indirectly generated by applications interacting with the pager.
General and specific advice on tackling the project is provided below.
One of the first things you should do is to write down a state-based flowchart
(aka finite state machine) for the life of a virtual page, from creation via
vm_extend
to destruction via vm_destroy
. Represent
each virtual page as a series of bits (i.e., the relevant state of a page),
where each state in the flowchart represents a specific setting of bits.
You will, of course, need to decide what bits you need to represent each state.
Some of these bits are straightforward (e.g., the page protection bits and the reference
and dirty bits), but other bits may be needed as well.
Ask yourself what events can happen to a page at each stage of its
lifecycle (one example: the application reads the page) and what effect each such event
will have on its state. As you
design the state machine, try to identify all of the places where work can
be deferred or avoided. This exercise may initially seem academic and
unnecessary, but the correctness of your program will critically depend on correctly
designing (and following) the state machine. I am happy to offer feedback on
state machine designs!
One practical use of your state machine is to help in designing your test suite. For example, you can trace through different transition paths that a page can take through the state machine, then write a short test case that causes a page to take each path.
One of the key mental hurdles in this project is understanding the full role
of vm_fault
within the pager. In particular, understand that
vm_fault
is not simply called when a non-resident
page is accessed: while a regular page fault is one specific case in which a fault should occur, it is not
the only case. Faulting is how your pager takes control from an active process
and is given an opportunity to update internal state, perform any needed bookkeeping, etc.
Thus, for any scenario where you need your pager to perform such tasks (even if not
attempting to access a non-resident page), you will need to ensure that faults occur
as needed by setting the page protection bits read_enable
and write_enable
accordingly.
When you are ready to begin coding, you should start with the basic pager functions
that are automatically called by the infrastructure for any new process; namely,
vm_init
, vm_create
, and vm_switch
(there's also
vm_destroy
, but that's not initially essential). You will need to set up
your core data structures within these functions.
Once these basic functions are working, move onto vm_extend
and
vm_fault
, which will allow an application to actually make use of the arena
memory.
Although it may seem counterintuitive, it is recommended to avoid tackling
vm_syslog
until most of the rest of the pager is working. You can pass
several of the autograder tests without having implemented syslog at all (remembering
that most of the output used to check your pager comes from the infrastructure itself,
not from vm_syslog
).
Specific tips on each of the pager functions are provided below:
vm_init
when the pager starts. This function should
set up whatever data structures you need to begin accepting vm_create
calls and
subsequent requests from processes.
vm_create
when a new application process starts. You
should initialize whatever data structures you need to handle the new process and
its subsequent calls to the pager. The process's initial page table should be empty,
since there are no valid virtual pages in its arena until vm_extend
is called.
Note that the new process will not be running until after it is switched to via
vm_switch
.
vm_switch
whenever the OS scheduler switches to
a new process. This function allows your pager to do whatever bookkeeping is needed
to register the fact that a new process is running.
vm_destroy
when the current application
process exits. This routine must deallocate all pager resources held by that process,
which might include page tables, physical pages, and disk blocks.
vm_extend
when it wants to make another virtual page in its
arena valid. Each new page should be backed by a disk block in swap space, which is used to store
the page when it is not resident in physical memory. This approach is called "eager" swap
allocation, since swap space is allocated up-front rather than when a page needs to be evicted
to disk. Remember that an application should see each byte of a newly extended virtual
page as initialized with the value 0. However, the actual data initialization
needed to provide this abstraction should be delayed as long as possible (another example
of work deferral).
vm_fault
routine is called in response to a read or write fault by the
application. Your pager determines which accesses in the arena will generate
faults by setting the read_enable
and write_enable
bits in the page table.
Remember that a faulting instruction is automatically retried after handling the fault via
vm_fault
(assuming 0 is returned); thus, you need to be careful to ensure
that the reattempted instruction will not immediately fault again.
vm_syslog
will be copying the array into the pager's C++
string. Note that this will be the one place in the pager where you will be performing virtual to
physical address translation (since this task is normally performed by the MMU). Be careful
to ensure that syslog treats access to the message exactly the same as the application itself.
For diagnostic printing in your pager, don't use cout
, as any extra output
sent to standard out (except for syslog
) will cause your pager to fail
test cases. Instead, print to standard error, i.e., using cerr
instead
of cout
. This will allow you to leave active debugging statements when
submitting to the autograder.
Liberal use of assertion statements is always a good idea to check for unexpected conditions generated by bugs.
Debugging in GDB is possible, but complicated somewhat by the impact of the
infrastructure on the pager operation, particularly if the pager is missing basic
functionality. At least initially, effective use of GDB may be limited until you've implemented
the most basic parts of the pager (e.g., vm_init
, vm_create
and vm_switch
).
As usual, starter code will be distributed via GitHub. GitHub repositories will be made available once groups are assigned (each group will share one repository).
Write your pager code inside pager.cc
and write each test case in a separate
file named anyname.N.cc
, where N
is the number of physical memory
pages to use for that test (which must be between 2 and 128). The included sample.4.cc
is an example of a test case. The pager infrastructure is Linux-specific and will not run on
any other system. Your program may use any functions in the standard C++ library, including (and
especially) the STL. You should not use any libraries other than the standard C++ library.
The public functions in vm_pager.h
are
declared extern
(meaning that code outside of the pager can call them), but all
other functions and global variables in your pager should be declared static
(meaning
that code outside of the pager will not see them) to prevent naming conflicts.
Compiling and testing your pager requires compiling your pager code in pager.cc
as well as an application program that uses the pager, such as sample.4.cc
. Note that
both the pager and the application program are separate executables, so they must be compiled and run separately.
You can compile the pager using the include Makefile
by simply running make
,
which will output an executable named pager
. The initial pager.cc
file contains
a dummy implementation of each required pager function, so you can compile the pager immediately. Separately,
to compile an application program (or test case) named mytest.N.cc
into an executable named
mytest
, you can run make mytest.N
. You can substitute whatever test program name you
wish (e.g., make sample.4
to build the sample test case).
To run your pager and an application, first start the pager (e.g., by running ./pager
). Remember that
you can suppress the regular infrastructure output by passing the -q
flag and/or specify the number
of physical memory pages via the -m
flag (although the number of disk blocks is non-configurable).
While the pager is running, you can then run one or more application processes
that will interact with the pager. For example, in a separate window, you could run ./sample.4
to run the sample test case. The same user must run the
pager and any applications that use the pager, and all processes must run on the same machine.
Sometimes the pager may get "stuck" and appear to still be running, but applications are unable to connect to it (this may happen if a buggy application previously crashed while using the pager). If an application appears to hang when started and cannot communicate with the pager, you may need to restart the pager process and try again.
You can submit your program to the autograder as follows (the pager must be named pager.cc
, but
the test programs can be named whatever you wish in the appropriate format):
submit3310 4 pager.cc test1.4.cc test2.4.cc ...
To more easily submit all your source files at once (pager.cc
and all of your test programs),
you can use a shell wildcard, like so:
submit3310 4 *.cc
Feedback note: The nature of this project means that the autograder evaluation of your submission may take substantially longer than in past projects. Expect a finished and reasonably efficient pager to take roughly 10-15 minutes to complete the primary pager test suite (an incomplete, buggy, or inefficient pager may take more or less time). Evaluating your test cases will take additional time; given the number of buggy pagers, it is very possible for a submission that includes a large number of test cases to take 1-2 hours to complete testing. Be patient and plan accordingly! However, if you have not received autograder feedback within 4 hours of submission, there is likely something wrong and you should let me know.
Note that your submissions are timestamped according to when you submit them, not when the feedback email is generated (so if you submit at 11:55 pm, it will still count as that day's submission even if you don't get feedback until after midnight).
In addition to writing the program itself, you will also write a short paper (~3-5 pages) that describes your pager. Your paper should include the following:
Remember that the writeup is a supplement (not a substitute) for the code itself. Strive to minimize any degree to which your writeup is 'code-like'. Instead, you should approach the writeup like a technical paper (providing clear, prose explanations that can be supplemented by reading the code). You need not try to include every technical detail in the writeup; focus on the major challenges and design decisions as you might explain them to another computer science student.
Upload your writeup as a PDF to Blackboard by the writeup deadline. You only need to submit one copy of the writeup per team. Typesetting your writeup in LaTeX is encouraged but not required.
Your project will be graded on program correctness, design, and style, as well as the quality of your project writeup. Remember that the autograder will only check the correctness of your program, nothing else!
You can (and should) consult the Coding Design & Style Guide for tips on design and style issues. Please ask if you have any questions about what constitutes good program design and/or style that are not covered by the guide.