schfs: the commit barrier, reflink, and attributes with an index over them
The last three phases of the filesystem, which were written together and
touch the same files throughout -- kmount.c, objw.c and vfs.c are each
rewritten by all three -- so they land as one commit rather than three
that could not be built separately.
Durability. A transaction that reaches the platter out of order is worse
than one that never reached it: the uberblock can name a tree whose
blocks are still in the drive's write cache. bdirty/bflush give the
buffer layer an explicit barrier, ideflush issues ATA FLUSH CACHE and
usb_storage_flush the SCSI SYNCHRONIZE CACHE that means the same thing,
and a commit now waits for the data before it writes the root that
points at it. The uberblock groups are read newest-first with fallback,
so a volume whose newest roots are torn mounts at an older one and says
which generations were lost.
Reflink. Extents carry a refcount, so two objects can name the same
blocks and neither frees them while the other holds them. Exposed as
FICLONE -- Linux's ioctl value, so `cp --reflink` works unmodified --
and as cmd/reflink for the case where there is no cp to hand.
Attributes, and the one genuinely new idea here. attr.c stores named
values on an object, reachable through setxattr/getxattr/removexattr at
Linux's syscall numbers. On top of that, schfs_index declares an index
over an attribute name and schfs_query answers "which objects carry this
value" from the B-tree rather than by walking. A query against an
unindexed attribute is refused with an error, never silently answered by
a full scan: a query that quietly becomes a scan is worse than one that
admits it cannot help. cmd/schattr drives both.
Three bugs fixed on the way, each found by the numbers being wrong
rather than by inspection:
- statfs's f_fsid was declared int64[2]. Linux's fsid_t is two *ints*,
so every field after it landed one slot late: userland read f_frsize
out of f_namelen, got 255, and df reported a 32 MB root filesystem
as 1992 KB. It hid for as long as it did because the fields used to
be invented constants, so wrong numbers looked like the usual ones.
- struct inode's nlink was a short against SchFS's uint32, so a file
past 32767 names came back negative -- and iput frees an object
whose count reaches zero. It is an int now, signed on purpose, so an
underflow lands somewhere a check can see rather than wrapping.
- filewrite split every write to fit v1's log. That limit belongs to
the journal, not to writing, and SchFS makes one transaction of
whatever it is handed: a 1 MB write was becoming 170 commits and
170 flushes for nothing. fs_ops carries max_write now, and SchFS
sets no limit.
Also here: a real mount table, so `mount -t schfs /dev/hdb /mnt` shows
up in /proc/mounts -- df looks a path up there before it calls statfs at
all, so a mount missing from that list was invisible to it -- several
volumes mounted at once, snapshots mounted by name, and /proc/fsstat
with an rdtsc-based counter, which is the only instrument fine enough to
see inside a single syscall.
Verified: `make schfs-check` passes in full, including the sweep that
crashes the volume at each of 119 points mid-transaction and requires
every one to mount with exactly its committed data, and the case where
both newest uberblocks are rubbish and the volume still comes back. The
headless boot check passes with 27 [ok] and no failures, on a SchFS root.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f4d5c671a7
commit
03cc316001
39 changed files with 3942 additions and 550 deletions
151
include/defs.h
151
include/defs.h
|
|
@ -23,6 +23,13 @@ void binit(void);
|
|||
struct buf* bread(uint, uint64);
|
||||
void brelse(struct buf*);
|
||||
void bwrite(struct buf*);
|
||||
// Modify without writing. The buffer stays dirty in the cache until an
|
||||
// eviction or a bflush() takes it out; the caller is promising to ask for the
|
||||
// ordering it needs explicitly rather than relying on write order.
|
||||
void bdirty(struct buf*);
|
||||
// Write out every dirty buffer for `dev`, then make the device commit them.
|
||||
// Returns 0, or -1 if the device could not confirm durability.
|
||||
int bflush(uint dev);
|
||||
|
||||
// console.c
|
||||
struct termios;
|
||||
|
|
@ -69,7 +76,12 @@ void mountinit(void);
|
|||
// 1 if the filesystem on this device refuses writes -- a mounted snapshot.
|
||||
// Ask before mutating, not after: the create path panics on a failed dirlink.
|
||||
int fs_readonly(uint dev);
|
||||
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root);
|
||||
int mount_add(uint dev, uint64 inum, uint fsdev, uint64 root,
|
||||
const char *path, const char *source, const char *type);
|
||||
// Walk the mount table for /proc/mounts. `path` and `source` need MAXPATH
|
||||
// bytes, `type` 16. Returns 0 for a live entry, -1 for an empty slot.
|
||||
int mount_get(int i, char *path, char *source, char *type,
|
||||
uint *fsdev);
|
||||
int mount_del_by_root(uint fsdev, uint64 root);
|
||||
struct inode* ialloc(uint, short);
|
||||
struct inode* iget(uint, uint64);
|
||||
|
|
@ -130,6 +142,8 @@ void iderw(struct buf*);
|
|||
// For probes, not for reads: iderw() panics on an absent drive, so anything
|
||||
// that is guessing whether a disk exists has to ask here first.
|
||||
int ide_disk_present(int dev);
|
||||
// ATA FLUSH CACHE: block until the drive has committed its write cache.
|
||||
int ideflush(int dev);
|
||||
|
||||
// fs/schfs/kmount.c -- SchFS.
|
||||
//
|
||||
|
|
@ -138,52 +152,18 @@ int ide_disk_present(int dev);
|
|||
// Every schfs_k* that changes anything is one transaction and is durable by the
|
||||
// time it returns.
|
||||
struct schfs_inode;
|
||||
void schfs_kmount(uint dev);
|
||||
|
||||
// Mounting. Slot 0 is the root filesystem, mounted by proc.c before pid 1 has a
|
||||
// working directory; schfs_kmount_dev takes any other SchFS disk and returns
|
||||
// the filesystem device number it landed on, or a negative errno.
|
||||
void schfs_kmount(uint bdev);
|
||||
int schfs_kmount_dev(uint bdev);
|
||||
int schfs_kumount(uint dev);
|
||||
int schfs_kmounted(void);
|
||||
uint64 schfs_klookup(const char *path);
|
||||
int schfs_kstat(uint64 ino, struct schfs_inode *out);
|
||||
long schfs_kread(uint64 ino, uint64 off, void *buf, unsigned n);
|
||||
int schfs_kreaddir(uint64 dir,
|
||||
int (*fn)(uint64, unsigned short, const char *,
|
||||
unsigned, void *),
|
||||
void *arg);
|
||||
int schfs_kinfo(char *buf, int cap);
|
||||
|
||||
int schfs_kcreate(uint64 dir, const char *name, unsigned namelen,
|
||||
unsigned short type, uint mode,
|
||||
unsigned short major, unsigned short minor, uint64 *out);
|
||||
long schfs_kwrite(uint64 ino, uint64 off, const void *buf,
|
||||
unsigned n);
|
||||
int schfs_ktruncate(uint64 ino, uint64 size);
|
||||
int schfs_kunlink(uint64 dir, const char *name, unsigned namelen);
|
||||
int schfs_klink(uint64 dir, const char *name, unsigned namelen,
|
||||
uint64 ino);
|
||||
int schfs_krename(uint64 olddir, const char *oldname,
|
||||
unsigned oldlen, uint64 newdir, const char *newname,
|
||||
unsigned newlen);
|
||||
int schfs_ksetattr(uint64 ino, const struct schfs_inode *in);
|
||||
int schfs_ksync(void);
|
||||
|
||||
// Pieces the VFS layer (fs/schfs/vfs.c) needs, where a directory entry and the
|
||||
// object it names are separate operations. The VFS keeps the link count.
|
||||
uint64 schfs_klookup_at(uint64 dir, const char *name, unsigned len);
|
||||
int schfs_kdirempty(uint64 dir);
|
||||
int schfs_kalloc_object(unsigned short type, uint mode,
|
||||
uint64 *out);
|
||||
int schfs_kdiradd(uint64 dir, const char *name, unsigned len,
|
||||
uint64 ino, unsigned short type);
|
||||
int schfs_kdirdel(uint64 dir, const char *name, unsigned len);
|
||||
int schfs_kfree(uint64 ino);
|
||||
uint64 schfs_kroot(void);
|
||||
|
||||
// A snapshot mounted read-only: the same block device with an older tree root.
|
||||
// The read path takes a device number so the VFS can address either view; there
|
||||
// is no write path, because a snapshot that could be written to would not be
|
||||
// one. See kernel/fs/schfs/kmount.c.
|
||||
// Returns the device number of the slot the snapshot landed in, or negative.
|
||||
int schfs_ksnap_mount(const char *name, unsigned namelen);
|
||||
void schfs_ksnap_unmount(uint dev);
|
||||
uint64 schfs_ksnap_root(uint dev);
|
||||
// Read side. `dev` is a filesystem device number -- a live volume or a mounted
|
||||
// snapshot -- not the block device underneath it.
|
||||
uint64 schfs_klookup(uint dev, const char *path);
|
||||
long schfs_kread_dev(uint dev, uint64 ino, uint64 off, void *buf,
|
||||
unsigned n);
|
||||
int schfs_kstat_dev(uint dev, uint64 ino, struct schfs_inode *out);
|
||||
|
|
@ -193,6 +173,85 @@ int schfs_kreaddir_dev(uint dev, uint64 dir,
|
|||
int (*fn)(uint64, unsigned short, const char *,
|
||||
unsigned, void *),
|
||||
void *arg);
|
||||
// Resume a directory walk at a tree key. The callback gets each entry's hash,
|
||||
// which is what getdents hands back to userland as its position.
|
||||
int schfs_kreaddir_from_dev(uint dev, uint64 dir, uint64 start_hash,
|
||||
int (*fn)(uint64, unsigned short, const char *,
|
||||
unsigned, uint64, void *),
|
||||
void *arg);
|
||||
uint64 schfs_kroot(uint dev);
|
||||
int schfs_kinfo(char *buf, int cap);
|
||||
|
||||
// Write side. Each is one transaction and is durable by the time it returns,
|
||||
// unless the caller has a bracket open -- see schfs_ktx_open.
|
||||
int schfs_kcreate(uint dev, uint64 dir, const char *name,
|
||||
unsigned namelen, unsigned short type, uint mode,
|
||||
unsigned short major, unsigned short minor, uint64 *out);
|
||||
long schfs_kwrite(uint dev, uint64 ino, uint64 off, const void *buf,
|
||||
unsigned n);
|
||||
int schfs_ktruncate(uint dev, uint64 ino, uint64 size);
|
||||
int schfs_kunlink(uint dev, uint64 dir, const char *name,
|
||||
unsigned namelen);
|
||||
int schfs_klink(uint dev, uint64 dir, const char *name,
|
||||
unsigned namelen, uint64 ino);
|
||||
int schfs_krename(uint dev, uint64 olddir, const char *oldname,
|
||||
unsigned oldlen, uint64 newdir, const char *newname,
|
||||
unsigned newlen);
|
||||
int schfs_ksetattr(uint dev, uint64 ino,
|
||||
const struct schfs_inode *in);
|
||||
int schfs_ksync(uint dev);
|
||||
int schfs_ksync_all(void);
|
||||
// Share src's data blocks with dst without copying. Both stay writable; the
|
||||
// first write to either copies only the run it touches. Behind FICLONE.
|
||||
int schfs_kreflink(uint dev, uint64 src, uint64 dst);
|
||||
|
||||
// Attributes, and the index over them. See kernel/fs/schfs/attr.c.
|
||||
int schfs_kattr_set(uint dev, uint64 ino, const char *name,
|
||||
unsigned namelen, const void *val, unsigned vallen,
|
||||
unsigned short type);
|
||||
int schfs_kattr_get(uint dev, uint64 ino, const char *name,
|
||||
unsigned namelen, void *out, unsigned cap,
|
||||
unsigned *vallen);
|
||||
int schfs_kattr_del(uint dev, uint64 ino, const char *name,
|
||||
unsigned namelen);
|
||||
int schfs_kindex(uint dev, const char *name, unsigned namelen);
|
||||
int schfs_kquery(uint dev, const char *name, unsigned namelen,
|
||||
const void *val, unsigned vallen, uint64 *out, int cap);
|
||||
|
||||
// Bracket a run of changes into one transaction group. Every schfs_k* call made
|
||||
// between open and close joins it and commits once, at the close; the bracket
|
||||
// nests, and holds the volume lock for its whole extent. Costly to leave open —
|
||||
// nothing else touches that volume meanwhile — so bracket a bounded piece of
|
||||
// work such as one write(2), not a whole syscall's worth of unrelated steps.
|
||||
// schfs_ktx_close returns the commit's status, or -1 if the caller had no
|
||||
// bracket open.
|
||||
void schfs_ktx_open(uint dev);
|
||||
int schfs_ktx_close(uint dev);
|
||||
|
||||
// Volume counts for statfs. 0 if `dev` is a mounted SchFS volume (live or
|
||||
// snapshot) and the counts are filled in, -1 if it is not.
|
||||
int schfs_kstatfs(uint dev, uint64 *total, uint64 *freeblocks,
|
||||
uint64 *objects);
|
||||
|
||||
// Pieces the VFS layer (fs/schfs/vfs.c) needs, where a directory entry and the
|
||||
// object it names are separate operations. The VFS keeps the link count.
|
||||
int schfs_kdirempty(uint dev, uint64 dir);
|
||||
int schfs_kalloc_object(uint dev, unsigned short type, uint mode,
|
||||
uint64 *out);
|
||||
int schfs_kdiradd(uint dev, uint64 dir, const char *name,
|
||||
unsigned len, uint64 ino, unsigned short type);
|
||||
int schfs_kdirdel(uint dev, uint64 dir, const char *name,
|
||||
unsigned len);
|
||||
int schfs_kfree(uint dev, uint64 ino);
|
||||
|
||||
// A snapshot mounted read-only: the same block device with an older tree root.
|
||||
// The read path takes a device number so the VFS can address either view; there
|
||||
// is no write path, because a snapshot that could be written to would not be
|
||||
// one. See kernel/fs/schfs/kmount.c.
|
||||
// Returns the device number of the slot the snapshot landed in, or negative.
|
||||
int schfs_ksnap_mount(uint dev, const char *name, unsigned namelen);
|
||||
void schfs_ksnap_unmount(uint dev);
|
||||
uint64 schfs_ksnap_root(uint dev);
|
||||
|
||||
// ramdisk.c
|
||||
uint uptime(void);
|
||||
|
|
@ -236,6 +295,8 @@ int ramdisk_present(void);
|
|||
// usb_storage.c
|
||||
void usb_storage_rw(struct buf*);
|
||||
int usb_storage_present(void);
|
||||
// SCSI SYNCHRONIZE CACHE: the same barrier as ideflush, over bulk transport.
|
||||
int usb_storage_flush(void);
|
||||
void usb_storage_dump(void); // `echo s > /proc/sysrq`
|
||||
|
||||
// ioapic.c
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
#define ENOMEM 12 // Out of memory
|
||||
#define EACCES 13 // Permission denied
|
||||
#define EFAULT 14 // Bad address
|
||||
#define ENOTBLK 15 // Block device required
|
||||
#define EBUSY 16 // Device or resource busy
|
||||
#define EEXIST 17 // File exists
|
||||
#define EXDEV 18 // Cross-device link
|
||||
|
|
@ -54,6 +55,7 @@
|
|||
#define EADDRNOTAVAIL 99 // Cannot assign requested address
|
||||
#define ECONNRESET 104 // Connection reset by peer
|
||||
#define EISCONN 106 // Transport endpoint is already connected
|
||||
#define ENODATA 61 // No data available -- no such attribute
|
||||
#define ENOTCONN 107 // Transport endpoint is not connected
|
||||
#define ECONNREFUSED 111 // Connection refused
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,16 @@ struct fs_ops {
|
|||
// position that starts at 0 and is echoed back; returns bytes written, 0 at
|
||||
// the end of the directory, or negative on error.
|
||||
int (*getdents)(struct inode *dp, char *out, int cap, uint64 *off);
|
||||
|
||||
// The largest write this filesystem wants in one call, or 0 for "no limit".
|
||||
//
|
||||
// v1 needs one: a write has to fit in the on-disk log, so filewrite splits
|
||||
// anything larger and each piece is its own transaction. That limit is a
|
||||
// property of the journal, not of writing, and applying it to SchFS was
|
||||
// actively harmful -- SchFS makes one transaction out of whatever it is
|
||||
// handed, so chopping a 1 MB write into 6 KB pieces bought 170 commits and
|
||||
// the flushes that go with them for nothing.
|
||||
uint max_write;
|
||||
};
|
||||
|
||||
// In-memory inode.
|
||||
|
|
@ -82,7 +92,12 @@ struct inode {
|
|||
short type;
|
||||
short major;
|
||||
short minor;
|
||||
short nlink;
|
||||
// int, not short. SchFS stores a uint32 link count and this truncated it, so
|
||||
// a file that passed 32767 names came back with a negative one -- and iput
|
||||
// frees an object whose count reaches zero. Signed rather than unsigned on
|
||||
// purpose: `nlink--` on a count that is somehow already zero should land
|
||||
// somewhere a check can see it, not wrap to four billion.
|
||||
int nlink;
|
||||
uint64 size;
|
||||
|
||||
uint rwmode;
|
||||
|
|
@ -95,13 +110,23 @@ struct inode {
|
|||
};
|
||||
#define I_VALID 0x2
|
||||
|
||||
// Device number that means "the mounted SchFS volume". Real disks are 0 and 1,
|
||||
// and this is deliberately far from them so a stray value never looks like a
|
||||
// SchFS inode.
|
||||
#define SCHFS_DEV 64
|
||||
// Device numbers for filesystems that are not a raw disk. Real disks are 0..3,
|
||||
// and these are deliberately far from them so a stray value never looks like a
|
||||
// SchFS inode. Nothing on any disk records them -- they are runtime identities
|
||||
// for the inode cache, so the numbering can be rearranged freely.
|
||||
//
|
||||
// Mounted SchFS volumes: SCHFS_DEV through SCHFS_DEV + SCHFS_VOL_SLOTS - 1.
|
||||
//
|
||||
// This was a single number, because there was a single volume: kmount.c held
|
||||
// one `kvol` and every write entry point named it directly, so `mount -t schfs
|
||||
// /dev/hdb /mnt` ignored its source argument entirely and handed back whichever
|
||||
// disk the kernel had found at boot. Slot 0 is that boot volume, which is why
|
||||
// SCHFS_DEV alone still means "the root" everywhere that only ever wanted it.
|
||||
#define SCHFS_VOL_SLOTS 4
|
||||
#define SCHFS_DEV 64
|
||||
#define SCHFS_IS_VOL_DEV(d) \
|
||||
((d) >= SCHFS_DEV && (d) < SCHFS_DEV + SCHFS_VOL_SLOTS)
|
||||
|
||||
// Device number that means "the synthetic /proc". Like SCHFS_DEV, it is
|
||||
// deliberately nowhere near a real disk's 0 or 1.
|
||||
// Mounted snapshots. Same block device, an older tree root, read-only.
|
||||
//
|
||||
// Each needs its own device number because an inode is identified by
|
||||
|
|
@ -114,11 +139,15 @@ struct inode {
|
|||
// the cost is two scratch pages per slot, allocated on first use. Four rather
|
||||
// than many because each is a device number and they are a fixed, small space.
|
||||
#define SCHFS_SNAP_SLOTS 4
|
||||
#define SCHFS_SNAP_DEV 66
|
||||
#define SCHFS_SNAP_DEV 96
|
||||
#define SCHFS_IS_SNAP_DEV(d) \
|
||||
((d) >= SCHFS_SNAP_DEV && (d) < SCHFS_SNAP_DEV + SCHFS_SNAP_SLOTS)
|
||||
|
||||
#define PROCFS_DEV 65
|
||||
// Anything SchFS answers for, live volume or snapshot.
|
||||
#define SCHFS_IS_ANY_DEV(d) (SCHFS_IS_VOL_DEV(d) || SCHFS_IS_SNAP_DEV(d))
|
||||
|
||||
// The synthetic /proc.
|
||||
#define PROCFS_DEV 80
|
||||
|
||||
// table mapping major device number to
|
||||
// device functions
|
||||
|
|
|
|||
46
include/fsstat.h
Normal file
46
include/fsstat.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
// Counters for the filesystem and block paths, read through /proc/fsstat.
|
||||
//
|
||||
// These exist because "the system feels slow" is not a bug report anyone can
|
||||
// act on, and every guess about where the time goes in a filesystem is wrong
|
||||
// until it is measured. `ls /bin` taking three seconds could be the directory
|
||||
// walk, the tree descents, the copy path, or the disk -- and those four call
|
||||
// for completely different fixes.
|
||||
//
|
||||
// Deliberately plain `uint64` with no locking. A counter that is off by one
|
||||
// because two CPUs raced tells you the same thing as one that is exact, and a
|
||||
// lock here would change the very timings it is meant to report.
|
||||
#pragma once
|
||||
|
||||
struct fsstat {
|
||||
// Buffer cache. `bread_miss` is the one that matters: a miss is a real
|
||||
// transfer, a hit is a memcpy.
|
||||
uint64 bread_calls;
|
||||
uint64 bread_miss;
|
||||
uint64 bget_evict_dirty; // had to write a buffer out to make room
|
||||
|
||||
// Actual device transfers, and the cycles spent inside them.
|
||||
uint64 disk_reads;
|
||||
uint64 disk_writes;
|
||||
uint64 disk_cycles;
|
||||
|
||||
// The VFS read path: one call is one read(2) reaching a SchFS file.
|
||||
uint64 read_calls;
|
||||
uint64 read_bytes;
|
||||
uint64 read_cycles;
|
||||
|
||||
// Directory listing.
|
||||
uint64 getdents_calls;
|
||||
uint64 getdents_cycles;
|
||||
uint64 getdents_walked; // entries stepped over, including ones re-walked
|
||||
|
||||
// B-tree descents from the root, and the block reads they cost.
|
||||
uint64 descents;
|
||||
|
||||
// exec(2), which on this system means reading an 800 KB multicall binary.
|
||||
uint64 exec_calls;
|
||||
uint64 exec_cycles;
|
||||
};
|
||||
|
||||
extern struct fsstat fsstat;
|
||||
|
|
@ -120,6 +120,9 @@ struct rlimit {
|
|||
#define TIOCSCTTY 0x540E
|
||||
#define TIOCGSERIAL 0x541E
|
||||
#define FIONREAD 0x541B
|
||||
// FICLONE: share another file's data blocks with this one. The argument is a
|
||||
// file descriptor, not a pointer. Linux's value, so `cp --reflink` works.
|
||||
#define FICLONE 0x40049409
|
||||
#define TIOCEXCL 0x540C
|
||||
#define TIOCNXCL 0x540D
|
||||
|
||||
|
|
@ -203,6 +206,17 @@ struct sysinfo {
|
|||
char _f[8];
|
||||
};
|
||||
|
||||
// Must match Linux's x86-64 struct statfs field for field, because the programs
|
||||
// reading it were compiled against Linux's.
|
||||
//
|
||||
// f_fsid is the trap. Linux's fsid_t is `struct { int val[2]; }` -- eight bytes
|
||||
// in total, not two eight-byte words -- and declaring it as int64[2] made this
|
||||
// struct one slot too long from f_fsid onwards. Everything after it landed in
|
||||
// the wrong place: userland read f_frsize out of f_namelen and got 255, so df
|
||||
// computed its block counts in units of 255 bytes and reported a 32 MB root
|
||||
// filesystem as 1992 KB. It went unnoticed for as long as it did because every
|
||||
// field here used to be an invented constant, so the numbers were meaningless
|
||||
// whichever slot they came from.
|
||||
struct statfs {
|
||||
int64 f_type;
|
||||
int64 f_bsize;
|
||||
|
|
@ -211,7 +225,7 @@ struct statfs {
|
|||
int64 f_bavail;
|
||||
int64 f_files;
|
||||
int64 f_ffree;
|
||||
int64 f_fsid[2];
|
||||
int32 f_fsid[2];
|
||||
int64 f_namelen;
|
||||
int64 f_frsize;
|
||||
int64 f_flags;
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@
|
|||
#define SCHFS_KEY_INDEX 5 // (idxid, INDEX, valuehash) -> objectid
|
||||
#define SCHFS_KEY_CSUM 6 // (CSUM, CSUM, diskblk) -> uint32 CRC32c
|
||||
#define SCHFS_KEY_SNAP 7 // (SNAP, SNAP, id) -> struct schfs_snap
|
||||
#define SCHFS_KEY_EXTREF 8 // (EXTREF,EXTREF, diskblk) -> struct schfs_extref
|
||||
|
||||
// Data-block checksums live under one reserved object id, exactly as btrfs
|
||||
// keeps a checksum tree. Tree nodes carry their checksum in their parent
|
||||
|
|
@ -52,6 +53,16 @@
|
|||
// B-tree is copy-on-write, so the blocks a root reaches are already immutable
|
||||
// as long as nothing frees them. Keeping one costs a record.
|
||||
#define SCHFS_SNAP_OBJECTID 2ULL
|
||||
// Shared extents, keyed by the disk block a run starts at. btrfs calls this the
|
||||
// extent tree and so, in effect, is this -- one reserved object id whose records
|
||||
// say how many files point at a run.
|
||||
//
|
||||
// Only *shared* runs have a record. A run nothing has cloned has no entry at
|
||||
// all, and absence means a reference count of one. That is not a space
|
||||
// optimisation so much as a correctness one: the overwhelmingly common case
|
||||
// stays exactly as it was, so the write path pays nothing for a feature it is
|
||||
// not using, and there is no second structure to keep in step on every write.
|
||||
#define SCHFS_EXTREF_OBJECTID 3ULL
|
||||
|
||||
// Object ids below this are reserved; the root directory is the first real one.
|
||||
#define SCHFS_FIRST_OBJECTID 16ULL
|
||||
|
|
@ -225,10 +236,25 @@ struct schfs_extent {
|
|||
// 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;
|
||||
// Whether this run is shared, as far as *this* file knows. A hint, not the
|
||||
// truth: the count that decides whether blocks may be released lives in the
|
||||
// extent-reference tree (SCHFS_KEY_EXTREF), because a per-file copy cannot
|
||||
// be decremented by the other file that shares the run.
|
||||
//
|
||||
// Kept because it makes the common case free: refs == 1 here means nothing
|
||||
// has ever cloned this run, so the write path can rewrite it in place
|
||||
// without consulting the tree at all.
|
||||
uint32_t refs;
|
||||
uint32_t pad;
|
||||
};
|
||||
|
||||
// How many files point at a run of blocks. Present only for runs with more than
|
||||
// one reference; see SCHFS_EXTREF_OBJECTID.
|
||||
struct schfs_extref {
|
||||
uint64_t nblocks;
|
||||
uint64_t refs;
|
||||
};
|
||||
|
||||
// Directory entry payload. The key's offset field holds the name hash, so a
|
||||
// lookup is one tree descent rather than a directory scan.
|
||||
//
|
||||
|
|
@ -248,6 +274,71 @@ struct schfs_dirent_rec {
|
|||
|
||||
#define SCHFS_NAME_MAX 255
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Attributes, and the index over them
|
||||
//
|
||||
// An attribute is a named value on an object -- the same thing Linux calls an
|
||||
// extended attribute, and the thing BeFS built its query interface on. Stored
|
||||
// under (ino, ATTR, namehash), packed exactly as directory entries are, and for
|
||||
// the same reason: two names can hash alike, and a filesystem that loses one of
|
||||
// them under a collision is not one to build on.
|
||||
//
|
||||
// The point of having them is the *index*. An attribute may be indexed, and an
|
||||
// indexed attribute gets a second set of records keyed by its value rather than
|
||||
// by the object -- so "every object whose `type` is `photo`" is a range scan of
|
||||
// a few keys rather than a walk of every object in the filesystem. That is the
|
||||
// whole of what "a filesystem you can query" means here, and the reason the
|
||||
// design is one B-tree with many key types: the feature is a few hundred lines
|
||||
// on machinery that already exists, rather than a second engine.
|
||||
struct schfs_attr_rec {
|
||||
uint32_t namelen;
|
||||
uint32_t valuelen;
|
||||
uint16_t type; // SCHFS_ATTR_* below; what a query compares as
|
||||
uint16_t flags;
|
||||
uint32_t pad;
|
||||
/* name bytes, then value bytes; the pair padded to an 8-byte boundary */
|
||||
};
|
||||
|
||||
// What a value *is*, so that a query can compare two of them meaningfully. A
|
||||
// string and an integer that happen to share bytes are not the same value, and
|
||||
// ordering differs between them.
|
||||
#define SCHFS_ATTR_RAW 0 // opaque bytes; only equality is meaningful
|
||||
#define SCHFS_ATTR_STRING 1
|
||||
#define SCHFS_ATTR_INT 2 // int64, little-endian
|
||||
#define SCHFS_ATTR_TIME 3 // seconds since the epoch, int64
|
||||
|
||||
#define SCHFS_ATTR_NAME_MAX 63
|
||||
#define SCHFS_ATTR_VALUE_MAX 255
|
||||
|
||||
// Step from one packed attribute record to the next. 8-byte aligned so the
|
||||
// uint32s of the next one are naturally aligned.
|
||||
#define SCHFS_ATTR_LEN(namelen, valuelen) \
|
||||
((unsigned)((sizeof(struct schfs_attr_rec) + (namelen) + (valuelen) + 7) & ~7u))
|
||||
|
||||
// An index entry: one object that carries a given value for a given attribute.
|
||||
//
|
||||
// Keyed (indexid, INDEX, valuehash), where indexid is the hash of the attribute
|
||||
// *name*. Several objects can share a value, so the payload is a packed list of
|
||||
// these, exactly as dirents and attributes are.
|
||||
struct schfs_index_rec {
|
||||
uint64_t objectid;
|
||||
uint32_t valuelen;
|
||||
uint32_t pad;
|
||||
/* value bytes follow, so a query can reject a hash collision */
|
||||
};
|
||||
|
||||
#define SCHFS_INDEX_LEN(valuelen) \
|
||||
((unsigned)((sizeof(struct schfs_index_rec) + (valuelen) + 7) & ~7u))
|
||||
|
||||
// Which attributes are indexed. One record, under the index object id, whose
|
||||
// value is a packed list of attribute names.
|
||||
//
|
||||
// A list rather than a flag on each attribute, because the question asked at
|
||||
// every setxattr is "is *this name* indexed", and answering it from one small
|
||||
// record is a single cached lookup rather than a search.
|
||||
#define SCHFS_INDEX_OBJECTID 4ULL
|
||||
#define SCHFS_INDEX_LIST_OFF 0ULL /* the one record's offset in the key */
|
||||
|
||||
// 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) \
|
||||
|
|
@ -274,5 +365,8 @@ 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);
|
||||
SCHFS_ASSERT_SIZE(schfs_extref, 16);
|
||||
SCHFS_ASSERT_SIZE(schfs_attr_rec, 16);
|
||||
SCHFS_ASSERT_SIZE(schfs_index_rec, 16);
|
||||
|
||||
#endif /* SCHFS_H */
|
||||
|
|
|
|||
|
|
@ -64,6 +64,20 @@ int geteuid(void);
|
|||
int nanosleep(void*);
|
||||
int mprotect(void*, uint64, int);
|
||||
int ioctl(int, uint64, void*);
|
||||
|
||||
// Attributes on a file, and the index over them. Only SchFS carries these;
|
||||
// anything else answers EOPNOTSUPP.
|
||||
//
|
||||
// schfs_query returns how many object ids it wrote, or -1 if the attribute has
|
||||
// no index -- it will not fall back to walking the filesystem, because a query
|
||||
// that silently becomes a full scan is worse than one that says it cannot help.
|
||||
int setxattr(const char *path, const char *name, const void *val, int size,
|
||||
int flags);
|
||||
int getxattr(const char *path, const char *name, void *val, int size);
|
||||
int removexattr(const char *path, const char *name);
|
||||
int schfs_index(const char *name);
|
||||
int schfs_query(const char *name, const char *value,
|
||||
unsigned long long *out, int cap);
|
||||
char* getcwd(char*, int);
|
||||
int arch_prctl(int, uint64);
|
||||
int set_tid_address(int*);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,18 @@ inb(ushort port)
|
|||
return data;
|
||||
}
|
||||
|
||||
// Cycle counter. Not a clock -- it does not convert to time without knowing the
|
||||
// frequency, and it is not synchronised across cores -- but for "which of these
|
||||
// two paths costs more" it is exactly the right instrument, and the only one
|
||||
// with fine enough resolution to see inside a single syscall.
|
||||
static inline uint64
|
||||
rdtsc(void)
|
||||
{
|
||||
uint lo, hi;
|
||||
asm volatile("rdtsc" : "=a" (lo), "=d" (hi));
|
||||
return ((uint64)hi << 32) | lo;
|
||||
}
|
||||
|
||||
static inline void
|
||||
insl(int port, void *addr, int cnt)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue