OSOperating Systems

Block D · Topics 18–22

Storage & I/O

How a stream of named bytes gets built out of scattered blocks, how that survives a power cut, how processes talk to each other — and what a container actually is once you stop calling it a lightweight VM.

18

File systems

inodedirectorieshard vs soft linksVFS
Why it exists

A disk is a flat array of numbered blocks with no notion of names, sizes, ownership or ordering. A file system is the data structure that turns that into "/home/hetav/notes.md, 4 KB, mine, modified Tuesday" — and it has to do it while surviving crashes and staying fast.

The inode is the file

The single most important idea in this topic: a file's identity lives in its inode, not in its name. A directory is just a table mapping names to inode numbers, which explains almost every question that gets asked here.

inode  (fixed size, in an on-disk array)     directory "/home/hetav"
  ├ mode + permissions   (rw-r--r--)          is itself a FILE whose
  ├ uid / gid                                 contents are entries:
  ├ size in bytes                               "."         → 128
  ├ timestamps: atime, mtime, ctime               ".."      → 2
  ├ link count           ← how many names        "notes.md" → 5051
  └ block pointers:                              "todo.txt" → 5052
       12 direct        → 12 × 4 KB   =  48 KB
       1 single indirect → 1024 × 4 KB =  4 MB
       1 double indirect → 1024² blocks ≈ 4 GB
       1 triple indirect → 1024³ blocks ≈ 4 TB

NOTE: the inode holds NO NAME. That is not an oversight; it is what
makes hard links, atomic rename and delete-while-open all work.

Modern file systems (ext4, XFS) use EXTENTS instead — (start, length)
pairs — because a 1 GB contiguous file needs one extent rather than
262,144 pointers. Same idea, far less indirection.

The consequences — and this is what gets asked

QuestionAnswer, from the inode model
What does rm do?Removes the directory entry and decrements the link count. The data and inode are freed only when the count hits 0 and no process still has it open.
Why does deleting a file not free space?A process still has it open, so the kernel keeps the inode alive. The classic case: deleting a log file that a running service holds — disk stays full until you restart it or truncate via /proc/PID/fd/N.
Why is mv instant within a filesystem but slow across?Same filesystem = rewrite a directory entry, same inode. Across filesystems = full copy then delete, because inode numbers are filesystem-local.
Why is rename atomic?It's a single directory-entry update. This is the basis of the safe-write pattern: write to a temp file, fsync, then rename over the target — readers see either the old file or the new one, never a half-written one.

Hard links vs symbolic links

Hard linkSymbolic (soft) link
IsAnother name for the same inodeA tiny file containing a path string
Link countIncrements itDoesn't touch it
Delete the originalData survives — the other name still points at the inodeDangles. The path no longer resolves.
Cross filesystemsNo — inode numbers are per-filesystemYes
Link to a directoryNo (would allow cycles that break tree traversal)Yes
Own inode?No — shares itYes, its own

Allocation methods

MethodHowRandom accessProblem
ContiguousAll blocks adjacentO(1) — best possibleExternal fragmentation; files can't grow
LinkedEach block points to the nextO(n) — must walkOne bad pointer loses the tail; terrible for seeking
FATLinked list held in one table in memoryO(n) but in RAM, so fastTable scales with disk size; table corruption is fatal
Indexed (inode)An index block lists the blocksO(1) for small files, +1–3 reads for huge onesIndex block overhead for tiny files
Extents(start, length) rangesO(1) for contiguous runsNeeds contiguous allocation to pay off

The VFS layer

Linux's Virtual File System is why read() works identically on an ext4 file, an NFS mount, a USB stick, a pipe, and /proc/cpuinfo. VFS defines the operations (open, read, write, lookup) and each file system implements them. It's the clearest example of the OS's abstraction job — and it's why /proc and /sys can present kernel state as files that don't exist on any disk.

Say this

