schfs: commit every 5 s, survive a full disk, mount up to 64 TiB
Transaction groups commit from a schfsd thread every 5 seconds, on sync and fsync, on unmount and shutdown, and early when freed-but-pending blocks crowd the free ones -- the contract ext4 and ZFS have. A crash still lands on the last group whole, and loses at most those seconds. Committing before every write(2) returned cost two cache flushes and an uberblock a call: 4 KB writes go from 2.8 to 6.6 MB/s on AHCI, and from 66 KB/s to 3.9 MB/s on QEMU's IDE disk. Filling the disk made the root volume unmountable, on the last commit as well: a tree update that ran out of blocks halfway was published by the next commit. 1% of the volume (1 to 64 MiB) is now a metadata reserve, as btrfs and ZFS keep one; data stops at it with ENOSPC as a short write, new names at half of it, and deletes may use the rest. A write that runs out part way records what it wrote in the inode rather than leaving blocks past an end of file nothing moved. Deleting a large file goes in 4 MiB steps from the end with a commit allowed between them, as ext4 restarts its transaction through a long truncate. Deleting 80 MB on a full disk ran out of room halfway and leaked the rest. The VFS's dirunlink called the object layer's whole unlink, which freed the object with its last name, so a file removed while open lost its data under the reader. It removes the name only now, and the object is freed on the last iput. An object still open when the machine went down is found by the mount's space scan and freed, like ext4's orphan list. The allocation bitmap has a second level of pointer pages, so the mount ceiling is 64 TiB instead of 64 GiB, and the freed-this-group bitmap has pages only where frees happened: a 1 TiB volume takes 32 MiB. A freed block written after the newest snapshot is reused at once (ZFS's birth-time rule, so bfree carries the block's generation); only older ones are kept while a snapshot exists. Deleting one snapshot no longer stops retaining for the others, which let their blocks be handed out again. Checked under QEMU: crash after sync, after 8 s and within 5 s; fill to ENOSPC, rm, remount; unlink while open; an orphan freed at the next mount; a 1 TiB volume. schfs-check passes, with a new test for the snapshot rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
parent
b336e4a566
commit
edc4e0537d
13 changed files with 572 additions and 144 deletions
|
|
@ -240,6 +240,7 @@ schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
|
|||
schfs_ktx_open(ip->dev);
|
||||
|
||||
uint done = 0;
|
||||
int err = 0;
|
||||
while (done < n) {
|
||||
uint want = n - done;
|
||||
if (want > BOUNCE)
|
||||
|
|
@ -253,6 +254,8 @@ schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
|
|||
}
|
||||
|
||||
long put = schfs_kwrite(ip->dev, ip->inum, off + done, tmp, want);
|
||||
if (put < 0 && done == 0)
|
||||
err = (put == -3) ? -ENOSPC : -EIO;
|
||||
if (put <= 0) break;
|
||||
done += (uint)put;
|
||||
if ((uint)put < want) break;
|
||||
|
|
@ -264,10 +267,10 @@ schfs_vfs_write(struct inode *ip, int user_src, uint64 src, uint64 off, uint n)
|
|||
// A failed commit means nothing durably happened, so report it that way
|
||||
// rather than claiming bytes that are not on the disk.
|
||||
if (cr != 0)
|
||||
return -1;
|
||||
return -EIO;
|
||||
if (off + done > ip->size)
|
||||
ip->size = off + done;
|
||||
return (int)done;
|
||||
return done ? (int)done : err;
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
@ -353,7 +356,13 @@ schfs_vfs_dirunlink(struct inode *dp, char *name, uint off)
|
|||
(void)off;
|
||||
unsigned namelen = 0;
|
||||
while (name[namelen]) namelen++;
|
||||
return schfs_kunlink(dp->dev, dp->inum, name, namelen) == 0 ? 0 : -1;
|
||||
// The name only, as the note at the top of this file says. This called
|
||||
// schfs_kunlink, the object layer's own unlink, which also freed the object
|
||||
// when its last name went -- so a file removed while still open lost its
|
||||
// data under the process reading it, and freeing a large one ran as one
|
||||
// operation big enough to exhaust a full disk halfway. The VFS drops nlink
|
||||
// and calls ifree on the last iput, which frees it in steps.
|
||||
return schfs_kdirdel(dp->dev, dp->inum, name, namelen) == 0 ? 0 : -1;
|
||||
}
|
||||
|
||||
static int
|
||||
|
|
|
|||
Loading…
Reference in a new issue