In this lab you will add large files and mmap to the xv6 file system.
In this assignment you'll increase the maximum size of an xv6 file. Currently xv6 files are limited to 268 blocks, or 268*BSIZE bytes (BSIZE is 1024 in xv6). This limit comes from the fact that an xv6 inode contains 12 "direct" block numbers and one "singly-indirect" block number, which refers to a block that holds up to 256 more block numbers, for a total of 12+256=268. You'll change the xv6 file system code to support a "doubly-indirect" block in each inode, containing 256 addresses of singly-indirect blocks, each of which can contain up to 256 addresses of data blocks. The result will be that a file will be able to consist of up to 256*256+256+11 blocks (11 instead of 12, because we will sacrifice one of the direct block numbers for the double-indirect block).
Modify your Makefile's CPUS definition so that it reads:
CPUS := 1XXX doesn't seem to speedup things
Add
QEMUEXTRA = -snapshotright before QEMUOPTS
The above two steps speed up qemu tremendously when xv6 creates large files.
mkfs initializes the file system to have fewer than 1000 free data blocks, too few to show off the changes you'll make. Modify param.h to set FSSIZE to:
#define FSSIZE 20000 // size of file system in blocks
Download big.c into your xv6 directory, add it to the UPROGS list, start up xv6, and run big. It creates as big a file as xv6 will let it, and reports the resulting size. It should say 140 sectors.
The code that finds a file's data on disk is in bmap() in fs.c. Have a look at it and make sure you understand what it's doing. bmap() is called both when reading and writing a file. When writing, bmap() allocates new blocks as needed to hold file content, as well as allocating an indirect block if needed to hold block addresses.
bmap() deals with two kinds of block numbers. The bn argument is a "logical block" -- a block number relative to the start of the file. The block numbers in ip->addrs[], and the argument to bread(), are disk block numbers. You can view bmap() as mapping a file's logical block numbers into disk block numbers.
You don't have to modify xv6 to handle deletion of files with doubly-indirect blocks.
If all goes well, big will now report that it can write sectors. It will take big minutes to finish. XXX this runs for a while!
Make sure you understand bmap(). Write out a diagram of the relationships between ip->addrs[], the indirect block, the doubly-indirect block and the singly-indirect blocks it points to, and data blocks. Make sure you understand why adding a doubly-indirect block increases the maximum file size by 256*256 blocks (really -1), since you have to decrease the number of direct blocks by one).
Think about how you'll index the doubly-indirect block, and the indirect blocks it points to, with the logical block number.
If you change the definition of NDIRECT, you'll probably have to change the size of addrs[] in struct inode in file.h. Make sure that struct inode and struct dinode have the same number of elements in their addrs[] arrays.
If you change the definition of NDIRECT, make sure to create a new fs.img, since mkfs uses NDIRECT too to build the initial file systems. If you delete fs.img, make on Unix (not xv6) will build a new one for you.
If your file system gets into a bad state, perhaps by crashing, delete fs.img (do this from Unix, not xv6). make will build a new clean file system image for you.
Don't forget to brelse() each block that you bread().
You should allocate indirect blocks and doubly-indirect blocks only as needed, like the original bmap().
Optional challenge: support triple-indirect blocks.
printf("bwrite block %d\n", b->blockno);Build and boot a new kernel and run this:
$ rm README
You should see a sequence of bwrite prints after the rm.
This assignment explores the xv6 log in two parts. First, you'll artificially create a crash which illustrates why logging is needed. Second, you'll remove one inefficiency in the xv6 logging system.
Submit your solution before the beginning of the next lecture to the submission web site.
The point of the xv6 log is to cause all the disk updates of a filesystem operation to be atomic with respect to crashes. For example, file creation involves both adding a new entry to a directory and marking the new file's inode as in-use. A crash that happened after one but before the other would leave the file system in an incorrect state after a reboot, if there were no log.
The following steps will break the logging code in a way that leaves a file partially created.
First, replace commit() in log.c with this code:
#include "kernel/proc.h" void commit(void) { int pid = myproc()->pid; if (log.lh.n > 0) { write_log(); write_head(); if(pid > 1) // AAA log.lh.block[0] = 0; // BBB install_trans(); if(pid > 1) // AAA panic("commit mimicking crash"); // CCC log.lh.n = 0; write_head(); } }
The BBB line causes the first block in the log to be written to block zero, rather than wherever it should be written. During file creation, the first block in the log is the new file's inode updated to have non-zero type. Line BBB causes the block with the updated inode to be written to block 0 (whence it will never be read), leaving the on-disk inode still marked unallocated. The CCC line forces a crash. The AAA lines suppress this buggy behavior for init, which creates files before the shell starts.
Second, replace recover_from_log() in log.c with this code:
static void recover_from_log(void) { read_head(); printf("recovery: n=%d but ignoring\n", log.lh.n); // install_trans(); log.lh.n = 0; // write_head(); }
This modification suppresses log recovery (which would repair the damage caused by your change to commit()).
Finally, remove the -snapshot option from the definition of QEMUEXTRA in your Makefile so that the disk image will see the changes.
Now remove fs.img and run xv6:
% rm fs.img ; make qemu
Tell the xv6 shell to create a file:
$ echo hi > a
You should see the panic from commit(). So far it is as if a crash occurred in a non-logging system in the middle of creating a file.
Now re-start xv6, keeping the same fs.img:
% make qemu
And look at file a:
$ cat a
You should see panic: ilock: no type. Make sure you understand what happened. Which of the file creation's modifications were written to the disk before the crash, and which were not?
static void recover_from_log(void) { read_head(); cprintf("recovery: n=%d\n", log.lh.n); install_trans(); log.lh.n = 0; write_head(); }
Run xv6 (keeping the same fs.img) and read a again:
$ cat a
This time there should be no crash. Make sure you understand why the file system now works.
Why was the file empty, even though you created it with echo hi > a?
Now remove your modifications to commit() (the if's and the AAA and BBB lines), so that logging works again, and remove fs.img.
Suppose the file system code wants to update an inode in block 33. The file system code will call bp=bread(block 33) and update the buffer data. write_log() in commit() will copy the data to a block in the log on disk, for example block 3. A bit later in commit, install_trans() reads block 3 from the log (containing block 33), copies its contents into the in-memory buffer for block 33, and then writes that buffer to block 33 on the disk.
However, in install_trans(), it turns out that the modified block 33 is guaranteed to be still in the buffer cache, where the file system code left it. Make sure you understand why it would be a mistake for the buffer cache to evict block 33 from the buffer cache before the commit.
Since the modified block 33 is guaranteed to already be in the buffer cache, there's no need for install_trans() to read block 33 from the log. Your job: modify log.c so that, when install_trans() is called from commit(), install_trans() does not perform the needless read from the log.
To test your changes, create a file in xv6, restart, and make sure the file is still there. XXX Does this speedup bigfile? XXX Maybe support lseek and modify shell to append to a file?