xv6, line by line
lab 22

Extension labs · lab 22 · File system · ★★☆☆☆

Doubly-indirect blocks: large files

In this tree a file can have at most 268 blocks: 12 whose numbers sit in the inode itself, and 256 more listed in one indirect block. At 1024 bytes a block, that is 268 KiB, and write refuses to go further. In this lab you give the inode one more level: a doubly-indirect block that lists 256 indirect blocks, each listing 256 data blocks. One direct slot is traded for it, so the on-disk inode keeps its size, and the limit grows to 11 + 256 + 65,536 = 65,803 blocks, a little over 64 MiB.

The change to bmap is about thirty lines. What makes the lab worth doing is everything those lines touch. Each new level is a block that must be allocated inside a transaction, zeroed, recorded in its parent and logged, or a crash (or just the buffer cache running out of room) loses part of the file. itrunc must free a three-level tree, in an order that is safe while other harts allocate blocks. The log’s fixed budget per system call, sized for one level of indirection, has to be recounted. The disk itself must grow from 2000 to 70,000 blocks for a maximum-size file to fit, which changes the layout that mkfs computes. And because the meaning of one inode slot changes, every old fs.img is misread by the new kernel, and even written to wrongly.

The recount finds a real problem: the deeper tree lets one operation log more blocks than the log promised, and on this kernel a broken promise ends in panic: bget: no buffers. So the budget is raised too. The reference solution is eight small commits. With it, a 6580-block file (24 times the old limit) is written, read back and freed in about 40 seconds on an otherwise idle computer, and usertests’ writebig writes a full 65,803-block file.

Read first: Tour 29: A disk read, end to end, Tour 30: The buffer cache, Tour 31: The log: begin_op, commit and group commit, Tour 32: Crash recovery, Tour 33: The life of an inode, Tour 36: Reading and writing a file · Locks and interrupt state

What this lab teaches

  • How a file’s block number becomes a disk block number through a tree of block-number tables, and how the arithmetic of bmap (subtract, divide, take the remainder) maps 65,803 file blocks onto 11 direct slots, one indirect block and a two-level tree.
  • Why the on-disk inode must keep its exact size (64 bytes, 16 per block), what checks that, and why changing what one slot means still changes the on-disk format, so fs.img must be rebuilt.
  • How a block is allocated inside a transaction: balloc marks the bitmap and zeroes the block, both through log_write, and the parent that records the new number must be logged too. What happens, on a real run and after a power cut, when one log_write is missing.
  • Why a partially grown file is always consistent after a crash: each chunk of a write is one transaction, and recovery replays it whole or not at all (shown on a real crash).
  • How to count the blocks one system call can log, and what to do when the count outgrows the budget: how many blocks can one chunk of a write, and one unlink, log now? What happens when a group commit fills the log, given that the buffer cache is exactly as large as the log and every logged block is pinned in it?
  • What FSSIZE controls (bitmap size, where data starts, how much fits) and what mkfs derives from it.

The reference branch

ext/22-bigfile in ShowMeTheStack/xv6-riscv-labs, branched from the frozen commit 06aad25; 8 commits.

git clone https://github.com/ShowMeTheStack/xv6-riscv-labs
cd xv6-riscv-labs
git checkout -b my-bigfile 06aad25   # start your own
git diff 06aad25 origin/ext/22-bigfile   # only when you want the answer

1. The spec

Behaviour. A file can grow to MAXFILE = 65,803 blocks (67,382,272 bytes). File blocks 0 to 10 are named directly by addrs[0..10] of the inode, blocks 11 to 266 through the singly-indirect block named by addrs[11], and blocks 267 to 65,802 through the doubly-indirect block named by addrs[12]: its entry i names an indirect block whose entry j names file block 267 + 256i + j. A write that would go past MAXFILE fails. Unlinking a file (or truncating it with O_TRUNC) frees every block of the tree.

What must not change.

The test program, bigfiletest (not bigfile: usertests’ partial_write creates and removes a file of that name, user/usertests.c:2897), and a test-only system call freeblocks() that counts the clear bits in the free-block bitmap:

$ bigfiletest
bigfiletest: 6580 blocks (old limit 268, new limit 65803)
bigfiletest: create: OK
bigfiletest: write: OK
bigfiletest: size: OK
bigfiletest: blocks used 6607 = 6580 data + 27 indirect
bigfiletest: block count: OK
bigfiletest: read back: OK
bigfiletest: unlink: OK
bigfiletest: free blocks 68930 before, 68930 after
bigfiletest: all blocks freed: OK
bigfiletest: ALL OK

bigfiletest n uses n blocks; with n = 65803 it also checks that one more block cannot be written. bigfiletest -w n writes a file and keeps it, bigfiletest -r checks a file left by an earlier boot: the tools for the crash experiments below.

2. Think first

Answer each question in your head (or on paper) before opening a hint. Hints get more specific; the reference answer comes last.

1Where does the extra block number come from?

An inode on disk is a struct dinode: four shorts, a size, and 13 block numbers (kernel/fs.h:32). The doubly-indirect block needs a slot of its own. You can make the array one entry longer, or give up one of the 12 direct slots. Which do you choose, and what exactly breaks with the other choice? Commit to an answer before the hints.

Check yourself

1warm-upType a number

How many bytes is struct dinode in this tree (four shorts, a uint size and 13 uint block numbers)? The kernel and mkfs both depend on it.

kernel/fs.h
31// On-disk inode structure
32struct dinode {
33 short type; // File type
34 short major; // Major device number (T_DEVICE only)
35 short minor; // Minor device number (T_DEVICE only)
36 short nlink; // Number of links to inode in file system
37 uint size; // Size of file (bytes)
38 uint addrs[NDIRECT + 1]; // Data block addresses
39};
decimal, 0x hex or 0b binary
2solidChoose one

A learner lowers NDIRECT to 11 but leaves uint addrs[NDIRECT + 1] in both structs, so the array has 12 entries. They run make qemu. What happens first?

mkfs/mkfs.c
85 exit(1);
86 }
88 assert((BSIZE % sizeof(struct dinode)) == 0);
89 assert((BSIZE % sizeof(struct dirent)) == 0);

2How far can a file grow, and where does block n live?

