docs: four directories, and a rule for when a thing leaves the roadmap
docs/ was ten files where a plan, a description, a guide and a bug report all
sat next to each other. It is now architecture/ (what SchOS is), plan/ (what it
will be), guides/ (how to work on it) and history/ (why it is like this), with
an index that states the one rule holding it together: when something is
finished it moves from plan/ to architecture/ and is rewritten as a description
of the thing rather than a record of building it.
A ticked checkbox says the work happened. It does not say what the work
*produced*, and six months later the checkbox is all that is left. The network
stack proved the point: finished, verified end to end, and for a month the only
description of it anywhere was a checkbox four levels of indentation deep.
So roadmap.md went from 534 lines to 181 -- nothing deleted, the done work
graduated. Seven subsystems that were finished and undescribed now have
descriptions: boot, kernel, syscalls, filesystem, networking, graphics, usb,
init. Every one ends with "What it does not do", which is the section that pays;
a document listing only strengths is marketing, and the reader who needs it most
is the one about to trip over a limitation nobody wrote down.
schfs.md still opened with "phases 1 through 3 1/2 are done, phase 4 is next"
long after all five had landed. It is a description now, with the phase record
and the hard-won bugs moved to history/.
Three corrections found on the way, all load-bearing:
* projects/schcom/README.md claimed SCM_RIGHTS was done. There is no
sendmsg/recvmsg in the kernel at all, and an entire GUI plan rested on it.
* The root README said "No TCP/IP" months after the stack landed.
* history/kernel.md sec.7 was marked "in progress" on a problem that process
groups solved.
Also adds plan/1.0.md, which draws a finish line so the open list stops being
infinite, and marks schwm/ and schde/ superseded: in Wayland the compositor is
the window manager, so that split dissolves rather than being abandoned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQ7Qx8E1q1M1bbCq8QLHS2
This commit is contained in:
parent
817d666068
commit
57aff40228
59 changed files with 3981 additions and 1124 deletions
170
docs/architecture/filesystem.md
Normal file
170
docs/architecture/filesystem.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# The VFS
|
||||
|
||||
The layer between `open(2)` and a filesystem. SchFS is described separately in
|
||||
[schfs.md](schfs.md); this is the machinery it plugs into, plus the buffer cache,
|
||||
mounts, and procfs.
|
||||
|
||||
---
|
||||
|
||||
## `struct fs_ops`
|
||||
|
||||
A filesystem is a vtable, chosen by `iget()` from the device number and never
|
||||
null once an inode is held.
|
||||
|
||||
```c
|
||||
struct fs_ops {
|
||||
const char *name;
|
||||
|
||||
void (*iload) (struct inode *); // fill type/size/nlink from disk
|
||||
void (*istore) (struct inode *); // write them back
|
||||
void (*ifree) (struct inode *); // last link gone, release it
|
||||
|
||||
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);
|
||||
void (*truncate)(struct inode *, uint64 newsize);
|
||||
|
||||
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);
|
||||
|
||||
struct inode *(*ialloc)(uint dev, short type);
|
||||
int (*getdents)(struct inode *dp, char *out, int cap, uint64 *off);
|
||||
};
|
||||
```
|
||||
|
||||
Two implementations exist: `schfs_ops` (`kernel/fs/schfs/vfs.c`) and `procfs_ops`
|
||||
(`kernel/fs/procfs.c`).
|
||||
|
||||
`poff` is deliberately **an opaque cookie** rather than a byte offset —
|
||||
meaningful only to the same filesystem's `dirunlink`. A directory does not have
|
||||
to be an array of fixed-size records for this interface to work.
|
||||
|
||||
`ialloc` creates an object **with no name**. The caller sets `nlink` and calls
|
||||
`dirlink`; until it does, nothing can reach the object. If the caller dies in
|
||||
between, the result is an unreferenced record that is harmless and, in SchFS,
|
||||
never reaches the disk anyway — nothing is durable until the transaction commits.
|
||||
|
||||
### The one place the models disagree
|
||||
|
||||
The VFS inherited xv6's shape: allocate an inode, fill it in, link a name to it.
|
||||
SchFS has no free-standing inode — an object exists because a record with its id
|
||||
exists.
|
||||
|
||||
`nlink` is the sharper disagreement. **The VFS adjusts it itself** and then calls
|
||||
`iupdate`, so SchFS's own link accounting is deliberately bypassed in the VFS
|
||||
path: `schfs_dir_add`/`schfs_dir_del` are used rather than `schfs_link`/
|
||||
`schfs_unlink`, and `iput`'s `ifree` hook releases the object once the VFS has
|
||||
decided there are no names left.
|
||||
|
||||
> Two pieces of code counting links independently is precisely how a filesystem
|
||||
> loses a file that is still referenced.
|
||||
|
||||
`nlink` is an `int`, not a `short`. SchFS stores a `uint32` and the narrower type
|
||||
truncated it, so a file past 32767 names came back with a *negative* count — and
|
||||
`iput` frees an object whose count reaches zero. Signed on purpose: `nlink--` on
|
||||
a count that is somehow already zero should land somewhere a check can see it,
|
||||
not wrap to four billion.
|
||||
|
||||
## Path resolution
|
||||
|
||||
`namex()` walks a path one component at a time. It was refactored into
|
||||
`namex_from(struct inode *start, ...)`, parameterising what used to be a
|
||||
hardcoded `idup(proc->cwd)`.
|
||||
|
||||
That refactor is what made the `*at()` family work. Every one of them — `openat`,
|
||||
`fstatat`, `unlinkat`, `renameat`, `linkat`, `symlinkat`, `mkdirat`, `mknodat`,
|
||||
`readlinkat`, `faccessat` — previously handled only two cases: `AT_FDCWD`, or an
|
||||
absolute path with `dirfd` ignored. A **real** dirfd with a **relative** path is
|
||||
the standard, efficient way any directory-tree walker avoids rebuilding full
|
||||
path strings, and it was either rejected outright or — worse, in `openat` —
|
||||
silently resolved against `proc->cwd`, which can open the wrong file entirely.
|
||||
|
||||
`sys_fs.c` now has `at_dir(dirfd)`, `at_namei()` and `at_nameiparent()`, and
|
||||
every `*at()` call uses them.
|
||||
|
||||
## The buffer cache
|
||||
|
||||
`kernel/dev/bio.c`, xv6's design unchanged: a fixed pool of buffers, an LRU
|
||||
list, one sleeplock per buffer. `bread()` returns a locked buffer; `bwrite()`
|
||||
flushes it; `brelse()` releases.
|
||||
|
||||
**Sleeping is the point.** A buffer lock is held across a disk read, which is
|
||||
why every path that touches a filesystem must be able to sleep — and why the
|
||||
root mount happens in `forkret()` rather than in `main()`.
|
||||
|
||||
## Mounts
|
||||
|
||||
`mount()` and `umount2()` at their Linux numbers, with a mount table consulted
|
||||
during path resolution. `/proc` is mounted by init:
|
||||
|
||||
```c
|
||||
mount("proc", "/proc", "proc", 0, 0);
|
||||
```
|
||||
|
||||
## procfs
|
||||
|
||||
`kernel/fs/procfs.c`. A real filesystem behind the same vtable, not a set of
|
||||
character devices.
|
||||
|
||||
**The reason it had to become one is `ps`.** /proc used to be `mknod`'d nodes —
|
||||
init ran `mknod("/proc/uptime", 6, 2)` and friends, so every file was a real
|
||||
inode on the root filesystem with a major/minor behind it. That is fine for a
|
||||
fixed list and cannot work at all for the thing /proc is mostly for: **one
|
||||
directory per process**. You cannot `mknod` a directory for a process that does
|
||||
not exist yet and will be gone in a minute. `ps` listed `/proc`, got no numbers,
|
||||
and reported nothing. It was never a `ps` problem.
|
||||
|
||||
Nothing is stored. Directory listings are generated from the process table when
|
||||
asked; file contents when read.
|
||||
|
||||
Three things worth knowing:
|
||||
|
||||
- **The split is deliberate.** `kernel/fs/procfs.c` owns the *namespace* — what
|
||||
exists. `kernel/dev/procfs.c` keeps the *generators* — what the global files
|
||||
say. They change for different reasons, and `proc_gen_global()` is the only
|
||||
thing that crosses between them.
|
||||
- **procfs never walks the process table.** `proc_snapshot()` copies what is
|
||||
needed out from under `ptable.lock`, so a process that exits between the
|
||||
readdir that listed it and the open that reads it produces a stale line rather
|
||||
than a use-after-free.
|
||||
- **Sizes are reported as zero**, exactly as Linux does. A synthetic file has no
|
||||
length until it is generated, and generating every one on every `stat()` to
|
||||
fill in a number almost nothing reads would be absurd.
|
||||
|
||||
### What is there
|
||||
|
||||
Per-process directories, plus the global files: `meminfo`, `uptime`, `cpuinfo`,
|
||||
`version`, `stat`, `loadavg`, `mounts`, `filesystems`, `schfs`, `pci`,
|
||||
`interrupts`, `devices`, `netif`, and `sysrq`.
|
||||
|
||||
**/proc is read-only except `sysrq`**, which is the one deliberate write target,
|
||||
and the code says so in one place. Marking every procfs inode read-only once
|
||||
broke `echo s > /proc/sysrq` and took out three USB checks within a minute —
|
||||
they drive the disk through sysrq.
|
||||
|
||||
`sysrq` keys: `c` crash on purpose, `u` / `s` storage checks, `d` display
|
||||
selftest, `t` text specimen, `n` the netdev ARP selftest.
|
||||
|
||||
---
|
||||
|
||||
## What it does not do
|
||||
|
||||
- **No page cache.** `mmap` of a file reads the whole thing in at map time, and
|
||||
writable `MAP_SHARED` of a file is refused rather than faked. This is the
|
||||
single largest gap in the layer.
|
||||
- **No symlinks in procfs**, which is why `/proc/<pid>/exe` is a regular file
|
||||
and `ps -o comm` prints `[init]`.
|
||||
- **No `AF_UNIX` socket inodes.** Socket names live in a table in
|
||||
`kernel/net/unix.c`, so `bind()` creates nothing that can be `stat()`ed or
|
||||
`unlink()`ed.
|
||||
- **No unified inode cache across filesystems** beyond the single `itable`.
|
||||
- **A known, un-root-caused hang:** heavy `ls -lR` use across many commands in
|
||||
one long shell session can eventually make a *later*, unrelated directory
|
||||
hang; the identical command succeeds on a fresh boot. It is not inode or file
|
||||
table exhaustion — both fail cleanly and neither fired. It smells like a lock
|
||||
or refcount leak in the `*at()` paths or in toybox's own fd handling. The next
|
||||
step is instrumenting `itable`/`ftable` occupancy across a long session to see
|
||||
what is climbing.
|
||||
- **`sys_delete`/`sys_show`/`sys_showstat`** are orphaned entries from the old
|
||||
v1 filesystem's recycle bin. Nothing reaches them and they die with `fs.c`.
|
||||
Loading…
Reference in a new issue