Operating systems and concurrency
Page cache and fsync
Why a successful write only reaches RAM, how fsync and fdatasync force data to disk, when O_DIRECT helps, and why a failed fsync cannot be retried.
When your program calls write, the kernel copies the bytes into the page cache, a region of RAM holding recently used file data. The call returns right away. The kernel writes the page to disk later, on its own schedule, often after a few seconds. If the machine loses power before that, the write is gone, even though the call succeeded.
Durability requires an explicit request: fsync. It asks the kernel to write the file's dirty pages and metadata to the device, and to wait until the device reports completion.
The big idea
There are three durability points, and most bugs come from confusing them:
- After
write: the data is in RAM. A process crash keeps it, a power cut may not. - After
fsync: the data and metadata needed to read it are on stable storage, as far as the device reports. - After
fsyncon the directory: a new or renamed file's name is on stable storage too.
Skipping the last one loses files that appear to have been saved correctly. A new file's directory entry can be lost, even when its contents were flushed.
Walking through it
The pattern for a durable write has three steps: write, flush the data, then flush the name when you create or rename a file. The full version appears in durable file writes. Here is the core call sequence:
import os
fd = os.open("journal.log", os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
os.write(fd, b"txn 42 commit\n")
os.fsync(fd) # data and metadata reach the device
os.close(fd)fdatasync does the same, but skips metadata that isn't needed to read the data back, such as timestamps. It's cheaper, and databases use it on the log when the file size didn't change. Note that appending grows the file, which is metadata, so fdatasync still has to flush it in that case.
The fsync gate
In 2018, PostgreSQL users discovered a painful failure. When the kernel's fsync reported an I/O error, the kernel cleared the dirty flags on the failed pages. A later fsync then returned success, even though the data was never written. PostgreSQL retried the flush, saw success, and carried on. Data was silently lost.
The fix was to treat an fsync error as fatal. A failed fsync must crash the process, so recovery replays from the log instead of trusting the cache. Retrying fsync is not a safe response. Any code that depends on fsync should treat its error as "this data is not on disk, and you can't know which part".
Warning
Ignoring the return value of fsync, or retrying it after an error, can report success for data that was never written. Crash or restart, and recover from a known-good source.
O_DIRECT
Opening a file with O_DIRECT bypasses the page cache and writes straight to the device. Databases use it to control their own caching, as they already do with the buffer pool. It has rules: buffers and offsets must be aligned to the device's block size, and the performance gain depends on whether the application caches better than the kernel does. Use it when you have measured both.
Common mistakes
- Calling
writeand assuming the data is durable beforefsync. - Forgetting to
fsyncthe directory after creating or renaming a file. - Retrying
fsyncafter an error and trusting the second result. - Assuming
fsyncmakes data durable on every device. Drives with volatile write caches may lie unless they have power-loss protection.
Check yourself
- Why is a successful
writenot enough for durability? Answer: the data is in the page cache. The kernel may write it later, so a power cut before then loses it. - Why must a failed
fsynccrash the process, not retry? Answer: after an error the kernel may have cleared the dirty pages, so a retry reports success for data that was never written.
Key takeaways
writereturns when data reaches RAM.fsyncis the durability point.- Flush the directory too when creating or renaming a file.
- Treat
fsyncerrors as fatal, and recover from a known-good source. O_DIRECTbypasses the cache, but it needs alignment and measurement.
Further learning
Next, see durable file writes for the full pattern for replacing a file safely.
Related
Files and storage
How file systems map names to inodes, how journaling and the page cache affect durability, and how the storage devices underneath behave, from SATA disks and SSDs to cloud volumes.
The memory hierarchy
From registers to SSDs to the network, the latency numbers that dominate performance, and why the tier your data sits in matters more than the code you write.
Write-ahead log
The write-ahead log records every change before the page is written. How commits become durable, how recovery replays the log, and what group commit and checkpoints do.
Buffer pool
The buffer pool caches database pages in RAM. How eviction chooses which page to drop, why sequential scans can wipe out the cache, and how midpoint insertion protects hot pages.