With 11 direct slots, one singly-indirect block and one doubly-indirect block, how many blocks can a file have? Given a file block number bn, how do you find the entry of the doubly-indirect block, and the entry of the indirect block below it, that hold its disk block number? Which file block is the first to need the doubly-indirect block?

Check yourself

1warm-upType a number

With NDIRECT = 11, one singly-indirect and one doubly-indirect block of 256 entries each, what is MAXFILE, in blocks?

decimal, 0x hex or 0b binary
2solidType a number

File block 6579 is the last block of bigfiletest’s file. Which entry (counting from 0) of the doubly-indirect block names the indirect block that maps it?

decimal, 0x hex or 0b binary
3solidMatch the pairs

Match each file block with where the new bmap finds its disk block number.

3Allocating a path of blocks inside a transaction

Writing file block 267 of a growing file needs three new disk blocks: the doubly-indirect block, indirect block 0 under it, and the data block. Each new block has a bit in the free bitmap and contents of its own, and each parent must record its child’s number. Which blocks must be logged with log_write? Does the order of those calls matter? And if the machine loses power halfway through a large write, what can the file look like after the reboot?

Check yourself

1solidTrue or false, and why

True or false: inside one transaction, bmap must call log_write on a newly allocated indirect block before it stores that block’s number in the parent; otherwise a crash could leave the parent pointing at a block of garbage.

Why?

2solidChoose one

The power fails in the middle of bigfiletest -w 6580 (one block per write). After the reboot, what can bigtest.dat look like on the reference branch?

4How many blocks can one chunk of a write log now?

Every file-system system call promises to log at most MAXOPBLOCKS = 10 blocks, and begin_op admits operations on that promise (kernel/log.c:138). filewrite keeps its side of it by writing at most ((MAXOPBLOCKS - 1 - 1 - 2) / 2) * BSIZE = 3 blocks per transaction (kernel/file.c:149-kernel/file.c:153); the comment says what the formula budgets for. That budget was made for one level of indirection. Recount it for the new tree: what is the largest number of distinct blocks one 3-block chunk can log now? Is the promise still kept?

Check yourself

1solidType a number

On a deliberately fragmented disk (one free block in each of the first five bitmap blocks), how many distinct blocks did one 3-block chunk of a write log, crossing from the singly- into the doubly-indirect range?

decimal, 0x hex or 0b binary
2deepChoose all that apply

Which of these blocks are in the transaction of the chunk that writes file blocks 264 to 267 (the doubly-indirect block does not exist yet)?

5How big must the disk be, and what follows from its size?

usertests’ writebig writes exactly MAXFILE blocks to one file (user/usertests.c:583), and it is one of the quick tests. The disk is FSSIZE = 2000 blocks. What must change, what in the disk layout follows from FSSIZE, and does the kernel need to know the new size?

Check yourself

1warm-upType a number

With FSSIZE = 70,000, how many bitmap blocks does mkfs reserve (nbitmap = FSSIZE / BPB + 1, with BPB = 8192)?

mkfs/mkfs.c
28int nbitmap = FSSIZE / BPB + 1;
30int nlog = LOGBLOCKS + 1; // Header followed by LOGBLOCKS data blocks.
31int nmeta; // Number of meta blocks (boot, sb, nlog, inode, bitmap)
decimal, 0x hex or 0b binary
2solidChoose one

You implement the doubly-indirect block correctly but leave FSSIZE at 2000. What does usertests -q do?

6Freeing a tree, and re-budgeting the log

When the last link to a file is removed and nobody has it open, iput calls itrunc (kernel/fs.c:365). For the doubly-indirect tree, what must be freed, and in what order? Then count: unlinking writebig’s 65,803-block file, how many distinct blocks does that one system call log? With this count and think 4’s, is MAXOPBLOCKS = 10 still a promise the kernel keeps, what happens if it is not, and what should change?

Check yourself

1solidPut in order

Put the reference itrunc's work on the doubly-indirect tree in order, for a file with two indirect blocks under it.

  1. free the doubly-indirect block and clear addrs[12]
  2. read indirect block 1, free its data blocks, then free it
  3. write the inode back with size 0 (iupdate)
  4. read indirect block 0 and free every data block it lists
  5. read the doubly-indirect block
  6. free indirect block 0
2deepType a number

usertests’s writebig writes a 65,803-block file on a fresh 70,000-block disk and unlinks it. How many distinct blocks did that unlink’s transaction log, as measured?

decimal, 0x hex or 0b binary

7How do you prove the blocks came back?

Reading the file back proves that the data is there. It cannot prove that unlinking freed every block, including the 27 indirect ones. What would, from a user program, and what could make such a check lie?

Check yourself

1warm-upType a number

How many indirect blocks (singly-indirect, doubly-indirect and the indirect blocks under it) does a 6580-block file use?

decimal, 0x hex or 0b binary
2deepType a number

A kernel whose itrunc forgets the doubly-indirect tree runs bigfiletest (6580 blocks). By how many blocks is the free count after unlink lower than before the write?

decimal, 0x hex or 0b binary

3. Build it

Start.

git checkout -b my-bigfile 06aad25

Write your test program first (user/bigfiletest.c, added to UPROGS in the Makefile). Do not call it bigfile: usertests’ partial_write overwrites and deletes a file of that name, and your program would vanish after the first usertests run. To count free blocks you need a small system call (the usual five places: syscall.h, syscall.c, sysfile.c, user.h, usys.pl) and a function in fs.c that counts clear bits in the bitmap. On the unmodified kernel, bigfiletest -w 300 must report wrote 268 blocks: that is the old limit, and your test seeing it.

Milestones, in an order that keeps the system bootable after each one.

  1. The inode layout. NDIRECT 11, addrs[NDIRECT + 2] in struct dinode and struct inode. MAXFILE stays NDIRECT + NINDIRECT (now 267). make rebuilds mkfs and fs.img. Test: boot, ls, usertests -q (writebig now writes 267 blocks, all through the old code paths).
  2. bmap. The third range. Nothing reaches it yet. Test: it compiles; reread the index arithmetic against the table in think 2.
  3. itrunc. The tree, children first. Test: still nothing reaches it; usertests -q.
  4. FSSIZE. 70,000. Look at mkfs’s output line nmeta 55 (boot, super, log blocks 31, inode blocks 13, bitmap blocks 9) blocks 69945 total 70000. Test: boot, usertests -q.
  5. Flip the switch: MAXFILE. Add the doubly-indirect range, and make mkfs’s own assertion say what it supports (NDIRECT + NINDIRECT). Test: bigfiletest 300 first (one indirect block under the doubly-indirect block, quick), then bigfiletest, then usertests -q, then bigfiletest again.
  6. Recount, then re-budget the log. Work through think 4 and think 6, then raise MAXOPBLOCKS to cover the worst cases, rewrite filewrite's formula term by term, and give mkfs an assertion that ties the bitmap size to the budget. Test: usertests -q; if you can, a race like the one in clinic 7.

