CS 372 Operating Systems

Lab 4 Programming Assignment:
File Systems

Clarifications and corrections are in pink.


Due: Tuesday Dec 1, 11:59pm


Lab Purpose

In this lab you will build a small file system.  The primary purpose of the lab is to familiarize you with the implementation techniques for file systems, including on-disk and in-memory data structures.

The second goal of this lab is to tie together three major topics of the course: scheduling, managing memory, and file systems.   Your simulated OS will have a scheduler because processes block when they access the disk.  These schedulers will be very similar to the schedulers you wrote in lab 1.  You manage memory because you must buffer disk blocks in memory pages, but you won't be able to buffer all of the disk, and so you must implement a memory replacement policy. Finally, all this work is in the context of implementing a file system with a single-level directory structure.

The third goal is to give you experience learning and modifying a large software project.  If you work in industry, you will often need to come up to speed on a large project with lots of code and spotty documentation.  Our documentation, while not perfect, is above average.  When you first approach the lab much of your time will just be spent getting your head around the code that exists.  That takes a while, so start early.

A final benefit is that  the program infrastructure we give you might contain interesting programming idioms or design patterns that you can use in future projects.  Please look around the code.

Start this lab early.  It is hard.

Trace Files

Before diving into the design specification for your file system, lets look at how you use the system.  The system reads a trace file which contains system calls made by simulated processes. The details of how the file is read and how events are fed to your simulated OS is handled by the code that we provide.  Your system responds to these user requests, schedules disk operations and deals with disk interrupts. 

You should always make sure your code is compiled. Eclipse takes care of that for you, but javac *.java is simple and effective from the command line.

The workflow for the simulator is to create a simulated disk, run a trace file on the simulated disk, and then check the simulated disk and the read check file for correctness.  The read check file records the values of every (non-zero) read request, so you can see the earliest point at which you read bad data.  Here is an example:
java Mkfs test_disk 128k       Make a simulated disk called, "test_disk", that is 128KB
java ProcessTrace                     Run the trace simple0.tr on test_disk (both values are defaults)
This is explained in greater depth below.  The Mkfs.java we give you does not work, so your first task is to complete its implementation.  When we evaluate your project, we will diff the final disk image against the final image produced by our implementation starting from the same clean file system (we will also check your Mkfs to make sure it creates a valid file system).  Similarly we will diff the read check files.  Any deviation will result in losing points.

Processing the Trace

The pid of the initial process (called init) is zero.  Like a real operating system, as part of its initialization, the OS code we gave you will start the init process.  A well formed trace file starts with an action of process 0.   After the first action, you can treat process 0 like any other process, e.g., it can kill itself.

After your code processes all events in the trace file, the ProcessTrace class calls the OS's shutdown method.  This method should clean up any loose ends, like flushing all disk buffers that reside in memory back onto the disk itself.

  0 proc_create(100)
100 create(/fileA)
100 open(/fileA, O_RDWR) = 3


This is the start of simple0.tr.  The individual calls will be explained below, but the init process (pid=0) starts a process, with identifier 100.  Process 100 creates a file (called fileA, located in directory /).  Then process 100 opens the file, with read and write permission, and file descriptor 3 will refer to the open file.  Each entry in the trace file is prefixed by the process id that
performs the system call.  The return value is depicted on the right, by the "= 3" for the open.  See the sample trace files for more examples.

Supported System Calls

Your system must support the following system calls.  The prototypes are conceptual.  See the trace file and the code for the concrete details.

create(String name)  This system
call creates a simulated file with the given name.     A process must create a file before opening it.  Unix implements create by overloading the open system call and setting the O_CREATE flag to true.  Having a separate create call will simplify your code.  Specifically, create might have to allocate an  inode, while open never will.   Open does not allocate any data for a file (doing so would go against the goal of supporting many small files, including zero-length files). Report an error if the system tries to create a create a file with the same name as an existent file. 

open(String name, int flags)  This system call opens the (simulated) file called, "name", with the flag values in the flags parameter.  It reports an error if the file was not previously created.  It returns a small integer called a file descriptor.  File descriptors are small integers that index a per-process open file table.  In our system, file descriptors are unique across processes (they index a global table).  So if pid 100 is using file descriptor 3, then no other pid can open a file with descriptor 3.   Any attempt to do this should return Error.BAD_FILE_DESCRIPTOR. After a file that was using file descriptor X is closed, a newly opened file can use file descriptor X. 