"The key idea is that a file is its inode — permissions, size, timestamps and block pointers — and the name lives in a directory, which is just a table of name-to-inode-number. That's why a hard link is a second name for the same inode and survives deleting the first, why rename is atomic and therefore the basis of safe writes, and why deleting a large log file doesn't free space if a process still has it open: the link count reached zero but the open file handle keeps the inode alive."

19

Crash consistency

journalingfsyncwrite barrierscopy-on-write
Why it exists

Appending one block to a file requires three separate writes: the data block, the inode (new size and pointer), and the free-space bitmap. A power cut between any two leaves the file system inconsistent — a block marked used that no inode references, or worse, an inode pointing at a block the bitmap thinks is free, which will be handed out again and cause silent corruption.

Why fsck wasn't enough

The original answer was to scan the entire file system after a crash and repair inconsistencies. Two fatal problems: it takes time proportional to the size of the disk, not the amount of recent work — hours for a large volume — and it can only restore consistency, not your data. It might correctly decide to discard a block you'd just written.

Journaling — write your intent first

1. Write a description of the update to the JOURNAL (a dedicated region),
   ending with a commit record.
2. Wait for the journal write to actually reach stable storage.
3. Only then apply the update to the real locations ("checkpointing").
4. Once applied, the journal entry can be discarded.

Crash recovery = replay the journal:
  · complete entry (commit record present) → REDO it. Idempotent, so
    replaying an already-applied entry is harmless.
  · incomplete entry (no commit record)    → discard it. The real
    structures were never touched, so they're still consistent.

Recovery time is proportional to the JOURNAL size (megabytes), not the
disk size (terabytes). That is the whole win.
Journal modeJournalsTradeoff
journal (full)Metadata and dataSafest — data is never lost or stale. Every byte written twice, so roughly half the throughput.
ordered (default)Metadata only, but data is forced to disk before the metadata commitsThe sensible default: you never see an inode pointing at stale blocks. Recent data can be lost, but never garbage.
writebackMetadata only, no orderingFastest. After a crash a file can have the right size but contain whatever was previously in those blocks — someone else's old data. Consistent metadata, garbage contents.
The distinction that matters

Journaling guarantees the file system is consistent after a crash. It does not guarantee your data was durable. Those are different promises. A successful write() only puts bytes in the page cache; they may sit in RAM for up to ~30 seconds. Only fsync(fd) forces them to stable storage and returns when it's done. Databases fsync their write-ahead log on every commit, and that fsync is the single biggest cost of a durable transaction — which is exactly why commit throughput is bounded by your disk's fsync rate.

The traps in "just call fsync"
  • Renaming needs two fsyncs. Write temp, fsync the file, rename, then fsync the directory — otherwise the directory entry itself may not be durable and the file can vanish entirely after a crash.
  • Drive write caches lie. A disk may acknowledge a write that's only in its volatile cache. Write barriers / FUA flush that cache; disabling barriers for speed is how people lose data with a perfectly journaled file system.
  • An fsync error may be reported once. On Linux, a failed writeback can be reported to the first fsync that asks and cleared afterwards — so ignoring the return value of fsync means silently losing the only notification you'll get.

Copy-on-write file systems

ZFS and Btrfs take a different route: never overwrite in place. Write new blocks elsewhere, then atomically update the pointers up to the root. The old version stays intact until it's no longer referenced, so a crash simply leaves the previous consistent root. No journal, no double write.

What you get for free: snapshots (just keep the old root — O(1)), cheap clones, and end-to-end checksums that detect silent bit rot rather than trusting the drive. What you pay: fragmentation from scattered writes (bad for databases doing random updates), higher memory use, and write amplification.

Say this

"One logical operation touches several structures, so a crash mid-way leaves the file system inconsistent. Journaling writes the intent to a log and commits it before touching the real structures, so recovery is replaying a few megabytes of journal rather than fsck-ing the whole disk. The important clarification is that journaling gives consistency, not durability — a successful write() is only in the page cache, and only fsync makes it durable, which is why database commit rates are bounded by disk fsync latency."