What to expect. On an otherwise idle computer bigfiletest took 37 seconds and usertests -q 7 minutes (421 s; the original kernel takes 43 s). Measured while the computer was busy with other work, they took 114 s and 57 minutes; timings on QEMU depend on what else the computer is doing, so yours will differ. Most of it is writebig: 65,803 one-block writes, each its own transaction. While you iterate, use bigfiletest 300 and usertests writebig.

Debugging advice. Start QEMU halted with make qemu-gdb (it picks its own gdb port and writes it into .gdbinit), and set breakpoints before the first continue.

4. Debugging clinic

Each of these bugs was put into the reference solution on purpose and run on three harts. The symptom is exactly what happened. Try to explain it before revealing why.

1The doubly-indirect block is changed but not logged

In bmap, the new indirect block’s number is stored in the doubly-indirect block, but that buffer is never passed to log_write:

     if ((addr = a[bn / NINDIRECT]) == 0) {
       addr = balloc(ip->dev);
       if (addr) {
         a[bn / NINDIRECT] = addr;
-        log_write(bp);
       }
     }

What happened when we ran it

$ bigfiletest
bigfiletest: 6580 blocks (old limit 268, new limit 65803)
bigfiletest: create: OK
bigfiletest: write: OK
bigfiletest: size: OK
bigfiletest: blocks used 6607 = 6580 data + 27 indirect
bigfiletest: block count: OK
panic: log_write outside of trans

# a fresh image: write the file, then cut the power (no sync, QEMU killed):
$ bigfiletest -w 6580
bigfiletest: wrote 6580 blocks
$ echo off
off
# next boot, same fs.img:
$ bigfiletest -r
panic: log_write outside of trans

# that image booted again, with gdb and a breakpoint on panic; it printed
# the hart and noff, then bt:
hart 1 noff 1
#0  panic (s=s@entry=0x80007548 "log_write outside of trans") at kernel/printk.c:139
#1  0x00000000800040c4 in log_write (b=b@entry=0x80016508 <bcache+3360>) at kernel/log.c:232
#2  0x0000000080002e38 in balloc (dev=1) at kernel/fs.c:80
#3  0x000000008000302c in bmap (ip=ip@entry=0x80020708 <itable+296>, warning: left shift count is negative
warning: right shift count is negative
bn=256, bn@entry=0) at kernel/fs.c:493
#4  0x0000000080003870 in readi (ip=0x80020708 <itable+296>, user_dst=user_dst@entry=1, dst=dst@entry=15136, off=535552, n=n@entry=1024) at kernel/fs.c:599
#5  0x00000000800044ac in fileread (f=0x80022238 <ftable+64>, addr=15136, n=1024) at kernel/file.c:122
[...]
# p bn, p ip->inum, p ip->size, p ip->addrs[12], p 'log.c'::log.outstanding in frame 3:
$1 = 256
$2 = 25
$3 = 6737920
$4 = 1338
$5 = 0

2Off by one at the end of the singly-indirect range

   bn -= NDIRECT;