Supported flags are O_RDONLY, O_WRONLY, O_RDWR, and O_APPEND.   O_RDONLY means the file is only open for reading.  Any writes to a file opened O_RDONLY fail  and print an  error message.  O_WRONLY means the file is only open for writing.  Any  reads from an O_WRONLY file fail and print an error message.  O_RDWR means the file is open for reading and writing.  O_APPEND means the file is opened, and the file pointer is set to the end of the file.  O_APPEND is an option to all of the other modes. It is used with other modes. i.e. O_RDONLY|O_APPEND or O_WRONLY|O_APPEND.  Report an error if the system tries to open a file that has not been created.

close(int fd) This system call closes the (simulated) file referred to by the file descriptor numbered fd.  Any future read or write to this file descriptor (without an intervening open) will fail with an error.  The file descriptor fd is now free and can be reassigned to another file. On a close you must flush to disk all the data blocks of this file in the buffer cache, and the inode block of this file.  A real OS does not flush on close, but doing this will shorten the distance between your having a problem and discovering it.

read(int fd, int size) This system call reads from the file referred to by the file descriptor numbered fd.  The read happens at the location of the current file pointer, and the file pointer's position is updated.
If a read is too long, and would exceed the maximum file size, do as much of it as you can before returning an error. After all, when something goes wrong with your computer, wouldn't you want to get some data rather than no data? Your implementation of read should call ReadCheck.write with the pid doing the reading and the data read. Only call ReadCheck.write if you have a non-zero sized read. You can call it for each block read or for all the data at once.

write(int fd, int size) = 0xXX
This system call writes to the file referred to by the file descriptor numbered fd.  The write happens at the location of the current file pointer, and the file pointer's position is updated.  The write call is followed by a single hex byte, which is the data value written.  For instance
write(3, 50) = 0xAA writes 50 bytes whose value is 0xAA at the current file pointer for the file referred to by file descriptor 3.  Because our trace file does not model how a process modifies memory between reading and writing it to disk, we use this mechanism to insure that we write non-zero data to the disk.  In your implementation, you will have to do the work of making sure the data ends up in the disk block you are writing.
If a write is too long, and would exceed the maximum file size, do as much of it as you can before printing an error. Return the number of bytes written (which can be 0).

seek(int fd, int whence, int offset)  This system call sets the file pointer for the file referred to by the file descriptor numbered fd.  Valid values of whence are SEEK_SET, SEEK_CUR, and SEEK_END.  These have their standard meanings, SEEK_SET sets the file pointer to offset bytes from the start of the file.  SEEK_CUR sets the file pointer to its current value plus offset bytes.  SEEK_END sets the file pointer at the end of the file.   Unix and windows file systems allow you to seek past the end of a file, creating files with "holes."  You do not need to support this behavior.

proc_create(int pid)  This system call creates a process with an identifier equal to  pid.

proc_kill(int pid) This system call kills the process with the identifier equal to pid.  If the system kills a process that has I/O pending, the  OS must wait for the I/O to complete before killing the process, because the process is locking pages that are being modified by the I/O device (the disk in this case).  This is why when you type kill -9 XXX and the process with pid XXX is in state D (blocked on I/O), the process will not exit, but will wait for the I/O to complete.  If the I/O is a read from a network file system whose server has crashed, this wait can take a very long time. Who thought killing a process could be such trouble?  In our lab a process may kill only itself.

Notice that there is no system call to remove (called unlink for interesting reasons in UNIX) a file.  That helps keep your implementation simpler.  Mkfs a new file system is the only way to get rid of data.  Please try to grok this---read the Beware Crashes section (below).

Error Output

In your implementation, you should print appropriate error output. Please see Error.java for the API, your calls will look like this Error.println(Error.XXXXX, fileName). When the code finds an error, return -1 from the system call. The standard convention in UNIX/Linux is to return a negative value on an error. The real OS returns a particular number indicating the actual error. We will just use -1. There are 6 kinds of errors.

Beware Crashes

Your system does data handling, reading information from the simulated disk image (which resides in a binary file), and copying it into simulated memory pages.  The metadata you store on disk will be "real" in the sense that you will read the metadata from simulated disk and use those bytes to create your internal data structures.  This is exactly analogous to the operation of a real operating system which buffers disk blocks in memory, but also maintains in-memory copy of disk-based data structures like inodes.  The in-memory data structures are not exactly the same as the on-disk versions, usually because the in-memory copies have more information, like additional pointers.  Also, as user programs modify the file system, those modifications are first buffered in memory before they are written to the disk.

