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>
297 lines
7.5 KiB
C
297 lines
7.5 KiB
C
#include "types.h"
|
|
#include "defs.h"
|
|
#include "stat.h"
|
|
#include "param.h"
|
|
#include "fs.h"
|
|
#include "spinlock.h"
|
|
#include "sleeplock.h"
|
|
#include "file.h"
|
|
#include "posix.h"
|
|
#include "errno.h"
|
|
|
|
extern struct devsw devsw[];
|
|
|
|
struct {
|
|
struct spinlock lock;
|
|
struct file file[NFILE];
|
|
} ftable;
|
|
|
|
void
|
|
fileinit(void)
|
|
{
|
|
initlock(&ftable.lock, "ftable");
|
|
}
|
|
|
|
struct file*
|
|
filealloc(void)
|
|
{
|
|
struct file *f;
|
|
|
|
acquire(&ftable.lock);
|
|
for(f = ftable.file; f < ftable.file + NFILE; f++){
|
|
if(f->ref == 0){
|
|
f->ref = 1;
|
|
// Cleared here rather than left to the caller: every creator sets
|
|
// readable/writable/off, but only open() sets nonblock, so a struct file
|
|
// recycled from a non-blocking device would hand its flag to the next
|
|
// pipe or socket that happened to land in this slot.
|
|
f->nonblock = 0;
|
|
release(&ftable.lock);
|
|
return f;
|
|
}
|
|
}
|
|
release(&ftable.lock);
|
|
return 0;
|
|
}
|
|
|
|
struct file*
|
|
filedup(struct file *f)
|
|
{
|
|
acquire(&ftable.lock);
|
|
if(f->ref < 1)
|
|
panic("filedup");
|
|
f->ref++;
|
|
release(&ftable.lock);
|
|
return f;
|
|
}
|
|
|
|
void
|
|
fileclose(struct file *f)
|
|
{
|
|
struct file ff;
|
|
|
|
acquire(&ftable.lock);
|
|
if(f->ref < 1)
|
|
panic("fileclose");
|
|
if(--f->ref > 0){
|
|
release(&ftable.lock);
|
|
return;
|
|
}
|
|
ff = *f;
|
|
f->ref = 0;
|
|
f->type = FD_NONE;
|
|
release(&ftable.lock);
|
|
|
|
if(ff.type == FD_PIPE)
|
|
pipeclose(ff.pipe, ff.writable);
|
|
else if(ff.type == FD_SOCKET)
|
|
sockclose(ff.sock);
|
|
else if(ff.type == FD_INODE){
|
|
// Give a device a chance to put the hardware back before the inode goes.
|
|
// Done here rather than in exit() because a program that closes the
|
|
// framebuffer without exiting has finished with it just as thoroughly.
|
|
if(ff.ip && ff.ip->type == T_DEV && ff.ip->major >= 0 &&
|
|
ff.ip->major < NDEV && devsw[ff.ip->major].close)
|
|
devsw[ff.ip->major].close(ff.ip);
|
|
|
|
begin_op();
|
|
iput(ff.ip);
|
|
end_op();
|
|
}
|
|
}
|
|
|
|
int
|
|
filestat(struct file *f, struct stat *st)
|
|
{
|
|
if(f->type == FD_INODE){
|
|
ilock(f->ip);
|
|
stati(f->ip, st);
|
|
iunlock(f->ip);
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int
|
|
fileread(struct file *f, char *addr, int n)
|
|
{
|
|
int r;
|
|
|
|
if(f->readable == 0)
|
|
return -1;
|
|
|
|
// O_NONBLOCK on a pipe or a socket. Reported here rather than inside
|
|
// piperead/sockread so that the blocking implementations stay unchanged:
|
|
// this is a check of the same condition their sleep loops wait on, made one
|
|
// level up. Writes still block -- fcntl documents that.
|
|
if(f->type == FD_PIPE){
|
|
if(f->nonblock && !(pipe_poll(f->pipe, f->readable, f->writable) & POLLIN))
|
|
return -EAGAIN;
|
|
return piperead(f->pipe, addr, n);
|
|
}
|
|
|
|
if(f->type == FD_SOCKET){
|
|
if(f->nonblock && !sockreadable(f->sock))
|
|
return -EAGAIN;
|
|
return sockread(f->sock, addr, n);
|
|
}
|
|
|
|
if(f->type == FD_INODE){
|
|
ilock(f->ip);
|
|
if(f->ip->type == 3){
|
|
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].read){
|
|
iunlock(f->ip);
|
|
return -1;
|
|
}
|
|
|
|
// O_NONBLOCK. Without this a game that opens an input device
|
|
// non-blocking and drains it once per frame stops dead on the first
|
|
// frame -- it blocks in read() waiting for a key, and the result looks
|
|
// exactly like a hung renderer rather than a blocking read.
|
|
if(f->nonblock && devsw[f->ip->major].ready &&
|
|
!devsw[f->ip->major].ready(f->ip)){
|
|
iunlock(f->ip);
|
|
return -EAGAIN;
|
|
}
|
|
|
|
r = devsw[f->ip->major].read(f->ip, f->off, addr, n);
|
|
if(r > 0)
|
|
f->off += r;
|
|
iunlock(f->ip);
|
|
return r;
|
|
}
|
|
|
|
if((f->ip->rwmode & 2) == 0){
|
|
cprintf("SchOS: permission denied (read)\n");
|
|
iunlock(f->ip);
|
|
return -1;
|
|
}
|
|
|
|
// The "recycle bin" check that used to sit here is gone.
|
|
//
|
|
// It made a file that still existed, still had a name and still had blocks
|
|
// refuse every read with a printf -- a state no POSIX program is prepared
|
|
// for. It was reachable only through `delete`, a NO_LINUX syscall from the
|
|
// third-party filesystem this file derives from, which the decision to keep
|
|
// only Linux numbering had already orphaned.
|
|
//
|
|
// The on-disk `showmode` field stays for now: removing it changes the inode
|
|
// layout, and this whole file is being replaced by SchFS. It is simply
|
|
// never consulted.
|
|
|
|
if((r = readi(f->ip, 0, (uint64)addr, f->off, n)) > 0)
|
|
f->off += r;
|
|
iunlock(f->ip);
|
|
return r;
|
|
}
|
|
|
|
panic("fileread");
|
|
}
|
|
|
|
int
|
|
filewrite(struct file *f, char *addr, int n)
|
|
{
|
|
int r;
|
|
|
|
if(f->writable == 0)
|
|
return -1;
|
|
|
|
if(f->type == FD_PIPE)
|
|
return pipewrite(f->pipe, addr, n);
|
|
|
|
if(f->type == FD_SOCKET)
|
|
return sockwrite(f->sock, addr, n);
|
|
|
|
if(f->type == FD_INODE){
|
|
ilock(f->ip);
|
|
|
|
if(f->ip->type == 3){
|
|
if(f->ip->major < 0 || f->ip->major >= NDEV || !devsw[f->ip->major].write){
|
|
iunlock(f->ip);
|
|
return -1;
|
|
}
|
|
r = devsw[f->ip->major].write(f->ip, f->off, addr, n);
|
|
if(r > 0)
|
|
f->off += r;
|
|
iunlock(f->ip);
|
|
return r;
|
|
}
|
|
|
|
if((f->ip->rwmode & 1) == 0){
|
|
cprintf("SchOS: permission denied (write)\n");
|
|
iunlock(f->ip);
|
|
return -1;
|
|
}
|
|
|
|
// Recycle-bin check removed here too; see the note in fileread().
|
|
iunlock(f->ip);
|
|
|
|
// How much may go in one call is the filesystem's business. v1 is bounded
|
|
// by its log; SchFS is not bounded at all and would rather have the lot,
|
|
// because it makes one transaction out of whatever it is given.
|
|
uint max = f->ip->ops->max_write;
|
|
int i = 0;
|
|
while(i < n){
|
|
int n1 = n - i;
|
|
if(max && (uint)n1 > max)
|
|
n1 = (int)max;
|
|
|
|
begin_op();
|
|
ilock(f->ip);
|
|
if ((r = writei(f->ip, 1, (uint64)(addr + i), f->off, n1)) > 0)
|
|
f->off += r;
|
|
|
|
iunlock(f->ip);
|
|
end_op();
|
|
|
|
if(r < 0)
|
|
break;
|
|
if(r != n1)
|
|
panic("short filewrite");
|
|
i += r;
|
|
}
|
|
return i == n ? n : -1;
|
|
}
|
|
panic("filewrite");
|
|
}
|
|
|
|
// poll(2) readiness for one open file. Returns a POLL* bitmask restricted
|
|
// to the requested events (plus error/hangup conditions, which POSIX says
|
|
// are always reported). Pipes and the console tty have real state; other
|
|
// inodes (regular files, block-ish devices) are always ready, per POSIX.
|
|
int
|
|
file_poll(struct file *f, int events)
|
|
{
|
|
if(f->type == FD_PIPE)
|
|
return pipe_poll(f->pipe, f->readable, f->writable) &
|
|
(events | POLLERR | POLLHUP);
|
|
|
|
if(f->type == FD_SOCKET){
|
|
int rev = 0;
|
|
if(sockreadable(f->sock))
|
|
rev |= POLLIN;
|
|
// A stream socket is writable whenever it has a peer. The buffer can
|
|
// still be full, in which case the write blocks -- same latitude poll()
|
|
// has always had, and the same answer a pipe gives.
|
|
if(sock_hup(f->sock))
|
|
rev |= POLLHUP;
|
|
else
|
|
rev |= POLLOUT;
|
|
return rev & (events | POLLERR | POLLHUP);
|
|
}
|
|
|
|
if(f->type == FD_INODE){
|
|
// An input device is only readable when an event is waiting. Reporting it
|
|
// always-ready -- which is what the generic answer below does -- turns a
|
|
// poll loop into a busy loop that blocks in read().
|
|
if(f->ip && f->ip->type == T_DEV && f->ip->major == INPUT_MAJOR){
|
|
int rev = 0;
|
|
if((events & POLLIN) && f->readable && input_ready(f->ip->minor))
|
|
rev |= POLLIN;
|
|
if(events & POLLOUT)
|
|
rev |= POLLOUT;
|
|
return rev;
|
|
}
|
|
|
|
if(f->ip && f->ip->type == T_DEV && f->ip->major == CONSOLE){
|
|
int rev = 0;
|
|
if((events & POLLIN) && f->readable && console_input_ready())
|
|
rev |= POLLIN;
|
|
if((events & POLLOUT) && f->writable)
|
|
rev |= POLLOUT;
|
|
return rev;
|
|
}
|
|
return events & (POLLIN | POLLOUT);
|
|
}
|
|
return POLLNVAL;
|
|
}
|