CMSC 412 Project 5
GOSFS Filesystem
Due Friday, December 13, 11:59pm
Notes and Updates
Previous versions of this project have had intermediate
deadlines. We're using a single deadline at the end,
but there is a lot of code to write. Please start early.
- 22 November: Added a few things to the specification for
Stat() and FStat().
- 4 December: Fixed an error in the description of Mount():
it had talked about the block size being wrong, but the block size isn't specified in the
GOSFS superblock.
-
5 December: Removed the requirement that Create_Directory recursively create
directories. (See Piazza post @165.)
1. Introduction
GeekOS currently has a filesystem of type PFAT
residing in IDE disk 0 (disk image diskc.img).
PFAT is extremely limited: all files are stored
in a single directory, file names are limited to twelve characters,
and it does not allow writing to disk.
The goal of this project is to develop a new filesystem type,
called GOSFS,
and to create an instance of it in IDE disk 1 (disk image diskd.img).
User programs will remain on the original PFAT filesystem while you
develop your GOSFS filesystem implementation.
Here, we use the term filesystem type to mean a set of rules
for laying out directories and files in a disk
and for adding, modifying and deleting them.
We use the term filesystem to mean an instance
of a filesystem type.
For example,
PFAT is a filesystem type
(whose rules are implemented in pfat.c);
diskc.img is a PFAT filesystem.
GOSFS is another filesystem type
(whose rules you will implement in gosfs.c);
your diskd.img will be a GOSFS filesystem.
References in the text
(depending on your edition,
chapter numbers may differ):
File-system interface (chapter 11),
File-system implementation (chapter 12).
2. Context: VFS and blockdev
GeekOS has a virtual filesystem (VFS) that acts as a wrapper for
different types of files. VFS provides a uniform set of system calls that
abstract away the details of the filesystem's implementation. It is not
limited to files on disk: you used VFS at the beginning of the semester to
provide access to pipes.
Initially VFS consists of just an empty root directory ("/").
After the OS has been initialized,
the PFAT filesystem in IDE 0 is mounted onto VFS at position
"/c",
after which user programs can access the PFAT filesystem
as the VFS subdirectory "/c".
Similarly,
your GOSFS filesystem can be mounted at another point
(say "/d") and accessed.
In addition to VFS, GeekOS has a virtual block device into which
any block-structured storage device (e.g., IDE, floppy) can be
registered.
You used the block device layer to access your paging file in Project 4,
and you will use it here to read and write the actual data in your filesystem.
The block device acts as a wrapper to the registered storage devices,
allowing them to be accessed in a uniform manner.
In GeekOS, only the kernel has direct access to block devices.
The figure below illustrates the context.
User programs invoke system calls,
which invoke functions in vfs.c,
which invoke functions in pfat.c or gosfs.c,
which invoke functions in blockdev.c,
which in turn invoke functions in ide.c.
You have to implement the functions in in gosfs.c and the
entry points to the system calls in syscall.c.
The other layers are already implemented.
You will need to understand how vfs.c and pfat.c work
and how to use blockdev.c.
VFS uses a set of common data structures to provide uniform access across
the filesystem types below it. Each user context has an array of ten File
structures. The user refers to files by the index in the array; this index
is called a file descriptor. Each element of the array is a pointer
to a struct File. You used file descriptors and the File struct to
refer to pipes in Project 0. As you may recall, the File struct contains
some generic information such as the current position in the file, a pointer
to a group of operations (function pointers) that VFS will use to act on the
file, and a pointer called fsData that is available for the
filesystem's use. The file operations and fsData pointer are what
VFS uses to invoke filesystem-specific code. When you open a file, you will
set the file operations to point to GOSFS functions. You will also create a
struct with whatever information you need to manage a GOSFS file,
and use the fsData pointer to associate your data with each File
struct. Again, this is how you implemented pipes in Project 0.
The File struct lets you perform operations such as reads and writes on files
that are already open. However, you need something more to create and
open files, and to support other filesystem actions that are not assocated
with a single file. VFS uses struct Mount_Point for this purpose.
Each mounted filesystem has an associated Mount_Point struct. Like struct
File, it has some generic data such as the prefix where the disk is
mounted and the associated block device, a pointer to a group of filesystem
operations, and a pointer to filesystem-specific data. You will again define
the struct that holds whatever data you need to manage a GOSFS filesystem.
VFS also provides some utility functions to the filesystem types below it.
For instance, it provides functions that register a filesystem type
(Register_Filesystem) and that allocate a File struct
(Allocate_File). You may use these VFS functions as you wish. The
baseline version of main.c includes a call to Init_GOSFS(),
which uses Register_Filesystem to tell VFS how to format or mount a
GOSFS filesystem.
By convention, the pfat functions that are called by vfs have names
of the form PFAT_<function>
(see pfat.c).
Similarly, the baseline GeekOS distribution has GOSFS functions with
names of the form GOSFS_<function> (see gosfs.c).
Note, however, that these names have no special significance to VFS. It
accesses the functions through the function pointers described above.
These function pointers are set at run time
(when a filesystem type is registered,
when a filesystem is mounted, and when a file is opened).
3. GOSFS
GOSFS provides a much richer interface than PFAT.
The directories of a GOSFS filesystem form a tree.
A directory can hold up to 36 entries, each of which may
represent a file or a directory.
A directory entry has a name
of up to 64 characters (bytes),
including the null at the end.
A full path to a file is at most 1024 characters.
Directories and files can be created and deleted.
Existing files can opened for reading and writing.
A raw disk can be formatted to hold a GOSFS filesystem.
(In contrast, PFAT is read-only, has only one directory,
its file names are at most 12 characters,
and there is no format capability.)
Internally, GOSFS uses a block size of 4KB.
Recall that an IDE disk in GeekOS has a block (or sector) size of 512 bytes.
Thus in an IDE disk with a GOSFS filesystem,
each GOSFS block is stored in 8 successive disk blocks.
Gosfs block 0 (stored in disk blocks 0-7)
is called the superblock;
it holds info about this particular filesystem, such as the
disk size, locations of free blocks, and the root directory's location.
Gosfs blocks 1 and higher contain files and directories or are free.
You must keep track of free GOSFS blocks using a bitvector.
A module called bitset is provided
(see bitset.h and bitset.c) that manages a bitvector
and find bits that are 0 (i.e. corresponding to free GOSFS blocks).
Note that one bit in the bitvector corresponds to a GOSFS (4KB) block
(i.e., 8 disk blocks).
So a bitvector of is 8192 bits (1024 bytes) can keep track
of a disk of size 8192 * 4KB = 32 MB.
3.1 Caching
When a file is read, the data from the disk is stored
in a cache. If the user reads the same blocks again, the data
may be returned from the cache without waiting for the disk again. Likewise,
when data is written to a file, it is first saved in the cache. The write can
then return without forcing the caller to wait for the disk, and the data can
be read back from the cache before it is written to disk.
(Of course, the
data must eventually be written from the cache to disk. This may happen when the
file is closed or when a later operation needs space in the cache,
but in any case, the Sync system
call makes sure that all cached changes are written to disk.)
You must implement block-level
caching in GOSFS. The automated tests will not detect whether or not you
are caching blocks, but we will check your code by hand to make sure.
Most of the work has already been done in a module called
bufcache.c, which we suggest you use. If you use some other approach
for caching, please email Jon to let him know and make sure your solution
meets the requirements.
3.2. GOSFS functions
Here is a quick summary of the functions you must implement in gosfs.c.
Except for Init_GOSFS, all the functions below are called by VFS.
Note that there are some functions in gosfs.c that relate to access control and file
ownership. You may ignore those.
- Init_GOSFS()
-
Register the GOSFS filesystem type with vfs.
(Sets pointers in vfs to some GOSFS_ functions.)
- GOSFS_Mount(...)
- Mount a GOSFS filesystem.
The call identifies a block device
(expected to have a GOSFS filesystem)
and the place in vfs where it is to be mounted
(for instance, "/d").
- GOSFS_Format(...)
-
Format a block device with an empty GOSFS filesystem,
i.e., creates superblock and empty root directory.
The call identifies the block device
(which should have been previously registered with blockdev).
- GOSFS_Open(...)
-
Returns a File struct corresponding
to a file in a mounted GOSFS filesystem.
The call identifies the mounted filesystem
and the path to the file in the filesystem.
The file is now open:
it can be read and written.
This call is also used to create a file.
- GOSFS_Open_Directory(...)
-
Return a File struct for an existing directory.
(Note that, in contrast to Open, this function will not create a new directory.)
The directory will be closed with GOSFS_Close.
- GOSFS_Create_Directory(...)
-
Create a new directory and return a File struct for it.
GOSFS_Close(...)
Close an open file.
GOSFS_Delete(...)
Delete a file (mark its directory entry unused and mark its blocks as free).
GOSFS_Read(...)
Read a specified part of an open file.
GOSFS_Read_Entry(...)
Read an entry from a directory.
GOSFS_Write(...)
Write a specified part of an open file.
GOSFS_FStat(...) and GOSFS_Stat(...)
Retrieve information about a file.
GOSFS_Seek(...)
Set the position within a file.
GOSFS_Sync(...)
Write all buffered changes to a file onto the disk.
3.3. Filenodes and Directory structure
The internal structure of a GOSFS directory is defined in gosfs.h.
Each directory in GOSFS takes up a single GOSFS block.
It is an array of 36 GOSFS filenodes (GOSFSfileNode),
one for every possible entry (file or subdirectory).
(The limit of 36 ensures that the array fits in a single 4KB block.)
A filenode has the following fields:
name of the entry;
size of the entry's data;
whether the entry is active;
whether the entry is a directory;
and an array of pointers to GOSFS blocks containing the entry's data.
Each "pointer" is simply a 4-byte block number.
(There are also some fields (isSetUid, acls) not relevant for project 5.)
The array of pointers is called blocks.
It is used as follows.
-
For a filenode of a directory,
only block[0] is used
(because the data of the directory fits in one GOSFS block).
-
For a filenode of a file,
the number of blocks entries used depends on the size of the file.
A GOSFS block that holds file data is referred to as a direct block.
You will use indexed allocation to track
the direct blocks of the file.
-
Entries 0 through 7 point to eight direct blocks,
holding the first 8*4KB of data of the file.
-
Entry 8 points to a so-called single-indirect block,
which is a GOSFS block containing pointers to direct blocks. Since each
pointer occupies 4 bytes, a 4KB block can hold up to 1024 pointers.
Thus, one single-indirect block can support file sizes up to 1024 * 4KB = 4MB
(plus the 32KB supported by direct blocks).
-
Entry 9 points to a so-called double-indirect block,
which is a GOSFS block containing pointers to single-indirect blocks. One
double-indirect block can support file sizes up to 4GB. As you will see below,
however, GOSFS disks are restricted to about 128MB.
The superblock and root directory have no associated GOSFSfileNode.
Every other directory and every file has an associated GOSFSfileNode in its
parent directory.
3.4. Disk Layout
Superblock (4 KB) |
Rest of the Disk (32 MB – 4 KB) |
Magic (4 bytes) |
Root Dir Ptr (4 bytes) |
Disk Size (4 bytes) |
Free Block Bitmap (1024 bytes) |
Unused (3060 bytes) |
Data (unallocated blocks, indirect blocks, and blocks allocated to files and directories) |
An example disk layout is provided above. The diagram shows a 32 MB disk,
but GOSFS disks may be larger or smaller than this size.
Gosfs block 0 is called SUPERBLOCK,
and contains filesystem housekeeping data.
Gosfs blocks ≥ 1 may be:
- direct blocks that contain file or directory data,
- indirect blocks that contain pointers to other blocks, or
- empty.
The superblock is structured as follows:
-
The magic number at the very beginning of the superblock should be 0xbeebee
to indicate that the disk contains a GOSFS filesystem.
If the GOSFS mount function is given a drive that doesn't have the correct
magic number at the beginning of block 0, it should return an error.
-
Root Dir Pointer holds the number of the GOSFS block
containing the root directory.
-
Size is the size of the disk in 4KB blocks.
32M / 4K = 8192 blocks for the example above.
-
Free Blocks Bitmap is Size bits large,
that is, Size/8 bytes large.
(8192 / 8 = 1024 bytes for the example above.)
Every GOSFS block has an associated bit, including the superblock and the
block that contains the root directory. Note that the size of a GOSFS disk
is limited by the number of bits available in the superblock.
When you do a Format(), you make a raw disk usable with GOSFS. That is:
-
Get the drive's size; convert it to number of GOSFS blocks.
To find the size, you can use the function Get_Num_Blocks(dev) from
blockdev.c.
-
Create a GOSFS block containing a valid but empty directory.
That will be the root directory. Make Root Dir Pointer in the superblock
point to it.
-
Set up the bitmap of free blocks.
Mark the superblock and the root directory block as used
in the bitmap. All other blocks should be marked free.
-
Add the magic number to the superblock.
-
Write the superblock and root directory block to the disk.
Keep in mind that the superblock and root directory
have no associated GOSFSfileNode.
4. VFS System Calls
Here are the system calls concerning the virtual filesystem (VFS).
As you see,
the semantics of these calls is very similar to that in UNIX.
Keep in mind that each call vectors through the VFS layer
before invoking a corresponding GOSFS_ or PFAT_ function.
You have to implement the initial system call code (i.e., Sys_*)
and the GOSFS_ functions.
The PFAT_ functions are already implemented in pfat.c. It may be useful
to look at PFAT as you implement GOSFS.
Several of the system calls use structures that are defined in fileio.h
and vfs.h, both in include/geekos.
There are several errors that can arise in the system calls below. This is a
list (not exhaustive) of these common error conditions. They are listed here to
avoid repetition in the descriptions of the system calls.
-
In a path specification, the prefix or some part of the directory chain does not exist.
For instance, /d/d1/d2/d3/file.txt where there is no d2 in
d1, or where there is no filesystem mounted on /d.
-
In a path specification, some part of the directory chain is actually a regular
file. For instance, /d/d1/oops/file.txt where oops is a
regular file in d1.
-
A function tries to return a new file descriptor, but all descriptors are
already in use.
-
A function takes a file descriptor as an argument, but (1) the descriptor
passed in does not reference an open file, or (2) the descriptor is out of
range (not in the range 0-9).
-
A disk read or write fails (Block_Read or Block_Write returns an
error code).
int Mount(char *dev, char *prefix, char *fstype)
-
Entry point in syscall.c: Sys_Mount
-
Parameters:
- dev: the name of the device, such as ide1
- prefix: the VFS prefix, such as d (note that the leading
slash is omitted from the call)
- fstype: the name of the FS type; for GOSFS this is gosfs
-
Return 0 on success, or an error code on failure:
-
a filesystem is already mounted under prefix.
-
a parameter in the superblock has an illegal value (in GOSFS, this means that
the magic number is wrong; there are other errors possible, such as the root
directory pointer being zero, but we will not test these).
-
Typically, the user-side code for a system call calls strlen() on each
string argument, and passes the string pointer and the string length as two separate
arguments to the kernel code. (Recall that the "arguments" to the kernel code are
stored in the x86 general-purpose registers ebx, ecx, edx, edi, and esi; eax is
used to hold the system call number.) With three string arguments, this approach
would require six separate arguments, but we only have five registers available.
Therefore, the user code for Mount() stores the three strings in a
struct VFS_Mount_Request (see fileio.h), and the only argument
to the kernel code is a pointer to the struct.
|
int Open(char *path, int permissions)
-
Entry point in syscall.c: Sys_Open
-
Parameters:
- path: the full path name of the file to open, such as /d/foo/bar/baz.txt
- permissions: an integer assembled by OR'ing together the flags O_CREATE, O_READ, and O_WRITE, defined in fileio.h
-
Return a newly assigned file descriptor (from 0 to 9) on success, or an error code on failure (see common errors):
-
the final name in the path does not exist, and permissions do not include O_CREATE
-
the final name in the path is a directory (use OpenDirectory instead)
-
Comments
-
There's no system call to create a plain file.
Setting O_CREATE tells Open to create a plain file (not a directory)
if it does not exist.
O_CREATE has no effect if the file already exists:
the function should act just as if O_CREATE had not been set.
-
Do not create directories recursively. For instance, if /d/d1 is an empty directory,
Open("/d/d1/d2/xFile", O_CREATE)
will fail.
-
You don't have to worry about the case where a process opens a file twice without closing
it, or about two processes having a file open at the same time.
-
Each open file has a current position, stored in struct File. When the file
is opened, set the position to zero.
|
int Open_Directory(char *path)
-
Entry point in syscall.c: Sys_OpenDirectory
-
Parameters:
- path: the full pathname of the directory to open
-
Return a newly assigned file descriptor (from 0 to 9) on success, or an error code on failure (see common errors)
-
Comments
-
Unlike Open, this function will not create a new directory. Use CreateDirectory
instead.
|
int CreateDirectory(char *path)
-
Entry point in syscall.c: Sys_CreateDirectory
-
Parameters:
- path: the full pathname of the directory (or directory chain) to create
-
Return 0 on success, or an error code on failure (see common errors):
-
a directory or regular file with the given name already exists
-
Comments
CreateDirectory creates a directory chain recursively if needed. For instance,
if /d/d1 is an empty directory, CreateDirectory("/d/d1/d2/d3") will
create d2 inside d1, and d3 inside d2. This operation
is atomic: either all directories in the chain are created, or none are.
-
If the intermediate directories in the path do not exist, CreateDirectory may,
at your option, return an error or recursively create them.
That is, if /d/d1 is an empty directory,
- CreateDirectory("/d/d1/d2") will create d2 within d1.
- We will not test CreateDirectory("/d/d1/d2/d3"). It may either return an error, or create
d2 within d1 and d3 within d2.
-
CreateDirectory does not open the new directory, since there would be no entries
to read.
|
int Close(int fd)
-
Entry point in syscall.c: Sys_Close
-
Parameters:
- fd: descriptor of the file to close
-
You may choose to write all buffered changes to disk when a file is closed.
-
Return 0 on success, or an error code on failure (see common errors)
|
int Delete(char *path)
-
Entry point in syscall.c: Sys_Delete
-
Parameters:
- path: full path of the file or directory to be deleted
-
Return 0 on success, or an error code on failure (see common errors):
-
The path supplied ends in a directory, and the directory is not empty.
-
Comments
-
If Delete is called and the file is still open in some process,
all subsequent operations on that file except Close() should fail.
(The file could be open in the same process that called Delete, or
in another process.)
|
int Read(int fd, char *buffer, int length)
-
Entry point in syscall.c: Sys_READ
-
Parameters:
- fd: descriptor of the file to be read. It must be a plain
file; use ReadEntry to read from a directory.
- buffer: pointer to a buffer in user space where data will be stored
- length: the number of bytes of data to be read
-
The read starts from the current file position. After reading,
increment the file position by the number of bytes read.
-
On success, return the number of bytes read. This may be less than the
number requested; if the starting position was at the end of the file, it
may be zero. On failure, return an error code (see common errors):
-
the file was not opened with the O_READ flag
-
the descriptor is for a directory rather than a plain file
-
the buffer pointer is null
|
int Read_Entry(int fd, struct VFS_Dir_Entry *dirEntry)
-
Entry point in syscall.c: Sys_ReadEntry
-
Parameters:
- fd: descriptor of the file to be read. It must be a directory;
use Read to read from a plain file.
- dirEntry: pointer to a VFS_Dir_Entry structure
-
On success, return 1, populate the VFS_Dir_Entry struct with the information for
the next directory entry after the current file position.
Note that, as files are added to and deleted from a directory, there may be empty
nodes in the middle of the directory. You should return the contents of the next
non-empty node.
At end of file (that is, if there are no nodes in use between the file position and
the end of the directory), return zero. On failure, return an error code (see
common errors):
-
the descriptor was for a plain file rather than a directory
-
the buffer pointer is null
|
int Write(int fd, char *buffer, int length)
-
Entry point in syscall.c: Sys_Write
-
Parameters:
- fd: descriptor of the file to be read. It must be a plain
file; there is no way to write directly to a directory.
- buffer: pointer to a buffer in user space that holds the data to be written
- length: the number of bytes of data to be written
-
On success, return the number of bytes written and increase the file position.
If the write goes past the end of the file, make the file grow: allocate blocks
as necessary, and increase the file size in the GOSFSfileNode in the directory.
On failure, return an error code (see common errors):
-
the file was not opened with the O_WRITE flag
-
the descriptor is for a directory rather than a plain file
-
the buffer pointer is null
-
the write would increase the number of blocks in the file, but there are no
more free blocks on the disk
|
int Stat(char *path, VFS_File_Stat *stat)
-
Entry point in syscall.c: Sys_Stat
-
Parameters:
- path: full path of the file or directory to be examined
- stat: pointer to a VFS_File_Stat struct that will be populated
with information about the file or directory
-
On success, return 0 and populate the stat struct. On failure, return
an error code (see common errors).
-
Stat is useful for testing whether a file exists without incurring the
overhead of opening it (or iterating through directory entries on the user side).
-
You can ignore the isSetuid and acl elements of
struct VFS_File_Stat. To keep the output of ls.c clean, however,
we recommend filling them with zeroes.
-
Set the size field of struct VFS_File_Stat to the number of
bytes found when the file is read. That is, you should include 4K for each unused
block in a sparse file.
-
Set the numBlocks field of struct VFS_File_Stat to the number of
blocks used to store the file on disk. This number includes indirect blocks, but does
not include the unused blocks in a sparse file.
|
int FStat(int fd, VFS_File_Stat *stat)
-
Entry point in syscall.c: Sys_FStat
-
Parameters:
- fd: descriptor for the (open) file or directory to be examined
- stat: pointer to a VFS_File_Stat struct that will be populated
with information about the file or directory
-
On success, return 0 and populate the stat struct. On failure, return
an error code (see common errors).
-
In GOSFS, FStat is useful mainly to check the size of an open file
or directory. In more fully-featured filesystems, stat and fstat
provide considerably more information about a file.
-
Regarding the contents of the VFS_File_Stat struct, see the description
of Stat().
|
int Seek(int fd, int offset)
-
Entry point in syscall.c: Sys_Seek
-
Parameters:
- fd: descriptor of the file (plain file or directory) to operate on
- offset: position to set in the file
-
offset is an absolute position in the file. For a plain file, interpret
it as a byte position; for a directory, interpret it as the position of a file node.
Position 0 refers to the beginning of the file.
-
Return 0 on success, or an error code on failure (see common errors):
-
Comments
-
It is legal to set the offset past
the end of the file. In this case, reads should return end of file, and writes
should take place at the specified position. You should treat the file as being
filled with zeroes up to the written position. For instance, after the code
int fd = Open("/d/test.txt", O_READ | O_WRITE | O_CREATE);
Write(fd, "test0", 5);
Seek(fd, 10000);
Write(fd, "test10000", 9);
Close(fd);
char buf[50];
fd = Open("/d/test.txt", O_READ);
Seek(fd, 9995);
Read(fd, buf, 10);
Close(fd);
the Read should return 10, and
the buffer should contain five zero bytes followed by the characters "test1".
-
You do not have to allocate the GOSFS blocks that Seek() skipped
over in the above example.
|
int Format(int device, char *filesystem_type)
-
Entry point in syscall.c: Sys_Format
-
Parameters:
- device: the device name (such as ide1) to be formatted
- filesystem_type: the type of filesystem that will be placed on
the device. You only need to handle the string gosfs for this parameter.
-
Return 0 on success, or an error code on failure:
-
the device contains a mounted filesystem
-
the device is not available
-
Don't call Format() unless the user actually runs the format program
(in format.c). This way data on the disk
will persist across sessions.
|
int Sync(void)
-
Entry point in syscall.c: Sys_Sync
-
Parameters: none
-
Return 0 on success, or an error code on failure (see common errors)
-
Sync writes all buffered filesystem data to disk. As implemented in
the VFS layer, it operates on all filesystems. The VFS Sync function will call
GOSFS_Sync for each mounted GOSFS filesystem. GOSFS_Sync must write all buffered
file changes to disk. After a Sync, it must be possible to reboot the machine
without losing any changes.
|
5. Notes
You do not need to consider situations where two processes have the same file open.
Make sure your Mount() works well, so that we can test your project.
If we cannot Mount() a GOSFS filesystem, we cannot grade your project.
You might also want to mount /d automatically in Main()
to speed up your testing,
but the code you submit should not mount /d automatically.
However, /c should be mounted automatically in Main(), as usual.
(So the code that you submit should have no changes in Main()).
We will test your filesystem with disk sizes up to 32 MB and file sizes up
to 5 MB (which is enough to require double-indirect blocks).
The discussion slides will describe how to create an arbitrary size
diskd.img which will be available as device ide1.
This project does not depend on previous ones. You may work on top of your
code from previous projects, or start from the baseline.
6. Testing
In src/user we will provide some programs that can be used to test
your file management syscalls. The tentative list as of November 19 is:
- cp.c
- ls.c
- mkdir.c
- mount.c
- format.c
- p5test.c
We will also provide one or two premade disk images that you can
mount. These will be available by November 26 in the subversion repository
(run svn update or check out a new version of the repository),
and we will update the spec with the final list of test programs at that time.