If you make a change to in-memory file system metadata, you must be sure to flush that change to disk if you replace the page (or when you close the file, or before the OS completes its shut down). 
If you do not flush your in-memory changes, then you can end up with a corrupt file system, just like real life.  Flushing updates back to disk is the main activity done by your OS when it shuts down.  If you have a bug in your code and your simulation crashes in the middle of a run, you might leave the file system in an inconsistent state, or it might be consistent but not what you want, like 1 out of 3 files were successfully created.  So while you are debugging, we suggest running mkfs to create a new file system before each test.  

BufferCache flush policy

Your code buffers disk data in memory.  The memory cache of disk data is called the buffer cache.  It holds data pages from disk files and inodes (metadata from the file system).  You need some policy to write this data back to the disk.  Here is that policy.
  • VFS.close(): (flush data block before inode block)
    1. flush all data blocks of that file if it is in the buffer cache
    2. flush the inode block where the inode of the file is located
  • BufferCache: when the replacement policy tells you to remove a page, flush the page before you remove it.
  • VFS.shutdown(): flush all the pages to the disk (this method is called right before simulator terminates)
You might question why we ask you to flush file changes on close.  That is a reasonable question.  We specify this mainly to help you debug your code since after you close a file, your changes will be visible on the disk right away, and not at some indeterminate point in the future.

Properties File

The simulator reads a file called system.properties.  This file specifies values for the following variables (their default values are shown).  If you only override a proper subset of these values, the remainder will have their default value.
  • trace_file "simple0.tr"  - The file name which contains the system call trace.
  • sched_class "RR" - The scheduler being used.
  • disk_file "test_disk" - The file name which contains the simulated disk.
  • sync_disks "true" - This parameter will always be true for this lab.  When you send a memory page to the block driver, it writes it to the disk and returns.
  • mem_percentage 100 - This controls the amount of memory given to your OS (which gives it to the BufferCache).  At 100, your BufferCache gets enough memory to buffer 100% of the disk.   That means if your disk is 128KB, then if mem_percentage is 100, your buffer cache can cache 128KB.  The system determines the size of your disk by looking at the size of your disk file.  At 50, the buffer cache gets enough memory to buffer 50% of the disk (64KB in our example).  The system checks that you have at least 6 memory pages. 
    Start the lab by getting your entire system to work with mem_percentage 100.  Don't bother with page replacement at first.  When your entire system works, then you MUST test your implementation with values of mem_percentage that are low enough to cause replacement from your BufferCache.  Depending on your trace files and simulated disk size values from 10-50 will probably be appropriate to test low memory situations.

  • read_check "rc" - This is the file name for the file that stores information on the contents of data read from the disk.  The system generates this data so we can check that you read back the same data you write to your disk.  Only call ReadCheck if you have actually read data, and only for however many bytes specified by the user.  Don't call it on a read that returns 0 bytes. Your code must call the ReadCheck function.
  • datablocks_per_inode 8 - Each inode can point to this many data blocks.
  • inode_block_ratio 8 - Inodes occur in blocks 3 + N*(inode_block_ratio+1), N in 0, 1, 2, 3, ...  See the picture of the disk layout below.
  • inodes_per_block 8 - How many inodes can fit in a block.  Because we have big blocks and small inodes, we don't use math to compute this value, but provide it as a property. 
The last three options are required to make a new file system.  Once a file system is made, the value for these three options is stored in the super block.  Once a file system is made, it exists as a file and the size of the file is the size of the simulated disk.  Making these values smaller will help you test out of data and out of inode error conditions.

You can provide an argument to ProcessTrace on the command line.  That value will override the default value for the trace_file.

How The Simulator Marks Time

You don't need to know how the simulator manages its sense of time, but you might be interested.  Within the trace file, the events for a pid happen in order, but events from different pids happen in the order determined by your scheduler.  See the code for more details.

Major Classes

Classes you must write

In general, sections of code that you need to write are marked with the following comment.    // Write me

VFS (virtual file system) You will most likely spend a good deal of time in your VFS code.  The vfs layer in an operating system represents an abstract view of the file system (e.g., things like the path name separator character (/ on Unix, \ on windows) are not part of the data representation). The implementation of this class is split between the files VFS.java and VFSSync.java. You will need to modify and understand both files. This split is an unfortunate consequence of object oriented development.

