From CS61
Jump to: navigation, search
Computer Science 61 and E61
Systems Programming and Machine Organization

Assignment 2: Caching and File I/O

In this assignment, you’ll gain experience with caching by writing your own buffered I/O library.

  • Assigned Wed 9/24
  • Intermediate checkin due Fri 10/3 at 11:59pm for college students (1 day later for extension); see Turnin below
  • Due Fri 10/10 at 11:59pm for college students (1 day later for extension)
  • This assignment may be completed in pairs

Get the code

Our code is released in the cs61-psets repository. To update it, run

git remote show handout

If this reports an error, run

git remote add handout git://code.seas.harvard.edu/cs61/cs61-psets.git

Then run git pull handout master. This will merge our Assignment 2 code with your previous work. If you have any “conflicts” from Assignment 1, resolve them before continuing further. Run git push to save this merge back to code.seas.

Use an explicit merge. If you copy code by hand, our automated scripts will have trouble analyzing your code, and it’ll be harder for you to incorporate our updates.

You may also create a new cs61-psets repository for this assignment. Make sure you update the grading server.


We’ve released a very simple IO61 I/O library, in io61.c. Our version of IO61 uses character-at-a-time system call I/O and is thus quite slow. Your goal is simple: speed it up using caching.

We also distribute several programs that use IO61.

  • cat61 reads a file sequentially one character at a time and writes it to standard output.
  • blockcat61 is like cat61, but reads the file sequentially one block at a time. Its -b BLOCKSIZE argument sets the block size.
  • randblockcat61 is like blockcat61, but it chooses a random block size for each read and write. It still reads and writes sequentially (only the block sizes are random). The -b argument is the maximum possible block size. This will test your buffering package.
  • reverse61 reads a file a character at a time in reverse and writes the result to standard output.
  • stride61 reads its input file using a strided access pattern, and outputs a transformed file. The -b and -s arguments set the block size and stride length, respectively.
  • reordercat61 reads its input file one block at a time in random order and writes each block to the output file at the correct position. The result is a copy of the input file, but created non-sequentially.
  • gather61 can take two or more input files. It reads blocks from them in round-robin order to create its single output file. This will test your ability to handle multiple input files.
  • scatter61 can take two or more output files. It reads blocks from a single input file, then writes them to the output files in round-robin order. This will test your ability to handle multiple output files.

Introduce buffering (that is, caching) to the io61_file abstraction and use them to speed up IO61 operations.

We’re giving you tons of freedom to implement the file caches as you like. You may even use memory-mapped I/O, prefetching system calls like madvise or posix_fadvise, or multiple threads or processes (although none of these are required). But you may not use another buffered I/O library or caching I/O library.

Your library should:

  • Return correct results for all access patterns and file types, including multiple files, very large files, non-seekable files, mixtures of calls (for instance, alternating io61_read and io61_readc), and random accesses.
  • Perform as well as stdio on sequential reads and writes for any files, including very large files (such as files that don’t fit in memory) and non-seekable files.
  • Improve on stdio’s performance for non-sequential access patterns, such as simple strides or reverse access.

We will grade your work based on how much you speed up sample executions of the handout programs—and other secret tests—with points deducted for incorrect results.

All your code should fit in io61.c.


We will evaluate you based on your code’s performance relative to a version of IO61 that uses stdio. That version is provided for you for testing in stdio-io61.c (and the makefile builds stdio-cat61, stdio-blockcat61, and so forth).

Run make check to see how your current implementation is doing on a battery of tests. This will also print summary statistics at the end. Run make check-TESTNUMBERS (e.g., make check-9 or make check-5-10 or make check-1,2,3) to run selected tests.

We will also measure your code’s performance against that of other students in class. :)

We may update the tests during the pset release period.


