SchOS/include/fs/file.h
Nexoniarz fd9d392416 Pseudo-terminals, and what tmux and htop needed around them
- /dev/ptmx and /dev/pts/N: Unix98 pairs with a full line discipline,
  window sizes, job control and hangup (kernel/dev/pty.c).
- The console speaks scroll regions, insert/delete line, SU/SD, RI/IND/NEL,
  DECSC/DECRC; swallows charset designations and private CSI sequences;
  and LF only returns the carriage under ONLCR.
- setsid() drops the controlling terminal, so a pane's pty becomes its ctty.
- pipe2() honours O_NONBLOCK and O_CLOEXEC (libevent's signal pipe).
- File modes are stored and enforced: chmod, fchmod(at), umask, creation
  modes; /tmp is 1777.
- /proc/stat has real user/system/idle; /proc/<pid>/task; kthread flag.
- SO_PEERCRED, IOV_MAX 1024.
- /proc/strace: name a program and its syscalls go to the kernel log.
- World gains htop, btop, tmux, mc, ncdu, fastfetch, games, X toys, NetSurf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-25 02:11:10 +01:00

253 lines
11 KiB
C

struct file {
// FD_MEMFD: an anonymous shared memory object, the thing memfd_create(2)
// returns. It is a descriptor with no name and no inode behind it, which is
// the entire reason memfd exists -- two processes share memory by passing
// the descriptor, not by agreeing on a path or a System V key.
//
// FD_EVENT: an eventfd, timerfd, signalfd or epoll set -- see
// kernel/sys/sys_event.c, which is the only file that looks inside `ev`.
enum { FD_NONE, FD_PIPE, FD_INODE, FD_SOCKET, FD_MEMFD, FD_EVENT } type;
int ref; // reference count
char readable;
char writable;
// O_NONBLOCK. A property of the open file description, not of the
// descriptor, which is why it lives here and not in myproc()->ofile_flags --
// dup() shares it and F_SETFL on either fd changes it for both, exactly as
// POSIX says.
char nonblock;
struct pipe *pipe;
struct inode *ip;
struct socket *sock;
// The shared segment behind an FD_MEMFD, as an index into the table in
// kernel/sys/sys_mem.c. Not a pointer, because the segment table is where
// the reference counting lives and handing out pointers into it would
// invite somebody to keep one.
int memfd_seg;
struct evobj *ev;
uint64 off;
// What /proc/<pid>/fd shows. `ino` names the open thing (one per socket,
// one per pipe); `path` is the absolute path an inode was opened by, as
// written -- symlinks along it are not resolved. kmalloc'd, or 0.
uint64 ino;
char *path;
struct file *fnext; // file.c's free list, while ref is 0
};
// ---------------------------------------------------------------------------
// Filesystem operations
//
// One table per filesystem, chosen by iget() from the device number and hung
// off the inode. Everything above this line -- the syscall layer, exec, the
// page cache -- calls the wrappers in fs.c and never learns which filesystem it
// is talking to.
//
// A vtable rather than a chain of `if (dev == SCHFS_DEV)` because there are two
// filesystems today and there will be more: the whole point of the exercise is
// that adding one is a new table, not another branch in fifteen functions.
//
// Every hook takes an inode that is already locked, except ialloc (which has no
// inode yet) and iload (which is what makes one valid in the first place).
// ---------------------------------------------------------------------------
struct stat;
struct fs_ops {
const char *name;
// Fill in the type/size/link fields of a freshly iget()'d inode.
void (*iload)(struct inode *);
// Write those fields back to disk.
void (*istore)(struct inode *);
// The last link is gone and nobody has it open: release the object.
void (*ifree)(struct inode *);
int (*read)(struct inode *, int user_dst, uint64 dst, uint64 off, uint n);
int (*write)(struct inode *, int user_src, uint64 src, uint64 off, uint n);
// Set the length. Growing is allowed only where the filesystem can represent
// the gap; v1 cannot and ignores it.
void (*truncate)(struct inode *, uint64 newsize);
// Directory operations. `poff` is an opaque cookie identifying where the
// entry lives, meaningful only to the same filesystem's dirunlink.
struct inode *(*dirlookup)(struct inode *dp, char *name, uint *poff);
int (*dirlink)(struct inode *dp, char *name, uint64 inum);
int (*dirunlink)(struct inode *dp, char *name, uint off);
int (*dirempty)(struct inode *dp);
// Create an object with no name yet. The caller sets nlink and calls
// dirlink; until it does, the object is unreachable.
struct inode *(*ialloc)(uint dev, short type);
// Read directory entries into a getdents64-shaped stream. `off` is an opaque
// 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;
// Object types this filesystem has no way to store, as a mask of 1 << T_*.
// Creating one fails with EPERM, which is what Linux answers for mknod or a
// symlink on vfat. 0 for "anything".
uint refuse_types;
// One name per file: link() fails with EPERM. Rename still works -- it is
// a second name for as long as it takes to remove the first.
int no_hardlinks;
};
// In-memory inode.
//
// `inum` and `size` are 64-bit because the filesystem underneath may be: SchFS
// object ids and file sizes are both uint64 on disk, and a 32-bit copy here
// would silently truncate them into a different file. The v1 filesystem uses a
// small fraction of the range and is unaffected.
struct inode {
uint dev;
uint64 inum;
int ref;
struct sleeplock lock;
int valid;
const struct fs_ops *ops; // chosen by iget from dev; never null once held
struct inode *hnext; // fs.c: hash chain while held, free list after
struct inode *anext; // fs.c: every inode there is, never reordered
short type;
short major;
short minor;
// 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;
// POSIX permission bits from a filesystem that keeps them, or 0 for one
// that does not, in which case stat makes some up from the type. Bit 18 set
// alone means a mode of 0000, which chmod can ask for.
uint mode;
uint supermode;
uint showmode;
uint atime;
uint mtime;
uint ctime;
uint addrs[NDIRECT+3];
};
#define I_VALID 0x2
// 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.
// Sixteen, which fills 64..79 up to PROCFS_DEV.
#define SCHFS_VOL_SLOTS 16
#define SCHFS_DEV 64
#define SCHFS_IS_VOL_DEV(d) \
((d) >= SCHFS_DEV && (d) < SCHFS_DEV + SCHFS_VOL_SLOTS)
// Mounted snapshots. Same block device, an older tree root, read-only.
//
// Each needs its own device number because an inode is identified by
// (dev, inum) and a snapshot's object ids are the *same numbers* as the live
// tree's -- one number would make the views alias in the inode cache and hand
// a reader whichever was cached first. So a slot is a device: SCHFS_SNAP_DEV
// through SCHFS_SNAP_DEV + SCHFS_SNAP_SLOTS - 1.
//
// More than one because "one at a time" is an odd thing to explain and
// the cost is two scratch pages per slot, allocated on first use. Not many
// because each is a device number and they are a fixed, small space.
#define SCHFS_SNAP_SLOTS 8 // 96..103, up to FAT_DEV
#define SCHFS_SNAP_DEV 96
#define SCHFS_IS_SNAP_DEV(d) \
((d) >= SCHFS_SNAP_DEV && (d) < SCHFS_SNAP_DEV + SCHFS_SNAP_SLOTS)
// 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
// The synthetic /dev. See kernel/fs/devfs.c.
#define DEVFS_DEV 81
// The synthetic /sys. See kernel/fs/sysfs.c.
#define SYSFS_DEV 82
// Graft it over `target`; 0 or a negative errno. In sysfs.c.
int sysfs_mount(const char *target);
// The buffer /proc renders a file into, and how big it is; one at a time, so
// every get is followed by a put. In kernel/dev/procfs.c.
char *proc_genbuf_get(int *cap);
void proc_genbuf_put(void);
// Mounted FAT volumes, FAT_DEV onward. See include/fs/fat.h.
// Linux's limits, for every filesystem behind the VFS. A path component can be
// NAME_MAX bytes and a whole path PATH_MAX including its NUL. Each filesystem
// may refuse less: SchFS and FAT take 255, the v1 format DIRSIZ.
//
// The VFS used to copy every component into a DIRSIZ buffer and every path
// into a MAXPATH one, which cut names at 29 bytes and paths at 127 without a
// word -- so a longer name opened whichever file shared its first 29 bytes.
#define VFS_NAME_MAX 255
#define VFS_PATH_MAX 4096 // one page: namex_from() kalloc()s it
// 1 if some inode on dev with this number is referenced. In fs.c.
int iheld(uint dev, uint64 inum);
// 1 if any inode on dev but `except` is referenced (0: no exception). In fs.c.
int ibusy(uint dev, uint64 except);
// How many in-memory inodes exist; the table grows past NINODE. In fs.c.
uint inode_slots(void);
// How many open file descriptions exist; the table grows past NFILE. file.c.
uint file_slots(void);
// Path lookup that follows a final symlink too; 0 with *err set on failure.
// `start` 0 means the cwd. In fs.c.
struct inode *nameifollow(struct inode *start, char *path, int *err);
// execve(2) with the argv and envp arrays still in the caller's memory. In
// exec.c; exec() is the same for arrays the kernel holds.
int exec_user(char *path, addr_t uargv, addr_t uenvp);
// table mapping major device number to
// device functions
struct devsw {
int (*read)(struct inode*, uint64, char*, int);
int (*write)(struct inode*, uint64, char*, int);
// Optional. Returns 0 when a read would block, which is the only thing
// fileread needs to know to honour O_NONBLOCK without every device driver
// growing a flags argument. A device that omits this is treated as always
// ready, which is the right answer for a disk and for /dev/zero.
int (*ready)(struct inode*);
// Optional. The last descriptor for this open file is going away. A device
// that hands a program exclusive-feeling control of the hardware needs this
// to put the hardware back -- /dev/fb0 uses it to give the text console its
// screen back when whatever was drawing on it exits.
void (*close)(struct inode*);
// Optional. A descriptor has just been opened on this device. /dev/input
// uses it to discard events that arrived while nobody was listening -- see
// inputopen().
void (*open)(struct inode*);
};
extern struct devsw devsw[];
#define CONSOLE 1
// Major number of /dev/input/eventN. The minor is the device index.
#define INPUT_MAJOR 7
// Major number of /dev/dsp. See include/dev/audio.h.
#define AUDIO_MAJOR 12