BufferCache The buffer (or page) cache keeps the contents of disk blocks in memory. You can develop your code assuming you have enough memory to buffer the entire disk, but you should test where you do not have enough memory. When you don't have enough memory to buffer the whole disk, your trace might read enough blocks to require your replacement policy to flush buffered blocks back to disk. The BufferCache is a write-back cache with write allocate.   You should implement an LRU replacement algorithm for your buffer cache. You also need to make appropriate call to Stat.inc() to keep track of the number of cache read, write, hit, and replacement.The implementation of the class is split across BufferCache.java and BufferCacheSync.java.

SJF
You remember schedulers, right?  Your code comes with an implementation of RR.java.  This implementation works, and is similar to what you implemented for lab 1.  The major addition is the clear_fd routine that closes any file descriptors a process has left open when it dies.  Other differences are: don't use ProcessTrace.log, use Dbg.println, and IScheduler has a setVFS method.  Please update your SJF code from lab 1 to work with this lab.  You can use code developed by either partner, but not by someone else in the class (we will check this).  Porting the scheduler should be easy--use RR.java as a guide.  If neither you nor your partner have a working implementation of SJF this will provide a good review exercise for you.

Mkfs This is a utility that creates an empty file system on a "device" in our case, a file.

Coding guidelines

We provide you a skeleton implementation of VFS, VFSSync, BufferCache, BufferCacheSync, and Mkfs.  We provide RR.java, which should be enough for you to implement SJF.java. Please do not change the code outside of the four files you must write.  

NOTE: You only need to write the synchronous bits for this lab.  I believe we have removed the asynchronous portions.

We have eliminated about 548 lines of code from our reference implementation, so that should give you some idea of how much work you have to do.  Of course lines of code are an imperfect metric.

In order to provide return types and get the code we hand you to compile, we often insert dummy variables, e.g.,
Inode inode = null;
return inode;
You should modify this code to create an appropriate Inode.

There is no asynchronous code for you to implement in this lab.

Test Cases

Any systems builder will tell you that 95% of of the time your code doesn't work right when it is first compiled.  Getting code to compile is an accomplishment, but it is a relatively small step to a solution.  Even getting your code to process input without dying does not mean your code is correct.  The only way to make sure code is correct is to prove it (which is hard), or write a test case (which is non-exhaustive).  One lesson from this course is that in order to write code you must write tests for that code.

In this lab we are giving you three totally trivial tests.  It should be clear to you that the functionality tested by our examples is a small fraction of the functionality of the entire lab.  Getting the right answer for these test cases will probably get you between 0-10% of the credit for the lab.  In this lab you fill in a complicated system with many correctness properties, and you should test them.  We will evaluate your lab using our own test cases.  These tests are long and do many operations.  They aren't tricky, but they do require that your implementation be complete in a way that you can only verify by writing large tests yourself.  We will not read your code to evaluate it, we will only run our test cases.

Tests often fall into two categories, unit tests and system tests.  Unit tests are focused test cases designed to stress one piece of functionality, like writing to file that was opened read-only should print an error.  System tests put the system under load and make sure all requests complete correctly.  You should develop many unit tests for this lab and a few system tests.

You may share trace files on the newsgroup, and you may share md5 sums of the final disk.  YOU MAY NOT SHARE DISK IMAGES OR SIGNIFICANT PORTIONS OF DISK IMAGES.  If you have a question as to whether your post is appropriate send it to Witchel and the TAs before you post and we will judge it for you.  In addition, EACH GROUP MAY ONLY POST 3 TRACE FILES AND MD5 CHECKSUMS.  We will reduce your grade if you post more.  The idea here is that you can ask your fellow students for help, and your fellow students can also be altruistic and provide interesting test cases.  But one smart team will not produce exhaustive tests for the whole class.  You need to understand the lab to understand what a test case should do, and posting too many checksums (or the files themselves) undermines student's motivation to understand the test case
.

Classes that support the file system implementation

Dentry A directory entry.

BitMapBlock A block that holds a bit map.  The bitmap could be for free blocks for inodes.


SuperBlock The superblock holds the file system parameters.


File Your vfs maintains an array of open Files.  These should not be confused with java.io.File.


Inode The in-memory version of the on-disk data structure.

Most of these classes must be created from the data you read from disk.  Here are two examples.
new DirectoryBlock(bufferCache.readDirDataBlock(devNum).get_bytes());
new Inode(getSuperBlock(devNum), page.get_bytes(), ino);