It’s easy in this problem set to design something too complex and get yourself stuck. Avoid that problem by tackling the simple cases first. Here’s a possible roadmap.

  • First, you can improve the way io61_read and io61_write work without introducing a cache. Use strace to investigate the operation of blockcat61. Look at the blockcat61 code; what system calls do you think it should make? Then run strace -o strace.out ./blockcat61 blockcat61.c. (That command line tells strace to write its output to strace.out, so examine strace.out with a command like less strace.out.) What system calls does blockcat61 actually make?
    • This will improve your performance on the block I/O tests (e.g. make check-4-6).
  • Then, implement a single-slot cache buffer for reading. This will hold bytes [N, N+B) of the file, where B is some largish number (try different numbers). Any read request that lies within that range of bytes can be satisfied without making a system call. In the book’s terminology, this is a single-slot cache. Check your work by examining strace output. Don’t worry about seeks yet. (This means your code will stop working on the “reverse order,” “stride order,” and “random seek order” tests.)
    • This will improve your performance on character I/O tests.
  • Then, implement a single-slot cache buffer for writing. This is like the cache buffer for reading, but it absorbs writes instead of prefetching reads.
    • This will improve your performance on character I/O tests. If you do this well, you should now match or beat stdio’s performance on all sequential I/O tests!
  • Then, fix your code to handle seeks correctly (but not necessarily in a high-performance way). This is actually pretty easy.
    • This should make your code produce correct results for all tests.
  • Then, fix your code to handle seeks correctly, but with good performance for reverse61. For ideas, try running strace on the stdio-reverse61 variant. What does it look like stdio is doing?
    • This should make your code roughly equal stdio’s performance on all tests.
  • Then, change your code to use memory-mapped I/O when this is possible. Memory-mapped I/O is not always possible, though, so keep your old block cache around for pipe files and other non-mappable files.
    • This should make your code beat stdio on some non-sequential I/O tests!
  • Then, if you have additional time, implement an associative cache that works for non-mappable files or for very large memory-mapped files.
    • Something like this will get the highest performance.
    • It is difficult and rewarding to implement an associative cache. You’ll learn a lot! But don’t encrapulate your other code. For instance, you might try working on a separate branch at least at first.


  • Read our file descriptors notes.
  • Write code to handle sequential I/O first. This isn’t so hard and covers many important cases.
  • Some of our tests, particularly on shorter files, are noisy, and can produce different timing results when run multiple times.
  • You will likely need to change most of the functions in io61.c.
  • To handle reordered I/O and particularly stride I/O, you will need to be clever and implement more complex cache algorithms.
  • Stdio is a well-written package! It is OK if you can’t always beat it.
  • Write your own tests! This will help you shake out bugs in your code, particularly correctness bugs. What diabolical things can you think of to try? You can add new tests pretty easily; just edit GNUmakefile and check.pl.
  • You can run the tests on their own, too. make check creates some useful input files for testing and puts them in files/.

You are allowed to make a couple assumptions.

  • You may assume that any file descriptor passed to io61_fdopen has its file pointer set to 0 (the beginning of the file).
  • You don’t need to set the file pointer after every call to io61_seek.


If you want to compile with gcc, rather than clang, run make PREFER_GCC=1. If you want to compile without optimization (which might help you debug), run make O=0 or edit the GNUmakefile to set O = 0 by default. The makefiles will remember your setting for PREFER_GCC until you run make clean.

Extra credit: Deadlock

We also release a program, pipeexchange61, that demonstrates a problem with conventional blocking I/O. The pipeexchange61 program forks two copies of itself. One copy, the requester, sends requests to the other copy, the responder. The requester can send many requests back-to-back and only then wait for responses (a form of batching). When you run pipeexchange61, the program appears to get stuck! Why? Can you construct an I/O library that unsticks pipeexchange61, without modifying pipeexchange61.c?


You will turn in your code by pushing your git repository to code.seas.harvard.edu. Inform us on the grading server if you have changed partner or repository from pset 1.

Don’t forget to fill out README.txt.

We are requiring an intermediate checkin for this problem set. Your grade on the intermediate checkin will be replaced with your final grade on the pset. The intermediate checkin is due 10/3 at midnight for college students, one day later for extension. By 10/3, you should roughly equal stdio’s performance on all sequential I/O tests. (It’s OK for your code to be broken on non-sequential tests.)

This pset was originally created for CS61.