Skip to content

The filesystem

EFS is the native filesystem. It is, in the author’s own description, “ext2 with extents”: block groups with per-group bitmaps and inode tables, and extents in place of indirect block pointer trees.

The full on-disk specification lives in the repository. This page is the shape of it.

  • Block groups, each with a block bitmap, an inode bitmap and an inode table.
  • Extent-based block mapping. No indirect blocks.
  • Inline data: a file up to 176 bytes lives entirely inside its inode.
  • 256-byte inodes with 64-bit nanosecond timestamps.
  • CRC32 checksums on every major on-disk structure.
  • Feature flags, split into compatible, incompatible and read-only-compatible, so a future format change fails a mount instead of corrupting one.
  • A metadata write-ahead journal, mandatory via INCOMPAT_JOURNAL.
  • An orphan chain, so a deletion interrupted part-way through finishes on the next mount. A file’s inode outlives its last name — the directory entry goes when it is unlinked, the blocks when the last open descriptor closes — and the chain is what records that window on disk, so a power cut inside it leaves a deletion to complete rather than an inode nothing can reach.

What it deliberately is not: copy-on-write, so there are no snapshots; and not a data journal, so file contents are not crash-safe. The semantics are data=writeback.

Metadata changes are written to the journal and committed before the corresponding blocks are allowed to reach their final location. Every mount walks the ring from the recorded tail and stops at the first break in transaction sequence, which is what tells it where the live region ends; on a clean boot the first block it reads fails that check and the scan costs one read:

[1.910495] <cpu-3:fs:k:14> efs journal: replay scan start tail_seq=2 tail_block=10
[1.910597] <cpu-3:fs:k:14> efs journal: scanned, nothing to replay

The walk is bounded by sequence continuity rather than by the head the journal superblock records, because that head is written after a commit: a crash in between leaves it naming a position older than what the ring holds. Trusting it is how recovery came to discard committed work for a while, and the same walk now runs in both the kernel and efs-fsck, from one implementation, so the checker’s verdict and the kernel’s recovery cannot drift apart.

A dedicated journal_committer kernel thread commits transactions, and block_writeback flushes dirty pages only once the journal says it is safe. That gating is the reason the two are separate threads rather than one.

Three layers sit between a read and the disk:

Layer What it holds
Page cache file pages, per inode, with read-ahead and asynchronous fill
Block page cache sharded raw block cache under the filesystem, and the journal’s own view
AHCI or NVMe up to 32 NCQ commands in flight on AHCI, 64 outstanding on one NVMe queue pair; both submit and complete in separate halves of the driver

Two rules govern them, and both exist because breaking them caused real hangs: page-cache locks and block-cache shards are never held across disk I/O, and inode.lock is released before any I/O the VFS performs on behalf of a write.

Filesystem Where
EFS the root
FAT32 read and write, used for EFI system partitions and interoperability
devfs /dev: fb, tty0, dsp, kbd, mouse, random, klog
procfs /proc: counters and process state
memfs /tmp, and the fallback root when no root= is given

tools/efs-mkfs formats a partition and can populate it from a directory in one pass; make sata-disk.img and the live-root image both use it. The same formatter is built as a userspace program, so edos-install creates an EFS root from inside EDOS with one implementation rather than two that can drift.

tools/efs-fsck checks an image and, with --repair, fixes what it safely can, including reclaiming inodes the kernel dropped under pressure rather than stalling a dying thread behind disk I/O.

devfs exposes one node per block device, /dev/sda and /dev/sdb, with byte-granular read and write, plus ioctls to flush, rescan the partition table, read the sector count, and ask whether the device backs a mount. Reads and writes go through the block page cache rather than around it, keyed by the same (device, page) the filesystem driver will use moments later; a node that bypassed the cache would leave stale pages behind and produce an install that looks fine and does not boot.

Writing to a device that backs a mounted filesystem is refused.