Other classes

OS What do these things do again?  It manages the boot process, creates coordinates the other major components.

OSEvent
This encapsulates the data in a callback, primarily interrupts and system calls.  You can also use them to pass parameters.  They aren't too important in synchronous mode, but you must instantiate them (using VFS.createEvent) when you call a method that takes OSEvent as a parameter, i.e. do not pass null to it, since some other classes, e.g. BlockDriver will use it. VFS.createEvent takes a syscall number, which will be ignored, but you might as well set it to something sensible, like OSEvent.SYSCALL_WRITE in VFSSync.write.

ProcessTrace
This controls the reading of the trace file and the sequencing of events.

DiskDumper
This allows you to print out parts of your simulated disk.  The Unix command od -x is also useful for this.

Dbg
This is a utility class that allows flexible printing of messages to the console to aid your debugging.  It is initialized in OS.OS.

See the code for more documentation and ideas.

Disk Format

Here is a picture of the disk layout you will use.  Block 0 is the superblock, block 1 is the used block bitmap, and block 2 is the inode bitmap.  The format and use of these blocks is described below.  Block 3 is an inode block that contains the inode for the directory (there is only a single directory for the file system).  Because inode blocks can contain multiple inodes, this block might contain inodes used for something other than the directory.  Block 4 is just a data block, but is used as the data block for the directory.  The directory only has a single data block.  While it might seem  odd to specify the directory's data block, you will find that this simplifies your life when the disk interface becomes asynchronous.

Block 3 is an inode block, and there are data blocks until the (3+InodeBlockRatio+1)th block (block 12 by default (which is the 13th block)) which is an inode.  After that inode, we have data blocks until the (3+2*(InodeBlockRatio+1)+1)th block, which is an inode (block number 21), etc.




Inodes only have direct pointers. The dataBlocksPerInode member of Superblock denotes this value. The maximum file size will be Disk.BYTES_PER_BLOCK * datablocks_per_inode

Superblock

The superblock has information about the file system, like the number of inodes per block, the inode_block_ratio, and the datablocks_per_inode, a pointer to the directory inode (its inumber), and the total number of blocks in the file system.

Usedblock Bitmap

The usedblock bitmap block stores a bitmap where each bit represents the state of each data block on the disk.  If the bit is 1, that block is used.  In this case, used means it is not available for allocation as a data block.  Therefore, all blocks which can be used for inodes are marked used.  That means mark all inode blocks as used in the
usedBlockBitmap in order not to allocate a data block there

Inode Bitmap

The inode bitmap block stores a bitmap where bit i represents whether inode i is in use.  Each inode block contains InodePerBlock inodes.  If InodePerBlock is 4, then inodes 0-3 are stored in block 3 in the above picture.

Directory Entries

Your directory entries are 36 bytes long.  They consist of a 32-byte fixed size name, and a 4-byte inode number that is the inumber for the file's inode.

Program Output

Your program generates three kinds of output.
  1. Final state of the simulated disk file.
  2. The read check file.
  3. Errors and statistics that are printed to stdout.
Your goal is to create a correct implementation that performs as well as possible.  Correctness is judged by comparing the final state of the simulated disk file and the read check file to one generated by our reference implementation, and by matching error messages.   Statistics do NOT have to match for correctness.

To check your answers, you can use the program md5sum. 
Running md5sum -b <filename> on a Unix machine will generate a 128-bit hash (represented by ascii text).  It is VERY unlikely that two different files have the same md5 hash.

Callbacks

