CS 537 Spring 2007
File Systems, Part I
Disk Scheduling and Buffer Cache

due date: 1:00 am, Wednesday, April 18, 2007.

Notes

Watch this space for announcements. See also the FAQ (Frequently Asked Questions) page.

April 16, 2007
There was a sentence in the What to Hand In section that seemed to imply you needed to write your own test program. It has been corrected.
April 15, 2007
The first sentence of the Buffer Cache section incorrectly said Kernel.bufferSize, when it should have said Kernel.cacheSize. This sentence has been corrected.

Introduction

Assignments 4 and 5 will use a common software structure called MiniKernel, which simulates a simple operating system. In this assignment, you will manage multiple programs accessing a disk and improve their performance by adding a disk buffer cache. In the next assignment, you will build a file system.

Frequently Asked Questions

There is a FAQ page for this project. Check it frequently for updates.

Getting Started

First, read and understand the MiniKernel documentation. Copy the code from ~cs537-1/project4 and run the examples.

    cd ~/private
    mkdir p4
    cd p4
    cp ~cs537-1/public/project4/* .
    make
To make sure you understand how all the pieces fit together, implement getTime() as described in the documentation.

Adding System Calls

Disks come in lots of different shapes and sizes, so we'll also need some facility for the user program to inquire the geometry of the disk. Add two system calls that report the number of blocks on the disk and the number of bytes in a disk block. The library interface to the system calls should look like this:

Add two more system calls that allow reading and writing of individual blocks.

The data array must be allocated by the caller and it must be at least BLOCK_SIZE bytes long, where BLOCK_SIZE is the value returned by getDiskBlockSize.

These systems calls are blocking. They return 0 on success and a non-zero value to indicate an error. The calling process is blocked until the operation completes. However the Disk has only non-blocking methods beginRead and beginWrite, so you will need to write a monitor to schedule requests to the disk. Call this monitor DiskQueue.java. At first, make this class very simple: It has three methods read, write and endIO. The read method waits until the disk is idle, records information about the current disk operation, calls Disk.beginRead, and waits for the disk operation to complete. The write method is similar. The endIO method is called by Kernel.interrupt() when it gets an INTERRUPT_DISK interrupt indicating that the current disk operation has completed and uses notifyAll to tell any waiting threads that they should re-check to see if it is time for them to do something.

You will need to modify the Kernel to create a DiskQueue instance in Kernel.doPowerOn and to call its read, write, and endIO methods at the appropriate points.

Disk Scheduling

Modify your DiskQueue class to implement the (2-way) Elevator scheduling algorithm. If the read or write method finds the disk busy, it should enter its request in a list of pending operations and wait until it is chosen. The endIO method should notify the thread that started the I/O operation, and then, if there are other requests in the queue, choose one and allow it to make another beginRead or beginWrite call. The request “list” can be any data structure you think is appropriate. Each time an I/O operation finishes, endIO needs to choose the request closest to current head position in its current direction of travel. The distance is simply the difference between the block number of the request and the block number most recently read or written. Ties may be broken any way you like. Assume the head starts out at block zero. Don't get too fancy. The queue should never be very long, so the cost of searching is unlikely to be important.

Add a method DiskQueue.shutdown() that delays the caller until all pending disk operations have completed. Add a call to this method to Kernel.doShutdown() just before the call to disk.flush().

Buffer Cache

Programs tend to request the same blocks of a disk over and over again. The kernel can help speed things up by making a buffer cache of Kernel.cacheSize disk blocks. Create a class DiskCache that maintains an array of buffers each of which has space to store the contents of a disk block as well as information about the block, such as its location on disk, whether it is “dirty.” Define methods DiskCache.read and DiskCache.write and have the Kernel call these methods instead of DiskQueue.read and DiskQueue.write.

Use the Clock (Second Chance) algorithm to allocate buffers in the cache. For each block in the cache, you will need a byte[] array to hold its data, the block number of the corresponding block on disk, an indication whether the buffer is “clean”, “dirty”, or “empty,” and a “ref” flag. Whenever read or write is called and the requested block is already in the cache, set the ref flag to true and copy the data to or from the cached array of bytes (you may find System.arraycopy handy for this). If an operation requests a block that is not in the cache, use the Clock algorithm to choose a buffer from the cache and use it instead. Look at each buffer in the cache, starting wherever you left off last time. If the buffer is clean but the ref flag is true, set it to false and go on to the next buffer. If it is dirty, schedule a write operation to write its contents out to disk. If it is both clean and unreferenced, use it. If the operation that requested the block was a read operation, you have to read the block from disk before returning.

Your DiskCache class also needs a method shutdown() that writes all dirty blocks back to disk. Add a call to this method to Kernel.doShutdown(). Note that this method doesn't have to be very fancy. Because it is called only at system shutdown, it doesn't have to be very efficient, and it doesn't have to worry about new requests arriving. When the MiniKernel shuts down cleanly it leaves behind a Unix file called DISK that contains the contents of the whole simulated disk. The next time you run MiniKernel, it will read this file to restore the contents of the simulated disk. If there is no DISK file, the simulated disk will start out with random data.

Errors

“User” programs have a habit of misbehaving. The Library and the Kernel should be vigilant in detecting invalid operations and returning an appropriate error value. You may define new error values if you like. For debugging purposes, you can use System.err.println or System.err.printf to print error messages, but you should be aware that the only way a “real” operating system kernel indicates errors is by returning error result codes from system calls (unless you count the blue screen of death as “indicating errors”:-).

Be very careful about race conditions and deadlocks. Use all the skills you learned from project 2. The synchronization for this project is surprisingly hard to get right. See the Hints section below and the FAQ for more advice.

Testing

We have supplied a little test program DiskTester.java that exercises the disk. It is invoked as

    DiskTester [options] id
where id is a positive integer. If you run it without any arguments, it will print a usage message listing all the available options. To compile it with make, remove the ‘#’ symbol from the second line of the Makefile. Note that you will not be able to compile it until you have added the methods getTime(), writeDiskBlock(), etc. to Library.java.

The disk starts out loaded with random (or at least unpredictable) data. You can use the command DiskTester -i 123 to initialize the contents of each block with a header that includes the id 123, the block number, and the number zero. If you include the -V option, the tester will verify each read to make sure the block has a header with the right block number.

If you specificy a block count with -n count, the tester will access count random blocks. If you specify -r each access will read the block, show the header (if you specified -v) and verify that it contains the right block number (if you specified -V). If you specify -w, each access will modify the header of the chosen block to include the id of this disk tester, the block number, and a sequence number. If you do not specify either -r or -w, each access will be a read or write with equal probability.

By default, random accesses are uniform: Each access may choose any block on the disk with equal probability. If you specify -c, 10% of the blocks on the disk are considered “hot.” Each read or write will select a “hot” block with 90% proability and a “cold” block with 10% probablity. The particular blocks considered hot depend on the last digit of the id specified on the command line. If the id ends with 0, the first 10% of the blocks on disk are “hot”. If it ends with 1, the next 10% are “hot,” and so on.

Hints

Grading

Your grade will be 70% for correctness, 20% for completeness of testing, and 10% for style. Don't forget the following:

What to Hand In

Place in the handin directory of the “senior” member of your team (the one whose login name comes first in alphabetical order): You may add new classes as you see fit. Do not modify Disk.java. As written, the Kernel automatically starts the program Shell. Do not change this feature. We will run your Kernel with our own test program and then examine your test program to verify the results.
Last modified: Mon Apr 16 05:57:33 CDT 2007

Copyright © 1996-2007 by Marvin Solomon.