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
Question
Answer, 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 link
Symbolic (soft) link
Is
Another name for the same inode
A tiny file containing a path string
Link count
Increments it
Doesn't touch it
Delete the original
Data survives — the other name still points at the inode
Dangles. The path no longer resolves.
Cross filesystems
No — inode numbers are per-filesystem
Yes
Link to a directory
No (would allow cycles that break tree traversal)
Yes
Own inode?
No — shares it
Yes, its own
Allocation methods
Method
How
Random access
Problem
Contiguous
All blocks adjacent
O(1) — best possible
External fragmentation; files can't grow
Linked
Each block points to the next
O(n) — must walk
One bad pointer loses the tail; terrible for seeking
FAT
Linked list held in one table in memory
O(n) but in RAM, so fast
Table scales with disk size; table corruption is fatal
Indexed (inode)
An index block lists the blocks
O(1) for small files, +1–3 reads for huge ones
Index block overhead for tiny files
Extents
(start, length) ranges
O(1) for contiguous runs
Needs 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 mode
Journals
Tradeoff
journal (full)
Metadata and data
Safest — 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 commits
The sensible default: you never see an inode pointing at stale blocks. Recent data can be lost, but never garbage.
writeback
Metadata only, no ordering
Fastest. 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.
Algorithm
Order
Tradeoff
FCFS
Arrival order
Fair, no starvation, terrible arm travel
SSTF
Nearest cylinder next
Good throughput; starves far-away requests
SCAN (elevator)
Sweep to one end, then reverse
Bounded waiting and good throughput — the classic answer
C-SCAN
Sweep one way only, then jump back
More uniform waiting than SCAN (edges aren't served twice in quick succession)
LOOK / C-LOOK
As SCAN/C-SCAN but reverse at the last request, not the disk edge
Strictly 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
Level
Method
Usable
Survives
Notes
0
Striping only
100%
Nothing
Fast reads and writes, zero redundancy. More likely to fail than one disk — any drive loss kills everything.
1
Mirroring
50%
1 disk
Simple, fast reads (either copy), rebuild is a plain copy. Expensive.
5
Striping + distributed parity
(n−1)/n
1 disk
Good capacity. Write penalty: one logical write = read old data + read old parity + write data + write parity (4 I/Os).
6
Two parity blocks
(n−2)/n
2 disks
Needed at large capacities because rebuilds take so long a second failure is likely.
10
Mirror, then stripe
50%
≥1 disk
The 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.
Model
Behaviour
Use for
Blocking
Thread sleeps until complete
Simple code; needs a thread per concurrent operation
Non-blocking + poll
Returns EAGAIN; you retry
Wasteful on its own
I/O multiplexing (epoll, kqueue)
One thread waits on thousands of descriptors
The event-loop foundation — nginx, Node, Redis
Async I/O (io_uring)
Submit operations to a shared ring; collect completions. Real async, not just readiness.
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.
Mechanism
Shape
Speed
Scope
Use for
Pipe (anonymous)
Unidirectional byte stream
Fast
Related processes only (inherited fd)
ls | grep — the shell pipeline
Named pipe (FIFO)
Same, but has a filesystem name
Fast
Any local process
Simple one-way feeds between unrelated programs
Shared memory
A shared region of address space
Fastest — no copying at all
Local
Large data, high throughput: databases, video buffers
Message queue
Discrete, typed, prioritised messages
Medium
Local
Structured async messaging with message boundaries preserved
Unix domain socket
Bidirectional stream or datagram
Fast (no network stack)
Local
Local service APIs — Docker, X11, database sockets. Can pass file descriptors and credentials.
TCP/UDP socket
Bidirectional
Slowest
Across machines
Anything distributed
Signal
A single number, asynchronously
Fast
Local
Notification only — "stop", "reload config". Carries no data.
eventfd / futex
Counter / wait primitive
Very fast
Local
Efficient 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
Type
Runs on
Examples
Tradeoff
Type 1 (bare metal)
Directly on hardware
ESXi, 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 OS
VirtualBox, VMware Workstation
Convenient 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
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:
Feature
Provides
Detail
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."