|
AquilaOS documentation
How it works, and its code
|
A UNIX system has one tree of files, from / down, but the files in it live in very different places: an ext2 partition on a disk, an archive the boot loader left in memory, a filesystem made of RAM, the kernel's own tables shown as text (/proc), the devices (/dev). Programs don't care: they open(), read(), write() and close(), and expect the same answers everywhere.
The virtual filesystem (VFS) is the layer that makes this work. It gives every file, whatever holds it, the same in-memory object and the same operations; it walks paths across filesystems; it keeps the open files and their offsets; and it hands each operation to the filesystem the file belongs to. The filesystems themselves are in Filesystems.
All of this is kernel design: the same on any machine (see The kernel and the machine). Nothing in kernel/fs/ knows it runs on x86. The few places the machine shows through are noted at the end.
Every UNIX since the Seventh Edition has had the same four pieces, and SunOS's vnode interface (1985) made them independent of the filesystem:
AquilaOS has exactly these, with the names and shapes they had in the BSDs.
A file in use is a struct vnode (include/fs/vfs.h):
The kernel works only with vnodes; how the file is stored is the filesystem's business, behind p. A vnode is referenced (ref) by whatever uses it: an open file, a process's current directory, a mount, a lookup in progress. vfs_vget() returns a vnode with one more reference, vfs_close() drops one, and when the last one goes the filesystem's close operation decides what happens to it (fs/close.c): a disk filesystem keeps it in its cache, or frees the inode if the file was removed meanwhile; a filesystem without a close keeps it forever.
Each filesystem keeps its own vnodes: there is no global vnode table. A disk filesystem finds a vnode by inode number in its vnode cache, a hash table (struct vcache, fs/vcache.c), so that two lookups of one file get the same vnode. Filesystems that live in memory don't need one: their vnodes are always there, and the inode number is the vnode's address.
A filesystem is a struct fs:
It has two tables of operations, for two kinds of objects:
An operation a filesystem doesn't have is NULL, and the VFS answers ENOSYS (or a sensible default: a file without lseek is a pipe, ESPIPE).
The VFS calls them through small wrappers, one per operation: vfs_read(), vfs_vmknod(), vfs_file_read(), ... in fs/vops.c, fs/fops.c, fs/read.c, fs/write.c and the others. Each one checks the request, and sends it where it goes:
Most filesystems want the same thing from an open file: read and write at its offset and move it, wait if there's nothing yet, seek, truncate. kernel/fs/posix/ has those once, as posix_file_read(), posix_file_write(), posix_file_lseek(), ...; a filesystem points its fops at them and only writes its vops.
posix_file_read() (fs/posix/read.c) shows how waiting works for anything that can have no data yet (a pipe, a terminal): it reads; if nothing came and it isn't the end of the file, it returns EAGAIN with O_NONBLOCK, else sleeps on the vnode's read_queue until a writer wakes it, or a signal or a timeout comes (sched_wait()). posix_file_write() is the mirror image, on the write_queue. A regular file never waits.
A read from a file may end in a user program's buffer (read()), or in the kernel's (a filesystem reading its disk, execve() reading a program). The two can't be treated the same: user memory may not be mapped yet, or not at all, and must be reached with copyin() and copyout() (see System calls).
So bytes move through a struct iobuf (include/fs/iobuf.h), which says where the memory is and whose it is. Whoever produces the data calls iobuf_out(), whoever consumes it iobuf_in(), without knowing which kind of buffer it is. A fault in user memory ends the transfer: what moved before it counts, and if nothing did, the call returns EFAULT. The VFS, the devices and the sockets all take an iobuf, and vfs_read()/vfs_write() wrap a kernel buffer in one.
Disk filesystems move a file's data a block at a time through a kernel buffer, then the block's piece in or out of the iobuf.
vfs_lookup() (fs/lookup.c) turns a path into a vnode. Where it starts is in the struct uio that every VFS call takes, which also says who is asking:
The walk goes one component at a time:
A symbolic link is followed where it's found (the last component too, unless O_NOFOLLOW, and unless a slash follows it): the walk goes on from the link's directory with the link's target, then what was left of the path. At most 8 links are followed (POSIX's SYMLOOP_MAX), then ELOOP. A trailing slash wants a directory.
Nothing but the path itself is a string: the walk holds vnodes, not names. A process's current directory is a vnode, so renaming it doesn't change it; a path for a vnode is computed only when asked for (vfs_vnode_path(), for getcwd() and /proc/mounts), by going up through .. and finding each directory's name in its parent.
Two variants serve the calls that create or remove an entry: vfs_lookup_parent() stops before the last component and returns its directory and its name (mkdir, unlink, rename); vfs_lookup_dir() returns the vnode and the directory it was found in.
Permissions are checked on the way, as POSIX says (vfs_access()): the owner's bits for the owner, the group's for the group, the others' for everyone else; root may do anything except run a file that has no x bit at all. There are no supplementary groups and no ACLs.
Mounting puts a filesystem's root on a directory: from then on the walk goes into the filesystem instead of the directory. A mount is two pointers (fs/vfs.c, vfs_bind()): the directory's mounted is the filesystem's root, the root's covered is the directory. Going down crosses mounted, going up through .. crosses covered. Mounting on / replaces the root.
vfs_mount() (fs/mount.c) finds the filesystem by name among the registered ones (vfs_install(), which each filesystem calls from its MODULE_INIT function), looks the directory up, and calls the filesystem's mount. A filesystem on a device looks the device's node up and reads it (load); one that needs no device binds its root. The mount is remembered for /proc/mounts.
The root filesystem is mounted before there is any process (see Booting): the initramfs from the boot loader's module, or a disk partition named by root= (vfs_mount_root_dev()).
AquilaOS has no umount: what's mounted stays mounted until the system stops.
open() (sys/sys_file.c, do_openat()) looks the path up (and creates the file with O_CREAT), checks the permissions against the open mode, and makes an open file:
This is POSIX's open file description (fs/file.c): one per open(), pipe(), socket() or accept(), with the offset and the status flags. A process's descriptors are an array of struct fd (FDS_COUNT, 64), each pointing to an open file, with its own FD_CLOEXEC flag. dup() and fork() copy descriptors, not files: the copies share the file, and its offset, which is why a shell's children writing to the same output don't overwrite each other.
An open file counts what refers to it: the descriptors, and the system calls using it at the moment. A system call holds each file it uses (file_hold()) until it returns, so a close() by another thread can't free a file from under a read() that's sleeping on it. The last reference closes the file: the filesystem's or the device's close, once, and the vnode reference goes.
poll() and select() wait until any of several files can be read or written. Each file says which queue a reader or writer of it would sleep on (vfs_file_wait_queue(): the vnode's read_queue or write_queue, a socket's own); the thread registers on all of them (poll_register(), sys/waitq.c) and sleeps. A wakeup of any of those queues wakes it to look again. A file that can't say what it would sleep on registers NULL, which any wakeup matches.
flock() locks a whole file, shared or exclusive, for an open file (fs/flock.c): descriptors that share the open file share the lock. The locks are advisory (only flock() looks at them), kept in one list, and released with LOCK_UN or when the open file is closed. There are no record locks (fcntl(F_SETLK)).
mmap() of a file, and execve() loading a program, map the file's pages into a process. A vnode's pages are kept in its vm_object (fs/vm_object.c), made on the first mapping: a page is read the first time a process touches it (vnode_page_in(), called from the page fault handler, see Memory), and stays for the next process that maps the same file. Private mappings (MAP_PRIVATE, and a program's segments) get a copy of a page when they write to it. Shared mappings need the filesystem's map operation, which only devices have (the framebuffer): MAP_SHARED on a regular file isn't supported.
The cached pages are never written back; a write() to the file, or a truncate, updates the cached copies.
AquilaOS caches in two places:
A filesystem itself writes through: when a file grows, its inode and bitmaps are written to the device at once. Writes reach the disk when the block cache writes them back: at the cache's next use once a block has been dirty for five seconds, when half the cache is dirty, on sync(), and before rebooting or powering off. sync() is the block cache's flush (kdev_sync()); there is no fsync(), and the VFS's own vfs_sync() (fs/sync.c) isn't implemented.
(fs/bcache.c is a smaller cache of a filesystem's own blocks, by number: minix keeps its indirect zone tables in one.)
Nearly nothing. The VFS is written against the kernel's interfaces only:
| File | Layer | What |
|---|---|---|
| kernel/include/fs/vfs.h | kernel | struct vnode, struct fs, vops, fops, struct file, struct uio |
| kernel/fs/vfs.c | kernel | registering filesystems, vfs_bind(), permissions |
| kernel/fs/lookup.c | kernel | path lookup, .., symbolic links, vfs_vnode_path() |
| kernel/fs/mount.c | kernel | vfs_mount(), the root from a disk |
| kernel/fs/vops.c, read.c, write.c, readdir.c, trunc.c, ioctl.c, close.c | kernel | the vnode operations' wrappers |
| kernel/fs/fops.c | kernel | the open file operations' wrappers, devices, FIFOs, sockets |
| kernel/fs/mknod.c, unlink.c, link.c, attr.c, stat.c | kernel | creating, removing, linking, renaming, attributes |
| kernel/fs/file.c | kernel | open files, holding them |
| kernel/fs/posix/ | kernel | the POSIX file operations most filesystems use |
| kernel/fs/iobuf.c | kernel | moving bytes to and from user or kernel memory |
| kernel/fs/vcache.c, bcache.c | kernel | the vnode cache, a filesystem's block cache |
| kernel/fs/flock.c | kernel | flock() |
| kernel/fs/vm_object.c | kernel | a file's pages |
| kernel/fs/sync.c | kernel | (stubs) |
| kernel/dev/blkcache.c | kernel | the disks' block cache |
| kernel/sys/sys_file.c | kernel | the file system calls, do_openat() |
| kernel/sys/waitq.c | kernel | poll_register() |