-  if (bn < NINDIRECT) {
+  if (bn <= NINDIRECT) {
     // Load indirect block, allocating if necessary.

File block 267 (bn = 256 after the subtraction) now goes to the singly-indirect branch, which reads and writes a[256]: four bytes past the end of the block’s 1024-byte data array.

What happened when we ran it

$ bigfiletest 300
bigfiletest: 300 blocks (old limit 268, new limit 65803)
bigfiletest: create: OK
bigfiletest: write: OK
bigfiletest: size: OK
bigfiletest: blocks used 302 = 300 data + 3 indirect
bigfiletest: block count: FAIL
bigfiletest: SOME TESTS FAILED
$ echo off
off
# QEMU killed, then booted again on the same fs.img:

xv6 kernel is booting

hart 1 starting
hart 2 starting
panic: invalid file system

# a fresh image, gdb breaking at `if ((addr = a[bn]) == 0)` when bn == 256;
# printed: bp->blockno, bp - bcache.buf, sizeof(struct buf), &a[bn],
# &bcache.buf[that + 1], &bcache.head, a[bn], bcache.buf[that + 1].valid,
# bcache.buf[that + 1].blockno
# first `bigfiletest 300` (writing file block 267):
$1 = 1081
$2 = 26
$3 = 1112
$4 = (uint *) 0x8001cd48 <bcache+30048>
$5 = (struct buf *) 0x8001cd48 <bcache+30048>
$6 = (struct buf *) 0x80020168 <bcache+43392>
$7 = 1
$8 = 1
$9 = 1330
# second `bigfiletest 300` (writing file block 267):
$10 = 1081
$11 = 16
$12 = 1112
$13 = (uint *) 0x8001a1d8 <bcache+18928>
$14 = (struct buf *) 0x8001a1d8 <bcache+18928>
$15 = (struct buf *) 0x80020168 <bcache+43392>
$16 = 1
$17 = 1
$18 = 1334
# the three runs' output:
bigfiletest: blocks used 302 = 300 data + 3 indirect
bigfiletest: block count: FAIL
[...]
bigfiletest: blocks used 302 = 300 data + 3 indirect
bigfiletest: block count: FAIL
[...]
bigfiletest: blocks used 6606 = 6580 data + 27 indirect
bigfiletest: block count: FAIL

3itrunc does not free the doubly-indirect tree

itrunc is left as it was: it frees the direct blocks and the singly-indirect tree, and never looks at addrs[NDIRECT + 1]:

-  if (ip->addrs[NDIRECT + 1]) {
-    bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
-    ...
-    bfree(ip->dev, ip->addrs[NDIRECT + 1]);
-    ip->addrs[NDIRECT + 1] = 0;
-  }

What happened when we ran it

$ bigfiletest
bigfiletest: 6580 blocks (old limit 268, new limit 65803)
bigfiletest: create: OK
bigfiletest: write: OK
bigfiletest: size: OK
bigfiletest: blocks used 6607 = 6580 data + 27 indirect
bigfiletest: block count: OK
bigfiletest: read back: OK
bigfiletest: unlink: OK
bigfiletest: free blocks 68930 before, 62591 after
bigfiletest: all blocks freed: FAIL
bigfiletest: SOME TESTS FAILED
$ usertests -q
usertests starting
test copyin: OK
[...]
test writebig: balloc: out of blocks
writebig: error: write big file failed i=62346
FAILED
SOME TESTS FAILED
$ bigfiletest
bigfiletest: 6580 blocks (old limit 268, new limit 65803)
bigfiletest: create: OK
balloc: out of blocks
bigfiletest: write: FAIL
bigfiletest: SOME TESTS FAILED

4NDIRECT lowered, but the arrays not lengthened

NDIRECT becomes 11, but both arrays keep their old declaration, which now means 12 entries:

#define NDIRECT    11 // addrs[NDIRECT] is singly, addrs[NDIRECT+1] doubly indirect
...
  uint addrs[NDIRECT + 1]; // in struct dinode, and the same in struct inode

bmap and itrunc are as in the reference, so they use addrs[NDIRECT + 1], one past the end of the array.

What happened when we ran it

# make fs.img (mkfs is built with the host's compiler; macOS's assert message):
mkfs/mkfs fs.img README user/_cat user/_echo user/_forktest user/_grep user/_init user/_kill user/_ln user/_ls user/_mkdir user/_rm user/_sh user/_stressfs user/_usertests user/_grind user/_wc user/_zombie user/_logstress user/_forphan user/_dorphan user/_sync user/_bigfiletest 
Assertion failed: ((BSIZE % sizeof(struct dinode)) == 0), function main, file mkfs.c, line 88.
make: *** [fs.img] Abort trap: 6

# the kernel itself compiled without a warning; booted on an image made by the
# reference mkfs (64-byte inodes):

xv6 kernel is booting

hart 1 starting
hart 2 starting
ireclaim: orphaned inode 4
panic: freeing free block

5FSSIZE left at 2000

Everything as in the reference, except kernel/param.h:

-#define FSSIZE      70000             // size of file system in blocks
+#define FSSIZE      2000              // size of file system in blocks

What happened when we ran it

$ usertests -q
usertests starting
test copyin: OK
[...]
test writetest: OK
test writebig: balloc: out of blocks
writebig: error: write big file failed i=933
FAILED
SOME TESTS FAILED

# a fresh image:
$ bigfiletest
bigfiletest: 6580 blocks (old limit 268, new limit 65803)
bigfiletest: create: OK
balloc: out of blocks
bigfiletest: write: FAIL
bigfiletest: SOME TESTS FAILED

6Running the new kernel on an old fs.img

The reference kernel, booted on an fs.img built by the original mkfs (NDIRECT 12, 2000 blocks), for example a copy kept from before the lab.

What happened when we ran it

xv6 kernel is booting

hart 1 starting
hart 2 starting
init: starting sh
$ ls
exec ls
 failed
$ echo hi
exec echo hi
 failed

7MAXOPBLOCKS left at 10

Everything as in the reference except commit 6: MAXOPBLOCKS stays 10 (so LOGBLOCKS and NBUF stay 30), filewrite keeps its old formula, and mkfs has no nbitmap + 3 check:

-#define MAXOPBLOCKS 13                // max # of blocks any FS op writes
+#define MAXOPBLOCKS 10                // max # of blocks any FS op writes

The trigger is lograce, a separate test program (not part of the branch). It sets up files so that one group commit holds four operations: a 3-block write W to a file f across file blocks 264 to 267, which begin_op admits and which then waits for f’s inode lock (a child is reading all of f); two 3-block overwrites of other files that finish meanwhile; and the unlink of a 9-block file whose blocks lie in 9 different bitmap blocks. It repeats this 12 times with different timings.

What happened when we ran it

$ lograce
lograce: g inode 32
[...]
lograce: setup done, 3086 free
panic: bget: no buffers

# the reference kernel (MAXOPBLOCKS 13) with commit printing and a count of free
# buffers, same program, first attempt:
commit: 30 blocks: 1192 1193 1194 1195 44 1196 1197 1198 1199 45 1200 42 46 55 56 57 58 59 60 61 62 63 67275 1201 67021 9427 17653 25879 34105 47
bcache: min free buffers 10
bcache: min free buffers 8
lograce: attempt 1 (spin 200000) survived
[...]
lograce: attempt 12 (spin 2400000) survived

5. The reference solution

Take the guided tour through the reference solution, one commit at a time, with the machine state at every step:

Open the reveal tour →

Or read the commits

  1. 752860e Give the inode a slot for a doubly-indirect block

    kernel/file.h

    @@ -25,9 +25,9 @@ struct inode {
    2525 short major;
    2626 short minor;
    2727 short nlink;
    2828 uint size;
    29 uint addrs[NDIRECT + 1];
    29 uint addrs[NDIRECT + 2];
    3030};
    3131
    3232// map major device number to device functions.
    3333struct devsw {

    kernel/fs.h

    @@ -22,9 +22,9 @@ struct superblock {
    2222};
    2323
    2424#define FSMAGIC 0x10203040
    2525
    26#define NDIRECT 12
    26#define NDIRECT 11 // addrs[NDIRECT] is singly, addrs[NDIRECT+1] doubly indirect
    2727#define NINDIRECT (BSIZE / sizeof(uint))
    2828#define MAXFILE (NDIRECT + NINDIRECT)
    2929#define NLINK_MAX 32767 // nlink is a short; refuse links past its maximum
    3030
    @@ -34,9 +34,9 @@ struct dinode {
    3434 short major; // Major device number (T_DEVICE only)
    3535 short minor; // Minor device number (T_DEVICE only)
    3636 short nlink; // Number of links to inode in file system
    3737 uint size; // Size of file (bytes)
    38 uint addrs[NDIRECT + 1]; // Data block addresses
    38 uint addrs[NDIRECT + 2]; // Data block addresses
    3939};
    4040
    4141// Inodes per block.
    4242#define IPB (BSIZE / sizeof(struct dinode))
  2. 3a250ba Map blocks through the doubly-indirect block in bmap

    kernel/fs.c

    @@ -411,9 +411,11 @@ ireclaim(int dev)
    411411//
    412412// The content (data) associated with each inode is stored
    413413// in blocks on the disk. The first NDIRECT block numbers
    414414// are listed in ip->addrs[]. The next NINDIRECT blocks are
    415// listed in block ip->addrs[NDIRECT].
    415// listed in block ip->addrs[NDIRECT]. The next NDINDIRECT
    416// blocks are found through block ip->addrs[NDIRECT+1], which
    417// lists NINDIRECT indirect blocks of NINDIRECT blocks each.
    416418
    417419// Return the disk block address of the nth block in inode ip.
    418420// If there is no such block, bmap allocates one.
    419421// returns 0 if out of disk space.
    @@ -453,8 +455,44 @@ bmap(struct inode *ip, uint bn)
    453455 }
    454456 brelse(bp);
    455457 return addr;
    456458 }
    459 bn -= NINDIRECT;
    460
    461 if (bn < NDINDIRECT) {
    462 // Load the doubly-indirect block, allocating if necessary.
    463 if ((addr = ip->addrs[NDIRECT + 1]) == 0) {
    464 addr = balloc(ip->dev);
    465 if (addr == 0)
    466 return 0;
    467 ip->addrs[NDIRECT + 1] = addr;
    468 }
    469 // Load the indirect block that maps bn, allocating if necessary.
    470 bp = bread(ip->dev, addr);
    471 a = (uint *)bp->data;
    472 if ((addr = a[bn / NINDIRECT]) == 0) {
    473 addr = balloc(ip->dev);
    474 if (addr) {
    475 a[bn / NINDIRECT] = addr;
    476 log_write(bp);
    477 }
    478 }
    479 brelse(bp);
    480 if (addr == 0)
    481 return 0;
    482 // Find the data block in it, allocating if necessary.
    483 bp = bread(ip->dev, addr);
    484 a = (uint *)bp->data;
    485 if ((addr = a[bn % NINDIRECT]) == 0) {
    486 addr = balloc(ip->dev);
    487 if (addr) {
    488 a[bn % NINDIRECT] = addr;
    489 log_write(bp);
    490 }
    491 }
    492 brelse(bp);
    493 return addr;
    494 }
    457495
    458496 panic("bmap: out of range");
    459497}
    460498

    kernel/fs.h

    @@ -22,12 +22,13 @@ struct superblock {
    2222};
    2323
    2424#define FSMAGIC 0x10203040
    2525
    26#define NDIRECT 11 // addrs[NDIRECT] is singly, addrs[NDIRECT+1] doubly indirect
    27#define NINDIRECT (BSIZE / sizeof(uint))
    28#define MAXFILE (NDIRECT + NINDIRECT)
    29#define NLINK_MAX 32767 // nlink is a short; refuse links past its maximum
    26#define NDIRECT 11 // addrs[NDIRECT] is singly, addrs[NDIRECT+1] doubly indirect
    27#define NINDIRECT (BSIZE / sizeof(uint))
    28#define NDINDIRECT (NINDIRECT * NINDIRECT)
    29#define MAXFILE (NDIRECT + NINDIRECT)
    30#define NLINK_MAX 32767 // nlink is a short; refuse links past its maximum
    3031
    3132// On-disk inode structure
    3233struct dinode {
    3334 short type; // File type
  3. 13e52a2 Free the doubly-indirect tree in itrunc

    kernel/fs.c

    @@ -501,10 +501,10 @@ bmap(struct inode *ip, uint bn)
    501501void
    502502itrunc(struct inode *ip)
    503503{
    504504 int i, j;
    505 struct buf *bp;
    506 uint *a;
    505 struct buf *bp, *bp2;
    506 uint *a, *a2;
    507507
    508508 for (i = 0; i < NDIRECT; i++) {
    509509 if (ip->addrs[i]) {
    510510 bfree(ip->dev, ip->addrs[i]);
    @@ -523,8 +523,29 @@ itrunc(struct inode *ip)
    523523 bfree(ip->dev, ip->addrs[NDIRECT]);
    524524 ip->addrs[NDIRECT] = 0;
    525525 }
    526526
    527 if (ip->addrs[NDIRECT + 1]) {
    528 bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
    529 a = (uint *)bp->data;
    530 for (i = 0; i < NINDIRECT; i++) {
    531 if (a[i] == 0)
    532 continue;
    533 // free the data blocks, then the indirect block that lists them.
    534 bp2 = bread(ip->dev, a[i]);
    535 a2 = (uint *)bp2->data;
    536 for (j = 0; j < NINDIRECT; j++) {
    537 if (a2[j])
    538 bfree(ip->dev, a2[j]);
    539 }
    540 brelse(bp2);
    541 bfree(ip->dev, a[i]);
    542 }
    543 brelse(bp);
    544 bfree(ip->dev, ip->addrs[NDIRECT + 1]);
    545 ip->addrs[NDIRECT + 1] = 0;
    546 }
    547
    527548 ip->size = 0;
    528549 iupdate(ip);
    529550}
    530551
  4. 0abcc11 Grow the file system to 70000 blocks

    kernel/param.h

    @@ -8,7 +8,7 @@
    88#define MAXARG 32 // max exec arguments
    99#define MAXOPBLOCKS 10 // max # of blocks any FS op writes
    1010#define LOGBLOCKS (MAXOPBLOCKS * 3) // max data blocks in on-disk log
    1111#define NBUF (MAXOPBLOCKS * 3) // size of disk block cache
    12#define FSSIZE 2000 // size of file system in blocks
    12#define FSSIZE 70000 // size of file system in blocks
    1313#define MAXPATH 128 // maximum file path name
    1414#define USERSTACK 1 // user stack pages
  5. d2005f0 Raise MAXFILE to 11 + 256 + 65536 blocks

    kernel/fs.h

    @@ -25,9 +25,9 @@ struct superblock {
    2525
    2626#define NDIRECT 11 // addrs[NDIRECT] is singly, addrs[NDIRECT+1] doubly indirect
    2727#define NINDIRECT (BSIZE / sizeof(uint))
    2828#define NDINDIRECT (NINDIRECT * NINDIRECT)
    29#define MAXFILE (NDIRECT + NINDIRECT)
    29#define MAXFILE (NDIRECT + NINDIRECT + NDINDIRECT)
    3030#define NLINK_MAX 32767 // nlink is a short; refuse links past its maximum
    3131
    3232// On-disk inode structure
    3333struct dinode {

    mkfs/mkfs.c

    @@ -269,9 +269,9 @@ iappend(uint inum, void *xp, int n)
    269269 off = xint(din.size);
    270270 // printf("append inum %d at off %d sz %d\n", inum, off, n);
    271271 while (n > 0) {
    272272 fbn = off / BSIZE;
    273 assert(fbn < MAXFILE);
    273 assert(fbn < NDIRECT + NINDIRECT); // mkfs only builds small files
    274274 if (fbn < NDIRECT) {
    275275 if (xint(din.addrs[fbn]) == 0) {
    276276 din.addrs[fbn] = xint(freeblock++);
    277277 }
  6. a5227d9 Recount the log budget for the doubly-indirect tree

    kernel/file.c

    @@ -146,12 +146,14 @@ filewrite(struct file *f, uint64 addr, int n)
    146146 return -1;
    147147 ret = devsw[f->major].write(1, addr, n);
    148148 } else if (f->type == FD_INODE) {
    149149 // write a few blocks at a time to avoid exceeding
    150 // the maximum log transaction size, including
    151 // i-node, indirect block, allocation blocks,
    152 // and 2 blocks of slop for non-aligned writes.
    153 int max = ((MAXOPBLOCKS - 1 - 1 - 2) / 2) * BSIZE;
    150 // the maximum log transaction size: the i-node,
    151 // up to 3 indirect blocks, the bitmap blocks of
    152 // 2 new indirect blocks, 1 block of slop for a
    153 // non-aligned write, and 2 per data block (the
    154 // block and its bitmap block).
    155 int max = ((MAXOPBLOCKS - 1 - 3 - 2 - 1) / 2) * BSIZE;
    154156 int i = 0;
    155157 while (i < n) {
    156158 int n1 = n - i;
    157159 if (n1 > max)

    kernel/param.h

    @@ -5,9 +5,9 @@
    55#define NINODE 50 // maximum number of active i-nodes
    66#define NDEV 10 // maximum major device number
    77#define ROOTDEV 1 // device number of file system root disk
    88#define MAXARG 32 // max exec arguments
    9#define MAXOPBLOCKS 10 // max # of blocks any FS op writes
    9#define MAXOPBLOCKS 13 // max # of blocks any FS op writes
    1010#define LOGBLOCKS (MAXOPBLOCKS * 3) // max data blocks in on-disk log
    1111#define NBUF (MAXOPBLOCKS * 3) // size of disk block cache
    1212#define FSSIZE 70000 // size of file system in blocks
    1313#define MAXPATH 128 // maximum file path name

    mkfs/mkfs.c

    @@ -86,8 +86,11 @@ main(int argc, char *argv[])
    8686 }
    8787
    8888 assert((BSIZE % sizeof(struct dinode)) == 0);
    8989 assert((BSIZE % sizeof(struct dirent)) == 0);
    90 // unlink of a file that spans every bitmap block logs each of
    91 // them, plus the directory block and two inode blocks.
    92 assert(nbitmap + 3 <= MAXOPBLOCKS);
    9093
    9194 fsfd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
    9295 if (fsfd < 0)
    9396 die(argv[1]);
  7. 4779ed3 Add freeblocks, a system call that counts free blocks

    kernel/defs.h

    @@ -54,8 +54,9 @@ int readi(struct inode*, int, uint64, uint, uint);
    5454void stati(struct inode*, struct stat*);
    5555int writei(struct inode*, int, uint64, uint, uint);
    5656void itrunc(struct inode*);
    5757void ireclaim(int);
    58int nfreeblocks(uint);
    5859
    5960// kalloc.c
    6061void* kalloc(void);
    6162void kfree(void *);

    kernel/fs.c

    @@ -105,8 +105,28 @@ bfree(int dev, uint b)
    105105 log_write(bp);
    106106 brelse(bp);
    107107}
    108108
    109// Count the free blocks in the bitmap.
    110// For testing: the count can be stale by the time it is used.
    111int
    112nfreeblocks(uint dev)
    113{
    114 int b, bi, n;
    115 struct buf *bp;
    116
    117 n = 0;
    118 for (b = 0; b < sb.size; b += BPB) {
    119 bp = bread(dev, BBLOCK(b, sb));
    120 for (bi = 0; bi < BPB && b + bi < sb.size; bi++) {
    121 if ((bp->data[bi / 8] & (1 << (bi % 8))) == 0)
    122 n++;
    123 }
    124 brelse(bp);
    125 }
    126 return n;
    127}
    128
    109129// Inodes.
    110130//
    111131// An inode describes a single unnamed file.
    112132// The inode disk structure holds metadata: the file's type,

    kernel/syscall.c

    @@ -102,8 +102,9 @@ extern uint64 sys_unlink(void);
    102102extern uint64 sys_link(void);
    103103extern uint64 sys_mkdir(void);
    104104extern uint64 sys_close(void);
    105105extern uint64 sys_sync(void);
    106extern uint64 sys_freeblocks(void);
    106107
    107108// An array mapping syscall numbers from syscall.h
    108109// to the function that handles the system call.
    109110static uint64 (*syscalls[])(void) = {
    @@ -129,8 +130,9 @@ static uint64 (*syscalls[])(void) = {
    129130 [SYS_link] = sys_link,
    130131 [SYS_mkdir] = sys_mkdir,
    131132 [SYS_close] = sys_close,
    132133 [SYS_sync] = sys_sync,
    134 [SYS_freeblocks] = sys_freeblocks,
    133135 // clang-format on
    134136};
    135137
    136138void

    kernel/syscall.h

    @@ -20,4 +20,5 @@
    2020#define SYS_link 19
    2121#define SYS_mkdir 20
    2222#define SYS_close 21
    2323#define SYS_sync 22
    24#define SYS_freeblocks 23

    kernel/sysfile.c

    @@ -527,4 +527,11 @@ sys_pipe(void)
    527527 return -1;
    528528 }
    529529 return 0;
    530530}
    531
    532// Return the number of free blocks on the root device.
    533uint64
    534sys_freeblocks(void)
    535{
    536 return nfreeblocks(ROOTDEV);
    537}

    user/user.h

    @@ -24,8 +24,9 @@ int getpid(void);
    2424char *sys_sbrk(int, int);
    2525int pause(int);
    2626int uptime(void);
    2727int sync(void);
    28int freeblocks(void);
    2829
    2930// ulib.c
    3031int stat(const char *, struct stat *);
    3132char *strcpy(char *, const char *);

    user/usys.pl

    @@ -42,4 +42,5 @@ entry("getpid");
    4242entry("sbrk");
    4343entry("pause");
    4444entry("uptime");
    4545entry("sync");
    46entry("freeblocks");
  8. a4eb39c Add bigfiletest, a test program for large files

    Makefile

    @@ -149,8 +149,9 @@ UPROGS=\
    149149 $U/_logstress\
    150150 $U/_forphan\
    151151 $U/_dorphan\
    152152 $U/_sync\
    153 $U/_bigfiletest\
    153154
    154155fs.img: mkfs/mkfs README $(UPROGS)
    155156 mkfs/mkfs fs.img README $(UPROGS)
    156157

    user/bigfiletest.c

    @@ -0,0 +1,148 @@
    1// Test files larger than the old 268-block limit.
    2//
    3// bigfiletest [n] write an n-block file (default 6580), read it
    4// back, count the blocks it used, unlink it, and
    5// check that every block came back.
    6// bigfiletest -w n only write an n-block bigtest.dat and keep it.
    7// bigfiletest -r only read bigtest.dat back and check it.
    8//
    9// (usertests creates and removes a file called "bigfile", so this
    10// program cannot have that name.)
    11
    12#include "kernel/types.h"
    13#include "kernel/stat.h"
    14#include "kernel/fs.h"
    15#include "kernel/fcntl.h"
    16#include "user/user.h"
    17
    18#define NAME "bigtest.dat"
    19
    20char buf[BSIZE];
    21
    22// Fill buf with what block n of the file should hold: its own
    23// block number, then a pattern that differs from block to block.
    24void
    25fill(int n)
    26{
    27 int i;
    28
    29 for (i = 0; i < BSIZE; i++)
    30 buf[i] = n + i;
    31 ((int *)buf)[0] = n;
    32}
    33
    34// How many indirect blocks a file of n blocks needs.
    35int
    36nindirect(int n)
    37{
    38 if (n <= NDIRECT)
    39 return 0;
    40 if (n <= NDIRECT + NINDIRECT)
    41 return 1;
    42 n -= NDIRECT + NINDIRECT;
    43 return 2 + (n + NINDIRECT - 1) / NINDIRECT;
    44}
    45
    46// Append n blocks to fd. Returns how many were written.
    47int
    48writeblocks(int fd, int n)
    49{
    50 int i;
    51
    52 for (i = 0; i < n; i++) {
    53 fill(i);
    54 if (write(fd, buf, BSIZE) != BSIZE)
    55 break;
    56 }
    57 return i;
    58}
    59
    60// Read NAME back. Returns its number of blocks, or -1 if a block
    61// does not hold what writeblocks() put there.
    62int
    63checkfile(void)
    64{
    65 char want[BSIZE];
    66 int fd, i, r;
    67
    68 if ((fd = open(NAME, O_RDONLY)) < 0)
    69 return -1;
    70 for (i = 0;; i++) {
    71 r = read(fd, want, BSIZE);
    72 if (r == 0)
    73 break;
    74 fill(i);
    75 if (r != BSIZE || memcmp(want, buf, BSIZE) != 0) {
    76 printf("bigfiletest: block %d is wrong\n", i);
    77 close(fd);
    78 return -1;
    79 }
    80 }
    81 close(fd);
    82 return i;
    83}
    84
    85void
    86check(char *what, int ok)
    87{
    88 printf("bigfiletest: %s: %s\n", what, ok ? "OK" : "FAIL");
    89 if (!ok) {
    90 printf("bigfiletest: SOME TESTS FAILED\n");
    91 exit(1);
    92 }
    93}
    94
    95int
    96main(int argc, char *argv[])
    97{
    98 int fd, n, f0, f1, f2, used;
    99 struct stat st;
    100
    101 if (argc == 2 && strcmp(argv[1], "-r") == 0) {
    102 n = checkfile();
    103 printf("bigfiletest: %s has %d good blocks, %d blocks free\n", NAME,
    104 n, freeblocks());
    105 exit(n < 0);
    106 }
    107 if (argc == 3 && strcmp(argv[1], "-w") == 0) {
    108 n = atoi(argv[2]);
    109 unlink(NAME);
    110 if ((fd = open(NAME, O_CREATE | O_WRONLY)) < 0)
    111 exit(1);
    112 printf("bigfiletest: wrote %d blocks\n", writeblocks(fd, n));
    113 close(fd);
    114 exit(0);
    115 }
    116
    117 n = argc > 1 ? atoi(argv[1]) : 6580;
    118 if (n < 1 || n > MAXFILE) {
    119 printf("usage: bigfiletest [1..%d] | -w n | -r\n", (int)MAXFILE);
    120 exit(1);
    121 }
    122 printf("bigfiletest: %d blocks (old limit 268, new limit %d)\n", n,
    123 (int)MAXFILE);
    124
    125 unlink(NAME);
    126 fd = open(NAME, O_CREATE | O_WRONLY);
    127 check("create", fd >= 0);
    128 f0 = freeblocks(); // after create: a new directory block is not ours
    129 check("write", writeblocks(fd, n) == n);
    130 if (n == MAXFILE)
    131 check("write past MAXFILE fails", write(fd, buf, BSIZE) < 0);
    132 close(fd);
    133
    134 check("size", stat(NAME, &st) == 0 && st.size == (uint64)n * BSIZE);
    135 f1 = freeblocks();
    136 used = f0 - f1;
    137 printf("bigfiletest: blocks used %d = %d data + %d indirect\n", used,
    138 n, nindirect(n));
    139 check("block count", used == n + nindirect(n));
    140 check("read back", checkfile() == n);
    141
    142 check("unlink", unlink(NAME) == 0);
    143 f2 = freeblocks();
    144 printf("bigfiletest: free blocks %d before, %d after\n", f0, f2);
    145 check("all blocks freed", f2 == f0);
    146 printf("bigfiletest: ALL OK\n");
    147 exit(0);
    148}

6. Verify and measure

On the branch (ext/22-bigfile, 8 commits; each commit builds, and mkfs runs, on its own), built with the project toolchain and run on 3 harts (-smp 3 -m 128M), on a fresh fs.img:

$ bigfiletest
bigfiletest: 6580 blocks (old limit 268, new limit 65803)
bigfiletest: create: OK
bigfiletest: write: OK
bigfiletest: size: OK
bigfiletest: blocks used 6607 = 6580 data + 27 indirect
bigfiletest: block count: OK
bigfiletest: read back: OK
bigfiletest: unlink: OK
bigfiletest: free blocks 68930 before, 68930 after
bigfiletest: all blocks freed: OK
bigfiletest: ALL OK
$ usertests -q
usertests starting
test copyin: OK
test copyout: OK
[...]
test writebig: OK
[...]
test kernmem: usertrap(): unexpected scause 0xd pid=6477
            sepc=0x1b64 stval=0x80000000
[...]
ALL TESTS PASSED
$ bigfiletest
[...]
bigfiletest: blocks used 6607 = 6580 data + 27 indirect
bigfiletest: block count: OK
bigfiletest: read back: OK
bigfiletest: unlink: OK
bigfiletest: free blocks 68927 before, 68927 after
bigfiletest: all blocks freed: OK
bigfiletest: ALL OK

The usertrap() lines are kernmem checking that user code cannot read kernel memory: expected kills. writebig now writes and reads back a 65,803-block file and unlinks it, and everything else in the suite (truncation, unlinking open files, concurrent writers in fourfiles and sharedfd) still works. The second bigfiletest starts from 68,927 free blocks, 3 fewer, as on the original kernel (948 before usertests, 945 after): usertests leaves a few small files (x, junk, bigarg-ok, stopforking) and a grown root directory behind. What matters is that before and after are equal.

The full-size case, on a fresh image:

$ bigfiletest 65803
bigfiletest: 65803 blocks (old limit 268, new limit 65803)
bigfiletest: create: OK
bigfiletest: write: OK
bigfiletest: write past MAXFILE fails: OK
bigfiletest: size: OK
bigfiletest: blocks used 66061 = 65803 data + 258 indirect
bigfiletest: block count: OK
bigfiletest: read back: OK
bigfiletest: unlink: OK
bigfiletest: free blocks 68930 before, 68930 after
bigfiletest: all blocks freed: OK
bigfiletest: ALL OK

The race of clinic 7, on the branch:

$ lograce
[...]
lograce: setup done, 3086 free
lograce: attempt 1 (spin 200000) survived
[...]
lograce: attempt 12 (spin 2400000) survived

On the original kernel (with only freeblocks and the test added), bigfiletest -w 300 prints bigfiletest: wrote 268 blocks, and bigfiletest 268 passes with blocks used 269 = 268 data + 1 indirect: the old limit, measured with the same tool. Six power cuts during bigfiletest -w 6580 all left a consistent file (reveal step 7).

All numbers from recorded runs (QEMU 10.2.1, 3 harts), except where a row says “from mkfs”. Timings on QEMU depend on what else the computer is doing: “quiet” times were measured on an otherwise idle computer, “busy” ones while it was busy with other work; your times will differ. Ranges are over our runs.

original (06aad25) this branch
largest file 268 blocks, 274,432 bytes 65,803 blocks, 67,382,272 bytes
struct dinode, inodes per block 64 bytes, 16 64 bytes, 16
FSSIZE, image size (from mkfs) 2000 blocks, 2 MB 70,000 blocks, 70 MB
log, buffer cache (MAXOPBLOCKS × 3) 30 blocks, 30 buffers 39 blocks, 39 buffers
metadata, first data block (from mkfs) 47 64
free blocks on a fresh image (with the test program) 948 at the first count 68,930
bigfiletest 268: write, read back, unlink 1.5 s 1.5 s
bigfiletest (6580 blocks) not possible 36.8 s quiet; 114 s with the computer busy
bigfiletest 65803 not possible 384 s quiet; 3157 s busy
usertests -q 43.0 s, 45.1 s 421 s quiet; 3390 s busy

Metadata cost. 27 indirect blocks for 6580 data blocks, 258 for 65,803: 0.4% either way. One table block per 256 data blocks, plus two.

What the log sees. A copy of each kernel printed every commit of 9 or more blocks during usertests -q, and the copy of the branch also kept the lowest number of free buffers it ever saw:

original this branch
commits of 9 or more blocks 11, 10 (sharedfd); 11, 13, 9 (fourfiles) 12 (writebig); 15 (sharedfd)
largest commit of a single operation under 9 (none printed) 12, writebig’s unlink
fewest free buffers not measured 23 of 39
lograce (clinic 7) (with MAXOPBLOCKS 10:) panic: bget: no buffers a 30-block group commit, 8 of 39 buffers free at the lowest; 12 of 12 attempts survive
one write chunk on a fragmented disk (frag13) not possible 13 blocks

The sharedfd and fourfiles commits are group commits: several processes write at once and their operations share one transaction. With the larger budget more operations fit in one group, so the branch’s sharedfd commit is larger. The 12-block one is a single operation: writebig runs alone, and its unlink logged 64 42 43 55 56 57 58 59 60 61 62 63, the directory block, two inode blocks and all nine bitmap blocks.

Time. usertests -q takes about ten times longer. The only quick test whose work grew is writebig, which now writes, reads and frees 65,803 blocks instead of 268, one transaction per block, so the difference is its (reasoned; we timed the suite, not each test). bigfiletest 268 takes the same 1.5 s on both kernels: the new code only adds work beyond block 266 (on the branch the 268-block file already has 3 table blocks instead of 1, which costs nothing measurable). Every one-block write is a transaction of its own: typically 4 logged blocks (bitmap, indirect block, data, inode), so 10 disk writes at commit (4 to the log, the header, 4 installs, the header again; commit).

7. Go further