20

Disks & I/O

disk schedulingRAIDSSDDMAasync I/O
Why it exists

Storage is five orders of magnitude slower than RAM, so the OS spends considerable effort hiding that: caching aggressively, reordering requests to suit the physical device, and letting the CPU do something else while the transfer happens.

Disk scheduling — and why it barely matters now

Access time on a spinning disk = seek (move the arm, 5–10 ms) + rotational latency (wait for the sector, ~4 ms at 7200 rpm) + transfer (fast).

Seek dominates, and seek time depends on the distance the arm moves. So reordering requests to reduce total arm travel is a genuine, large win.
AlgorithmOrderTradeoff
FCFSArrival orderFair, no starvation, terrible arm travel
SSTFNearest cylinder nextGood throughput; starves far-away requests
SCAN (elevator)Sweep to one end, then reverseBounded waiting and good throughput — the classic answer
C-SCANSweep one way only, then jump backMore uniform waiting than SCAN (edges aren't served twice in quick succession)
LOOK / C-LOOKAs SCAN/C-SCAN but reverse at the last request, not the disk edgeStrictly better; what real elevator schedulers do

Worth saying out loud: on an SSD this whole topic is nearly irrelevant. There is no arm and no rotation, so there is no seek to optimise; what matters instead is queue depth and parallelism. Linux ships none/mq-deadline for NVMe precisely because elaborate reordering only adds latency. Knowing when the classic answer stops applying is the senior version of this answer.

SSDs behave differently, and it shows

· Read/write granularity is a PAGE (4–16 KB); ERASE granularity is a
  BLOCK (128–256 pages). You cannot overwrite a page in place — you must
  erase the whole block first.
· So updates go to a fresh page and the old one is marked stale.
  The Flash Translation Layer keeps the logical→physical map.
· GARBAGE COLLECTION later consolidates live pages and erases blocks.
  → WRITE AMPLIFICATION: writing 4 KB can cost far more physical writes.
  → and GC causes LATENCY SPIKES — the p99 problem on cheap SSDs.
· Cells wear out (limited erase cycles), so the FTL does WEAR LEVELLING,
  spreading writes evenly.
· TRIM tells the drive which blocks the file system no longer needs, so
  GC doesn't preserve dead data. Without TRIM, an SSD slows down over time.

Practical consequences: keep free space (over-provisioning) so GC has
room; sequential writes are far kinder than random; and a full SSD is a
slow SSD.

RAID

LevelMethodUsableSurvivesNotes
0Striping only100%NothingFast reads and writes, zero redundancy. More likely to fail than one disk — any drive loss kills everything.
1Mirroring50%1 diskSimple, fast reads (either copy), rebuild is a plain copy. Expensive.
5Striping + distributed parity(n−1)/n1 diskGood capacity. Write penalty: one logical write = read old data + read old parity + write data + write parity (4 I/Os).
6Two parity blocks(n−2)/n2 disksNeeded at large capacities because rebuilds take so long a second failure is likely.
10Mirror, then stripe50%≥1 diskThe database answer. No parity write penalty, fast rebuild, best random-write performance.
RAID is not a backup

Say this unprompted; it's the point of the question. RAID protects against drive failure and nothing else. It faithfully replicates DROP TABLE, ransomware, a bad migration, and file corruption. It also doesn't protect against fire, theft, or the controller itself failing. Also worth knowing: RAID 5's rebuild window is dangerous — rebuilding a large array reads every remaining disk in full for hours, which is exactly when a second drive is most likely to fail, and with single parity that's total loss. That risk is why RAID 6 and RAID 10 displaced RAID 5 for large arrays.

How data actually moves, and I/O models

DMA is why I/O doesn't consume the CPU: the CPU tells the controller "put 4 KB at physical address X" and goes away; the controller transfers directly to memory and raises an interrupt when done. Without DMA (programmed I/O), the CPU copies every byte itself.

ModelBehaviourUse for
BlockingThread sleeps until completeSimple code; needs a thread per concurrent operation
Non-blocking + pollReturns EAGAIN; you retryWasteful on its own
I/O multiplexing (epoll, kqueue)One thread waits on thousands of descriptorsThe event-loop foundation — nginx, Node, Redis
Async I/O (io_uring)Submit operations to a shared ring; collect completions. Real async, not just readiness.High-performance storage; drastically fewer syscalls

select vs poll vs epoll comes up a lot: the first two are O(n) in watched descriptors on every call (and select caps out around 1024), while epoll registers interest once and returns only ready descriptors — O(1) in the number watched. That difference is what made 10,000+ concurrent connections per thread practical.

21

Inter-process communication

pipesshared memorymessage queuessignalssockets
Why it exists

Process isolation is the feature — and it means two processes cannot simply share a variable. IPC is the set of controlled holes in that wall, and they differ enormously in speed, structure and how far they reach.

MechanismShapeSpeedScopeUse for
Pipe (anonymous)Unidirectional byte streamFastRelated processes only (inherited fd)ls | grep — the shell pipeline
Named pipe (FIFO)Same, but has a filesystem nameFastAny local processSimple one-way feeds between unrelated programs
Shared memoryA shared region of address spaceFastest — no copying at allLocalLarge data, high throughput: databases, video buffers
Message queueDiscrete, typed, prioritised messagesMediumLocalStructured async messaging with message boundaries preserved
Unix domain socketBidirectional stream or datagramFast (no network stack)LocalLocal service APIs — Docker, X11, database sockets. Can pass file descriptors and credentials.
TCP/UDP socketBidirectionalSlowestAcross machinesAnything distributed
SignalA single number, asynchronouslyFastLocalNotification only — "stop", "reload config". Carries no data.
eventfd / futexCounter / wait primitiveVery fastLocalEfficient wakeups, often paired with shared memory
The tradeoff to state

Shared memory is fastest precisely because the kernel is not involved in each transfer — the data is never copied, so there is no syscall per message. That's also why it's the most dangerous: it provides no synchronisation. You must supply your own semaphores or mutexes, and every concurrency bug from the concurrency topics is now available to you across process boundaries. Everything else copies through the kernel, which costs two copies and a syscall but gives you framing and synchronisation for free.

Signals — the details that get asked

SIGTERM (15)  polite "please exit" — CATCHABLE. What you send first.
SIGKILL (9)   immediate kill — CANNOT be caught, blocked or ignored.
              No cleanup runs, no destructors, no flush. Last resort.
SIGSTOP       suspend — also uncatchable.  SIGCONT resumes.
SIGSEGV       invalid memory access.       SIGINT  Ctrl-C.
SIGHUP        terminal closed; conventionally repurposed as "reload config".
SIGCHLD       a child changed state — how a parent knows to reap.

Why a process can be unkillable even with -9: it's in uninterruptible
sleep (state D), usually stuck in a kernel driver waiting on hardware.
The signal is DELIVERED only when it leaves kernel mode, and it never
does. Fix the hardware/mount, or reboot.

Handler discipline: a handler interrupts arbitrary code at an arbitrary
instruction, so it may only call ASYNC-SIGNAL-SAFE functions. malloc and
printf are not — calling them can deadlock on a lock the interrupted code
already held. The standard pattern is: set a volatile flag (or write a
byte to a self-pipe) and do the real work in the main loop.
Say this

"Pipes for a simple byte stream between related processes, Unix domain sockets for a local bidirectional API — they skip the network stack and can pass file descriptors — and TCP sockets when it has to cross machines. Shared memory when throughput matters, because there's no copy and no syscall per message; the tradeoff is that it gives you no synchronisation at all, so you're back to supplying your own mutexes and you've extended every race condition across process boundaries. Signals are notification only — they carry no payload — and a handler can only call async-signal-safe functions, so the safe pattern is to set a flag and handle it in the main loop."

22

Virtualisation & containers

hypervisornamespacescgroupsisolation
Why it exists

One machine, several workloads that must not interfere — and processes alone aren't enough isolation, because they share the file system, the network stack, the process list and all the kernel's resources. Virtualisation and containers are two very different answers, at two very different costs.

Hypervisor types

TypeRuns onExamplesTradeoff
Type 1 (bare metal)Directly on hardwareESXi, Xen, Hyper-V, KVM (arguably)Best performance and isolation; it is the OS. Production and cloud.
Type 2 (hosted)As an app on a host OSVirtualBox, VMware WorkstationConvenient on a desktop; extra layer costs performance

Full virtualisation emulates hardware so an unmodified guest OS runs; hardware extensions (Intel VT-x, AMD-V) make this fast by letting guest kernel code run natively while trapping privileged operations. Paravirtualisation instead modifies the guest to call the hypervisor deliberately — faster historically, less needed now that hardware support is universal.

Containers are not lightweight VMs

Virtual machines Containers hardware hypervisor guest OS (full kernel) libs + app guest OS (full kernel) libs + app guest OS (full kernel) libs + app Boots in ~30s · GBs of image · full kernel each Isolation boundary = the HYPERVISOR. A guest kernel bug stays inside its guest. Can run a different OS entirely. hardware ONE shared host kernel — namespaces + cgroups libs + app container libs + app container libs + app container Starts in ~50ms · MBs of image · no guest kernel Isolation boundary = the KERNEL's own checks. A kernel exploit escapes EVERY container. Must share the host's kernel (so: Linux on Linux).
The whole distinction in one line: a VM virtualises hardware; a container is just isolated processes. That's why containers are fast and why their security boundary is weaker.

What a container actually is

There is no "container" object in the Linux kernel. A container is a normal process with three kernel features applied:

FeatureProvidesDetail
Namespaces"What can I see?"Separate views of PIDs (your process is PID 1), mounts, network interfaces, hostname (UTS), users, IPC objects, and cgroups.
cgroups"How much can I use?"Limits and accounting for CPU, memory, block I/O, PIDs. This is what makes --memory=512m real.
Union filesystem"What's my root?"OverlayFS stacks read-only image layers with a writable top layer — why images are shareable and containers are cheap to create.
Capabilities, seccomp, AppArmor/SELinux"What may I ask the kernel?"Drop root privileges to a subset and filter which syscalls are even permitted.
Consequences people miss in interviews
  • A container memory limit is enforced by the cgroup OOM killer. Exceed it and your process is killed inside the container while the host is fine — the mysterious "exit code 137" (128 + 9 = SIGKILL).
  • PID 1 in a container has special duties. It must reap zombies and forward signals. Most application processes don't, which is why containers accumulate zombies and ignore SIGTERM — hence --init or tini.
  • The kernel is shared, so it isn't a security boundary you'd bet on for hostile multi-tenancy. That's why Firecracker microVMs and gVisor exist: VM-grade isolation with container-grade start times.
  • Tools inside a container may report the host's resources. Older JVMs and nproc read host CPU/memory rather than the cgroup limit, so a runtime sizes its thread pools and heap for a 64-core machine inside a 1-core container.
Say this

"A VM virtualises hardware — each guest runs its own kernel on a hypervisor, so the isolation boundary is the hypervisor and you can run a different OS. A container is just a process on the host's kernel with namespaces restricting what it can see, cgroups limiting what it can use, and an overlay filesystem for its root. That's why it starts in milliseconds instead of seconds and ships megabytes instead of gigabytes — and why its isolation is weaker, since a kernel vulnerability escapes every container on the box. For hostile multi-tenancy you'd want microVMs, which buy back the hardware boundary at close to container start-up speed."