Lab: mount/umount

In this lab you will add support for mounting/unmounting of file systems to xv6. This lab will expose you to many parts of the xv6 file system, including pathname lookup, inodes, logging/recovery, disk driver, concurrency, etc.

Your job is modify xv6 so that your modified kernel passes the tests in mounttest. You will have to implement two system calls: mount(char *source, char *target) and umount(char *target). Mount attaches the device referenced by source (e.g., /disk1) at the location specified by target. For example, mount("/disk1", "/m") will attach disk1 at the directory /m. After this mount call, users can use pathnames such as /m/README to read the file README stored in the root directory on disk1. Umount removes the attachment. For example, umount("/m") unmounts disk1 from /m.

There are several major challenges in implementing the mount system calls:

The rest of this assignment provides some hints how you might go about the above challenges.

Adding system calls

Add the stubs for the two systems calls to xv6 so that you can compile mounttest and add two empty functions for the two system calls to sysfile.c. Run mounttest and it will fail on the first call to mount.

Adding a second disk

To be able to mount another disk, you need to extend xv6 to support at least two disks. Modify virtio_disk.c to support an array of two disks instead of a single disk. The address of the second disk is 0x10002000; modify the macro R to take a disk number (0, 1,..) and read/write to the memory address for that disk.

All functions in virtio_disk.c need to take the disk number as an argument to update the state of the disk that is read/written to or to receive an interrupt from the disk. Modify virtio_disk_init to take a disk number as an argument and update is to that it initializes that disk. Similar, go through the other functions; make these changes should be most mechanical (i.e., text substitutions).

The second disk interrupts at IRQ 2; modify trap.c to receive that interrupt and virtio_disk_intr with the number of the disk that generated the interrupt.

Modify the file Makefile to tell qemu to provide a second disk. Define the variable QEMUEXTRA = -drive file=fs1.img,if=none,format=raw,id=x1 -device virtio-blk-device,drive=x1,bus=virtio-mmio-bus.1 and add $(QEMUEXTRA) to the end of QEMUOPTS.

Create a second disk image fs1.img. Easiest thing to do is just copy the file fs.img. You might want to add rules to the Makefile to make this image and remove it on make clean.

Add to the user program init a call to create a device for the new disk. For example, add the line mknod("disk1", DISK, 1); to init.c. This will create an inode of type device in the root directory with major number DISK and minor number 1.

The first argument of the mount system call ("disk1") will refer to the device you created using mknod above. In your implementation of the mount system call, call virtio_disk_init with the minor number as the argument to initialize the second disk. (We reserve minor number 0 for the first disk.)

Boot xv6, run mounttest, and make sure virtio_disk_init gets called (e.g., add print statement). You won't know if your changes are correct, but your code should compile and invoke the driver for the second disk.

Modify the logging system

After calling virtio_disk_init, you need to also call loginit to initialize the logging system for the second disk (and restore the second disk if a power failure happened while modifying the second disk). Generalize the logging system to support to two logs, one on disk 0 and one disk 1. These changes are mostly mechanical (e.g., log. changes to log[n].), similar to generalizing the disk driver to support two disks.

To make xv6 compile, you need to provide a disk number to begin_op and end_op. It will be a challenge to figure out what the right value is; for now just specify the first disk (i.e., 0). This isn't correct, since modifications to the second disk should be logged on the second disk, but we have no way yet to read/write the second disk. Come back to this later when you have a better idea how things will fit together, but make sure that xv6 compiles and still runs.

Pathname lookup

Modify namex to traverse mount points: when namex sees an inode to which a file system is attached, it should traverse to the root inode of that file system. Hint: modify the in-memory inode in file.h to keep some additional state, and initialize that state in the mount system call. Note that the inode already has a field for disk number (i.e., dev), which is initialized and passed to reads and writes to the driver. dev corresponds to the minor number for disk devices.

Your modified xv6 should be able to pass the first tests in mounttest (i.e., stat). This is likely to be challenging, however, because now your kernel will be reading from the second disk for the first time, and you may run into many issues.

Even though stat may return correctly, your code is likely to be incorrect, because in namex because iunlockput may modify the second disk (e.g., if another process removes the file or directory) and those modifications must be written to the second disk. Your job is to fix the calls to begin_op and end_op to take the right device. One challenge is that begin_op is called at the beginning of a system call but then you don't know the device that will be involved; you will have to postpone this call until you know which inode is involved (which tells you will which device is involved). Another challenge is that you cannot postpone calling begin_op passed ilock because that violates lock ordering in xv6; you should not be calling begin_op while holding locks on inodes. (The log system allows a few systems calls to run; if a system call that holds an inode lock isn't admitted and one of the admitted system calls needs that inode to complete, then xv6 will deadlock.)

Once you have implemented a plan for begin_op and end_op, see if your kernel can pass test0. It is likely that you will have to modify your implementation of the mount system call to handle several corner cases. See the tests in test0.

Run usertests to see if you didn't break anything else. Since you modified namex and begin/end_op, which are at the core of the xv6 file system, you might have introduced bugs, perhaps including deadlocks. Deadlocks manifest themselves as no output being produced because all processes are sleeping (hit ctrl-p a few times). Your kernel might also suffer kernel panics, because your changes violate invariants. You may have to iterate a few times to get a good design and implementation.

umount

Once your kernel passes usertests and test0 of mounttest, implement umount. The main challenge is that umount of a file system should fail if the file system is still in use; that is, if there is an inode on the mounted device that has a ref > 0. Furthermore, this test and unmounting should be an atomic operation. (Hint: lock the inode cache.) Make sure your kernel passes test1 of mounttest.

Test2 of mounttest stresses namex more; if you have done everything right above, your kernel should pass it. Test3 tests concurrent mount/unmounts with file creation.

crash safety

One of the main goals of the file system is to provide crash safety: if there is a power failure during a file system operation, xv6 should recover correctly. It is difficult to introduce power failure at the critical steps of logging; instead, we added a system call that causes a kernel panic after committing an operation but before installing the operation. Test4 with crashtest tests if your xv6 recovers the mounted disk correctly.

Optional challenges

Modify xv6 so that init mounts the first disk on the root inode. This will allow you to remove some code specific for the first disk from the kernel.

Support mounts on top of mounts.