This lab will not use callbacks, because your code will assume that disks respond synchronously, i.e., you wait for them.  This allows the code you write to be simple, e.g.,
void process_superblock_sync() {
   read_superblock();
   update_superblock();
   write_superblock();
}
While simple, this would be a terrible thing to do with a real disk.  Disks take a very long time to respond relative to instruction execution, so systems overlap the disk request of one process with computation from another.  Many systems (like Linux and Windows) have kernel threads which call the scheduler after making a disk request.  The scheduler suspends the process, and preserves the stack state of the process, usually using assembly language to switch stack contexts to the next kernel thread that must run.  In these systems, most of the state for the current system call is stored on the stack (with some information stored in the process table, the process' PCB, etc.).

The asynchronous disk part of the lab will use callbacks, but that is not part of this assignment.
 

Hints

If you crash in the middle of a disk operation, you can leave the disk in an inconsistent state.  You might need to recreate the disk from scratch if you crash using Mkfs.

When the simulator starts (known as booting up in deference to the expression to pull oneself up by one's bootstraps), the superblock, 2 bitmap blocks, directory inode block, directory data block (i.e. block 0 ~ 4) will be loaded into the BufferCache. In the buffer cache, pin bit is set to true. It means you should not remove these 5 blocks from the buffer cache. They always reside in memory, allowing you to know that reading these blocks always hits the cache. See VFS.startup() for the details, though this is more important for the asynchronous case.

Pages, sectors and blocks are all 4KB.

If one process opens a file, no other process may use that file descriptor. You should check and disallow it. Try to take advantage of SuperBlock.getBytes(), BitMapBlock.getBytes(), DirectoryBlock.getBytes(), Inode.getBytes() MemPage.get_pages() when you create an array of byte.

The interface for the BufferCache is narrow.  The buffer cache reads disk blocks into pages that it manages.  If the VFS reads a newly allocated block, it could conceivably tell the BufferCache not to bother reading the disk block because the contents are totally new.  This would complicated the interface, so we don't do it.  If you read a block that has never been allocated, you still read the block from disk.

In the code you will see c_style_identifiers, and javaStyleIdentifiers.  Sometimes I feel like a nut, sometimes I don't.

Extra Credit

If you implement any of these you will receive extra credit.  The more you implement, the more credit you get.
  1. Add an indirect block to your inode.
  2. Implement unlink.
  3. Add file permissions.
  4. Write fsck.
  5. Allow multiple directories, arranged hierarchically.
If you do the extra credit, you need to submit at least 4 additional test cases (for each feature) that test your functionality, called extra00.tr, extra01.tr,...,extra10.tr, extra11.tr, etc (the first number is the Xth extra feature, the second number goes from 0 up to 9).  Also include a file called EXTRA.txt which has any notes or assumptions you made in your implementation.

What to Turn In

Please turn in a single jar file which has Mkfs.java, VFS.java, VFSSync.java, BufferCache.java, BufferCacheSync.java, and SJF.java along with your README, ANSWERS.txt (see below), and your test cases.  We will unpack your jar file, and use the ORIGINAL source files you were given for the rest of the project.  ANY MODIFICATIONS you made to files besides the ones you are supposed to turn in WILL BE LOST when we test your program, so don't change these files. We will run your lab like this:
java ProcessTrace

You are required to turn in 3 test cases,  in files named test0.tr, test1.tr, test2.tr (you may include additional files that conform to this naming scheme).  test0 should create lots of small files, test1 should create at least one large file.  test2 (and subsequent tests) I leave to your creativity.

Don't forget to test your code under low memory situations (mem_percentage is 20 or so depending on your trace and the size of your simulated disk).

Please include a file called AUTHORS.txt which includes a sequence of lines.  Each line has a name, followed by a colon (:) followed by your eid.

Lab Questions

Please put these questions and your answers in a file called ANSWERS.txt.  Answer these questions for the default values of system.properties.
  1. What is the maximum file size?
  2. What is the maximum number of files you can create, assuming each file has a unique name created from all lowercase letters (26 of them)?
  3. Why is the default value of inodes_per_block equal to the default vale for inode_block_ratio?

Other Details and Submission Instructions

  • Each student must do the assignment independently, or work as a pair in the pair programming style. In pair programming, two students must sit at the same computer and both must participate actively. We require each student who is in a pair programming group to spend at least 80% of the total development time participating actively, and at least 40% of that time being the one typing on the keyboard. (This style does not mean two students can split up the program, work separately, and then reconvene to put the solution together.)

    If you pair program, follow these additional guidelines.

    • Only one person turns in the assignment.

    • Your README file also contains, the name of both pairs.

    • See the Pair Programming web page for how to do pair programming and the benefits of it.

    In your README file, include the following:

      Name
      Slip days used (this project): _______ Slip days used (total): ______

      On my honor, name, this programming assignment is my own work.

      ...if applicable...
      Pair Name
      Slip days used (this project): _______ Slip days used (total): ______

      On our honor, name and name, this programming assignment is our own
      work. We spent 80% of our time on this project together, we split the
      keyboard time evenly, and we both participated equally in the solution
      design.
      ........


      Total hours spent on this project:
  • Please turn in your project using the following command (in one line):
    /lusr/bin/turnin --submit naga86 lab4 FileSystem.jar