Skip to content

Buffer Cache

The buffer cache is the layer that sits between the VFS and an actual storage driver, keeping recently read or written disk blocks in memory so that neither reading the same block twice nor writing it repeatedly has to touch the disk each time. ext2 and the other filesystem articles on this wiki describe reading a block as though it always meant reading it from disk; in practice, that read almost always checks this cache first.

Every cached block is keyed by the pair that uniquely identifies it: which device it belongs to, and which block number within that device. A read request for a given device and block number checks the cache first, commonly through a hash table keyed on exactly that pair, and only issues an actual read through the storage driver on a cache miss, a block the cache doesn’t currently hold. A block that’s already cached, a cache hit, returns immediately from memory, orders of magnitude faster than any physical storage access regardless of whether that storage is a spinning disk or flash.

struct buffer_head *bread(dev_t dev, uint32_t block) {
struct buffer_head *bh = cache_lookup(dev, block);
if (bh) return bh; // cache hit
bh = cache_alloc(dev, block);
driver_read(dev, block, bh->data); // cache miss: goes to disk
cache_insert(bh);
return bh;
}

A write doesn’t go straight to disk either: it’s applied to the cached copy in memory, and that block is marked dirty, a single bit recording that this cached copy no longer matches what’s actually on disk. A dirty block gets written out later, either by a periodic background flush (commonly every few seconds) or when memory pressure forces the cache to evict something and it has to save a dirty block’s contents before reclaiming the memory it occupies, rather than by every single write triggering its own immediate disk operation.

void bwrite(struct buffer_head *bh) {
memcpy(bh->data, new_data, block_size);
bh->dirty = true; // not written to disk yet
}
void flush_dirty_buffers(void) {
for_each_buffer(bh) {
if (bh->dirty) {
driver_write(bh->dev, bh->block, bh->data);
bh->dirty = false;
}
}
}
A write landing in the cache and being marked dirty immediately, with the actual disk write only happening later on a periodic flushwrite()returns immediatelyBuffer cacheblock updateddirty = trueseconds later, or under memory pressureDiskactually writtengap where an unclean shutdown loses this write

This gap between a write returning and the block actually reaching disk is exactly what an unclean shutdown exploits: a dirty block still sitting only in the cache at the moment power is lost never makes it to disk at all, indistinguishable afterward from a write that was never issued in the first place. Journaling protects a filesystem’s own structural consistency across this same gap, but that’s a narrower guarantee than protecting every dirty block’s content: journaling can guarantee the filesystem’s metadata stays internally consistent after a crash without guaranteeing that a specific dirty data block, still sitting unflushed in the cache when power was lost, survives at all.

A block already cached from a read and a block dirtied by a write are the same structure, not two separate mechanisms layered on top of each other: the cache doesn’t distinguish “read cache” from “write cache” internally, only whether a given cached block currently happens to be dirty. fsync and similar explicit-flush calls exist specifically to force a synchronous write-back of a particular block, or every dirty block belonging to a particular file, bypassing the normal periodic schedule for a caller that needs a specific guarantee that its data has actually reached disk before proceeding, a database’s transaction commit being the canonical example. Evicting a clean (non-dirty) block from the cache to make room for something else is safe at any time, since disk already holds an up-to-date copy, but evicting a dirty block requires writing it out first, which is what makes cache eviction under memory pressure occasionally slower than an ordinary allocation: the evicted block’s write has to complete before its memory can actually be reused.

  1. ^ M. Bach, The Design of the UNIX Operating System, Chapter 3: the classic description of the buffer cache and its interaction with filesystem I/O.
  • VFS: the layer whose block reads and writes this cache sits directly underneath.
  • Journaling: a narrower guarantee, protecting filesystem structure across the same crash window this cache’s dirty blocks are exposed to.