There were none: fcntl answered F_SETLK with EINVAL and flock was not a system call. Alpine's apk, which runs unmodified on SchOS through Alpine's own musl, locks its database before reading it and stopped there. kernel/fs/lock.c keeps one table of advisory locks keyed by (dev, inum): flock() for the whole file, owned by the open file description; F_SETLK/F_SETLKW/F_GETLK for byte ranges, owned by the process and released when it closes any descriptor on the file or exits; and the F_OFD_* forms, ranges owned by the description. flock and record locks do not see each other, as on Linux. Waiters sleep until any release and are interrupted by a signal. cmd/locktest checks each between a parent and a child, including range splitting, F_GETLK's report, and a blocked waiter being woken. shell.nix gains pkg-config, meson, ninja, m4, bison, flex and xz, for the X ports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
393 lines
11 KiB
C
393 lines
11 KiB
C
#include "core/types.h"
|
|
#include "core/defs.h"
|
|
#include "uapi/stat.h"
|
|
#include "core/param.h"
|
|
#include "fs/fs.h"
|
|
#include "core/spinlock.h"
|
|
#include "core/sleeplock.h"
|
|
#include "fs/file.h"
|
|
#include "fs/fsstat.h"
|
|
#include "uapi/posix.h"
|
|
#include "uapi/errno.h"
|
|
#include "core/kmem.h"
|
|
|
|
extern struct devsw devsw[];
|
|
|
|
// Open file descriptions, shared by every process. NFILE of them in the
|
|
// kernel image and more from a slab cache once those are all open; a closed
|
|
// one goes on the free list. This was NFILE and then ENFILE, which a few
|
|
// processes each holding a hundred descriptors -- an editor, a build, a
|
|
// shell -- were already a third of the way to.
|
|
struct {
|
|
struct spinlock lock;
|
|
struct file file[NFILE];
|
|
struct file *free;
|
|
struct kmem_cache *more;
|
|
uint n;
|
|
} ftable;
|
|
|
|
void
|
|
fileinit(void)
|
|
{
|
|
initlock(&ftable.lock, "ftable");
|
|
for(int i = NFILE - 1; i >= 0; i--){
|
|
ftable.file[i].fnext = ftable.free;
|
|
ftable.free = &ftable.file[i];
|
|
}
|
|
ftable.n = NFILE;
|
|
}
|
|
|
|
uint
|
|
file_slots(void)
|
|
{
|
|
return ftable.n;
|
|
}
|
|
|
|
struct file*
|
|
filealloc(void)
|
|
{
|
|
struct file *f;
|
|
|
|
acquire(&ftable.lock);
|
|
if((f = ftable.free) != 0){
|
|
ftable.free = f->fnext;
|
|
} else {
|
|
// The slab allocator takes only its own lock and kalloc's.
|
|
if(ftable.more == 0)
|
|
ftable.more = kmem_cache_create("file", sizeof(struct file));
|
|
if(ftable.more == 0 || (f = kmem_cache_alloc(ftable.more)) == 0){
|
|
release(&ftable.lock);
|
|
return 0;
|
|
}
|
|
memset(f, 0, sizeof *f);
|
|
ftable.n++;
|
|
}
|
|
f->ref = 1;
|
|
FSSTAT_TAKE(fsstat.file_used, fsstat.file_peak); // 0 -> 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
// Its flock() and OFD locks go with it, before the slot can be handed to
|
|
// someone else: they are keyed by this address.
|
|
lock_close_file(f);
|
|
ff = *f;
|
|
f->ref = 0;
|
|
f->type = FD_NONE;
|
|
f->fnext = ftable.free;
|
|
ftable.free = f;
|
|
FSSTAT_DROP(fsstat.file_used); // 1 -> 0
|
|
release(&ftable.lock);
|
|
|
|
if(ff.type == FD_MEMFD)
|
|
shm_detach(ff.memfd_seg); // the last mapper frees the pages
|
|
else if(ff.type == FD_EVENT)
|
|
ev_close(ff.ev);
|
|
else 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)
|
|
{
|
|
// A memfd has no inode, so the size is the only field with an answer. It is
|
|
// the field that matters: mmap length and ftruncate round-trips are checked
|
|
// against it, and a caller that fstat()s to find out how big the buffer is
|
|
// gets the truth.
|
|
if(f->type == FD_MEMFD){
|
|
memset(st, 0, sizeof(*st));
|
|
st->type = T_FILE;
|
|
st->size = memfd_size(f->memfd_seg);
|
|
st->nlink = 1;
|
|
return 0;
|
|
}
|
|
if(f->type == FD_INODE){
|
|
ilock(f->ip);
|
|
stati(f->ip, st);
|
|
iunlock(f->ip);
|
|
return 0;
|
|
}
|
|
// An eventfd, timerfd, signalfd or epoll set: Linux calls these anonymous
|
|
// inodes, and fstat succeeds on them with nothing much to say. A program
|
|
// that fstat()s every descriptor it is handed should not fail on one.
|
|
if(f->type == FD_EVENT){
|
|
memset(st, 0, sizeof(*st));
|
|
st->nlink = 1;
|
|
return 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int
|
|
fileread(struct file *f, char *addr, int n)
|
|
{
|
|
int r;
|
|
|
|
if(f->readable == 0)
|
|
return -1;
|
|
|
|
if(f->type == FD_EVENT)
|
|
return ev_read(f, addr, n);
|
|
|
|
// A memfd is a file: read() and write() move bytes in and out of the shared
|
|
// pages, and the descriptor carries its own offset like any other. mmap is
|
|
// what it is usually for, but a descriptor that refuses read() is a surprise
|
|
// nobody needs.
|
|
if(f->type == FD_MEMFD){
|
|
r = memfd_rw(f->memfd_seg, 1, (uint64)addr, f->off, n, 0);
|
|
if(r > 0)
|
|
f->off += r;
|
|
return r;
|
|
}
|
|
|
|
// 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){
|
|
// POLLHUP counts as readable here, and leaving it out was a bug: a pipe
|
|
// whose writers have all gone is not "nothing yet", it is end of file, and
|
|
// the answer to a read is 0. Told EAGAIN instead, a caller that polls and
|
|
// reads spins forever waiting for an end that has already happened.
|
|
// sockreadable() has always included the equivalent condition.
|
|
if(f->nonblock &&
|
|
!(pipe_poll(f->pipe, f->readable, f->writable) & (POLLIN | POLLHUP)))
|
|
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_EVENT)
|
|
return ev_write(f, addr, n);
|
|
|
|
if(f->type == FD_MEMFD){
|
|
r = memfd_rw(f->memfd_seg, 1, (uint64)addr, f->off, n, 1);
|
|
if(r > 0)
|
|
f->off += r;
|
|
return r;
|
|
}
|
|
|
|
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;
|
|
i += r;
|
|
// Short means full. v1 and SchFS never wrote short, which is why this
|
|
// was a panic; FAT does, when the volume or its root directory fills.
|
|
// write(2) reports what went in, and the next call gets the error.
|
|
if(r != n1)
|
|
break;
|
|
}
|
|
if(i > 0)
|
|
return i;
|
|
return r < 0 ? r : -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_EVENT)
|
|
return ev_poll(f, 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(f->ip ? f->ip->minor : 1))
|
|
rev |= POLLIN;
|
|
if((events & POLLOUT) && f->writable)
|
|
rev |= POLLOUT;
|
|
return rev;
|
|
}
|
|
return events & (POLLIN | POLLOUT);
|
|
}
|
|
return POLLNVAL;
|
|
}
|