An x86-64 operating system built from xv6, now largely rewritten: SMP, a copy-on-write root filesystem (SchFS), PCI/USB/virtio drivers, a TTF rasteriser, and a Linux-numbered syscall ABI. No history before this point -- the tree was never under version control. This commit exists so the rename that follows has something to undo to.
246 lines
10 KiB
C
246 lines
10 KiB
C
// SchFS — on-disk format.
|
|
//
|
|
// Design rationale lives in docs/architecture/schfs.md. In short: copy-on-write
|
|
// with the checksum of a block stored in the pointer that leads to it (ZFS),
|
|
// one B-tree reused for every kind of record (btrfs), and attribute indexes as
|
|
// just another record type (BeFS).
|
|
//
|
|
// This header is shared by the kernel and by host tools (tools/mkfs,
|
|
// tools/schfs_test), so it must stay free of kernel-only types.
|
|
|
|
#ifndef SCHFS_H
|
|
#define SCHFS_H
|
|
|
|
#include <stdint.h>
|
|
|
|
#define SCHFS_MAGIC 0x53636846u // "SchF"
|
|
#define SCHFS_NODE_MAGIC 0x536e6f64u // "Snod"
|
|
|
|
// 4096 to match modern media and make extents worth having. The old format
|
|
// used 1024, which is also why its directories were so wasteful.
|
|
#define SCHFS_BLKSIZE 4096
|
|
|
|
// Deepest tree we will walk without recursing. 8 levels of a ~84-way internal
|
|
// node is far more than any plausible volume needs.
|
|
#define SCHFS_MAX_LEVEL 8
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Keys
|
|
//
|
|
// Every record in the filesystem is addressed by one 24-byte key, sorted by
|
|
// (objectid, type, offset). This is the whole reason there is only one tree:
|
|
// inodes, directory entries, extent maps and indexes are just different `type`
|
|
// values in the same keyspace.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define SCHFS_KEY_INODE 1 // (ino, INODE, 0) -> struct schfs_inode
|
|
#define SCHFS_KEY_DIRENT 2 // (dirino,DIRENT, namehash) -> packed dirent list
|
|
#define SCHFS_KEY_EXTENT 3 // (ino, EXTENT, fileblk) -> struct schfs_extent
|
|
#define SCHFS_KEY_ATTR 4 // (ino, ATTR, namehash) -> attribute value
|
|
#define SCHFS_KEY_INDEX 5 // (idxid, INDEX, valuehash) -> objectid
|
|
#define SCHFS_KEY_CSUM 6 // (CSUM, CSUM, diskblk) -> uint32 CRC32c
|
|
|
|
// Data-block checksums live under one reserved object id, exactly as btrfs
|
|
// keeps a checksum tree. Tree nodes carry their checksum in their parent
|
|
// pointer, but file *data* blocks have no parent to hold it — without these
|
|
// records "end-to-end checksums" would only cover metadata, which is the part
|
|
// least likely to rot.
|
|
#define SCHFS_CSUM_OBJECTID 1ULL
|
|
|
|
// Object ids below this are reserved; the root directory is the first real one.
|
|
#define SCHFS_FIRST_OBJECTID 16ULL
|
|
#define SCHFS_ROOT_OBJECTID 16ULL
|
|
|
|
struct schfs_key {
|
|
uint64_t objectid;
|
|
uint64_t offset;
|
|
uint8_t type;
|
|
uint8_t pad[7];
|
|
}; /* 24 bytes */
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Node header — identical for internal nodes and leaves.
|
|
//
|
|
// `blocknr` is a self-pointer: a block that was written to the wrong place
|
|
// still checksums correctly, so the only way to catch a misdirected write is
|
|
// for the block to say where it thinks it lives.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
struct schfs_node_hdr {
|
|
uint32_t magic; // SCHFS_NODE_MAGIC
|
|
uint32_t crc; // CRC32c of the whole block, this field zeroed
|
|
uint64_t blocknr; // where this block believes it lives
|
|
uint64_t generation; // transaction that wrote it
|
|
uint16_t level; // 0 = leaf, >0 = internal
|
|
uint16_t nritems;
|
|
uint32_t pad;
|
|
}; /* 32 bytes */
|
|
|
|
// Internal node entry: a key and the child it leads to, plus that child's
|
|
// checksum and generation. Storing the checksum *here* rather than in the
|
|
// child is what makes the tree self-validating.
|
|
struct schfs_kptr {
|
|
struct schfs_key key;
|
|
uint64_t blocknr;
|
|
uint64_t generation;
|
|
uint32_t crc;
|
|
uint32_t pad;
|
|
}; /* 48 bytes */
|
|
|
|
// Leaf entry. Items grow forward from the end of the header; their data grows
|
|
// backward from the end of the block, exactly like btrfs. `offset` is measured
|
|
// from the start of the block.
|
|
struct schfs_item {
|
|
struct schfs_key key;
|
|
uint32_t offset;
|
|
uint32_t size;
|
|
}; /* 32 bytes */
|
|
|
|
#define SCHFS_NODE_SPACE (SCHFS_BLKSIZE - (int)sizeof(struct schfs_node_hdr))
|
|
#define SCHFS_MAX_KPTR (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_kptr))
|
|
#define SCHFS_MAX_ITEMS (SCHFS_NODE_SPACE / (int)sizeof(struct schfs_item))
|
|
|
|
// Largest value that can live inline in a leaf. Anything bigger becomes an
|
|
// extent record pointing at data blocks.
|
|
#define SCHFS_MAX_INLINE (SCHFS_NODE_SPACE / 4)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Uberblock
|
|
//
|
|
// A ring of these is written round-robin. Mounting means reading them all and
|
|
// taking the highest generation that checksums — that is the entire crash
|
|
// recovery story, in place of a journal replay.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define SCHFS_UBER_COUNT 128
|
|
#define SCHFS_UBER_START 1 // block 0 stays free for a boot sector
|
|
|
|
// First block available for the tree and for file data: everything below is
|
|
// the boot block and the uberblock ring.
|
|
#define SCHFS_FIRST_BLOCK (SCHFS_UBER_START + SCHFS_UBER_COUNT)
|
|
|
|
#define SCHFS_VERSION 1
|
|
#define SCHFS_LABEL_MAX 32
|
|
|
|
struct schfs_uberblock {
|
|
uint32_t magic; // SCHFS_MAGIC
|
|
uint32_t crc; // CRC32c of this struct, this field zeroed
|
|
uint32_t version; // SCHFS_VERSION
|
|
uint32_t blocksize; // SCHFS_BLKSIZE; a reader must refuse a mismatch
|
|
uint64_t generation; // transaction number; highest valid one wins
|
|
uint64_t root; // block number of the tree root
|
|
uint32_t root_crc; // checksum of that root block
|
|
uint32_t pad0;
|
|
uint64_t root_generation;
|
|
uint64_t total_blocks;
|
|
// Informational only. Which blocks are actually free is derived from the
|
|
// tree at mount rather than stored, so this is what the volume believed at
|
|
// commit time — useful for df before anything is mounted, never trusted.
|
|
uint64_t free_blocks;
|
|
uint64_t reserved0;
|
|
uint64_t next_objectid;
|
|
uint64_t root_objectid; // normally SCHFS_ROOT_OBJECTID
|
|
uint8_t uuid[16];
|
|
char label[SCHFS_LABEL_MAX];
|
|
uint8_t pad[SCHFS_BLKSIZE - 136];
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Records
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define SCHFS_T_FILE 1
|
|
#define SCHFS_T_DIR 2
|
|
#define SCHFS_T_DEV 3
|
|
#define SCHFS_T_SYMLINK 4
|
|
|
|
struct schfs_inode {
|
|
uint64_t size;
|
|
uint64_t blocks;
|
|
uint64_t atime, mtime, ctime;
|
|
// Containing directory. Meaningful for directories only, where it is what
|
|
// ".." resolves to — neither "." nor ".." is stored on disk, so a directory
|
|
// costs nothing until it has real entries. For files with several hard
|
|
// links "parent" is simply the first directory that linked them and must
|
|
// not be trusted.
|
|
uint64_t parent;
|
|
uint32_t mode; // real POSIX mode bits, at last
|
|
uint32_t uid, gid;
|
|
uint32_t nlink;
|
|
uint16_t type; // SCHFS_T_*
|
|
uint16_t rdev_major, rdev_minor;
|
|
uint16_t pad;
|
|
uint32_t flags; // SCHFS_I_* — none defined yet
|
|
uint32_t gen; // transaction that last changed this inode;
|
|
// what an incremental send/snapshot diff needs
|
|
};
|
|
|
|
// An extent maps a run of file blocks onto a run of disk blocks. refs is what
|
|
// makes snapshots and reflink copies nearly free: sharing a range is a refcount
|
|
// bump rather than a data copy.
|
|
struct schfs_extent {
|
|
uint64_t disk_block;
|
|
uint64_t nblocks;
|
|
// Transaction group that allocated this run. A run stamped with the group
|
|
// currently being built cannot be reached from any committed root, so its
|
|
// blocks may be overwritten in place — which is what stops a program that
|
|
// writes a hundred bytes at a time from copying a whole 4 KB block, and
|
|
// then a whole tree path, on every call.
|
|
//
|
|
// A full 64 bits, matching the tree's generation counter, because a 32-bit
|
|
// one would wrap after four billion commits and a wrapped comparison says
|
|
// "same group" about a block belonging to a committed tree — the one
|
|
// mistake this field exists to prevent. Only ever trusted together with
|
|
// refs == 1; a shared run belongs to somebody else as well.
|
|
uint64_t gen;
|
|
uint32_t refs;
|
|
uint32_t pad;
|
|
};
|
|
|
|
// Directory entry payload. The key's offset field holds the name hash, so a
|
|
// lookup is one tree descent rather than a directory scan.
|
|
//
|
|
// Two names in one directory can hash the same, and the key would then be
|
|
// identical — the second insert would silently replace the first. So the value
|
|
// is a *packed list*: one schfs_dirent_rec plus its name bytes, repeated. A
|
|
// 64-bit hash makes lists longer than one vanishingly rare, but "vanishingly
|
|
// rare" is not "impossible", and a filesystem that loses a file under a hash
|
|
// collision is not one to build on.
|
|
struct schfs_dirent_rec {
|
|
uint64_t objectid;
|
|
uint16_t type;
|
|
uint16_t namelen;
|
|
uint32_t pad;
|
|
/* name bytes follow, namelen of them, not NUL-terminated */
|
|
};
|
|
|
|
#define SCHFS_NAME_MAX 255
|
|
|
|
// Step from one packed dirent to the next. Records are 8-byte aligned so the
|
|
// uint64 objectid of the next one is naturally aligned.
|
|
#define SCHFS_DIRENT_LEN(namelen) \
|
|
((unsigned)((sizeof(struct schfs_dirent_rec) + (namelen) + 7) & ~7u))
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Compile-time layout checks.
|
|
//
|
|
// Every one of these is a number that ends up baked into images on disk. A
|
|
// silent change to any of them — adding a field, changing a type, a compiler
|
|
// padding a struct differently — makes every existing image unreadable in a way
|
|
// that looks like corruption rather than like a mistake. Failing the build is
|
|
// enormously cheaper than debugging that.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define SCHFS_ASSERT_SIZE(t, n) \
|
|
typedef char schfs_sizecheck_##t[(sizeof(struct t) == (n)) ? 1 : -1]
|
|
|
|
SCHFS_ASSERT_SIZE(schfs_key, 24);
|
|
SCHFS_ASSERT_SIZE(schfs_node_hdr, 32);
|
|
SCHFS_ASSERT_SIZE(schfs_kptr, 48);
|
|
SCHFS_ASSERT_SIZE(schfs_item, 32);
|
|
SCHFS_ASSERT_SIZE(schfs_uberblock, SCHFS_BLKSIZE);
|
|
SCHFS_ASSERT_SIZE(schfs_inode, 80);
|
|
SCHFS_ASSERT_SIZE(schfs_extent, 32);
|
|
SCHFS_ASSERT_SIZE(schfs_dirent_rec, 16);
|
|
|
|
#endif /* SCHFS_H */
|