fsync fdatasync and Durability
A successful
write(2)does not mean your bytes are on the disk. On Linux a buffered write copies data into the page cache, marks the page dirty, and returns — the data still lives only in volatile RAM and is lost on a power cut until writeback eventually drains it, seconds later.fsync(2)is the syscall that turns “written” into “durable”: it forces a file’s dirty pages out, waits for the device to confirm, and issues a cache-flush down to the storage medium so the bytes survive a crash.fdatasync(2)is the cheaper cousin that skips metadata not needed to read the data back;O_SYNC/O_DSYNCmake everywrite()behave like an implicitfsync/fdatasync. This note explains the durability contract, the hardware barrier (FLUSH+FUA) that makes it real, and the 2018 “fsyncgate” incident in which Linux silently cleared write-back errors after a single read — corrupting PostgreSQL databases — and theerrseq_tmechanism that was meant to fix it but at first made it worse. All code is from the Linux 6.12 LTS tree (released 2024-11-17). (fs/sync.c, v6.12; fsync(2))
The reason durability needs an explicit syscall at all is the same reason buffered I/O is fast: the kernel decouples “the application thinks it wrote the data” from “the data is on the platter.” That decoupling — the page cache plus deferred writeback — is what lets a CPU dirty memory at gigabytes per second while a disk commits at a fraction of that. The price is that durability becomes the application’s responsibility: only a returned fsync (or fdatasync, or a write to an O_SYNC file) is a promise that the data will survive a crash. Databases, message queues, and any program that says “your transaction is committed” must fsync before they make that claim, or they are lying.
Mental Model — Three Barriers Between write() and the Platter
Think of durability as crossing three successive barriers, each of which can be skipped for speed and each of which loses data if you skip it at the wrong moment:
flowchart TB APP["write(fd, buf, n)<br/>returns immediately"] PC["Page cache<br/>(dirty page in RAM)<br/>LOST on power cut"] DC["Device write cache<br/>(volatile DRAM on the drive)<br/>LOST on power cut"] MED["Stable medium<br/>(platter / NAND)<br/>SURVIVES power cut"] APP -->|"barrier 1:<br/>writeback or fsync<br/>(page → device)"| PC PC -->|"barrier 1 cont.<br/>filemap_write_and_wait_range"| DC DC -->|"barrier 2:<br/>FLUSH (REQ_PREFLUSH)<br/>empties device cache"| MED DC -->|"barrier 3:<br/>FUA (REQ_FUA)<br/>this write bypasses cache"| MED
The durability staircase. What it shows: data starts in the page cache (volatile), an fsync first pushes it into the device’s own write buffer (still volatile — modern drives have a DRAM cache that lies about completion), and only a cache flush (REQ_PREFLUSH) or a Force Unit Access write (REQ_FUA) guarantees the bytes reach the stable medium. The insight: “the device confirmed the write” is not the same as “the write is durable” — the drive may have only buffered it in volatile DRAM. fsync must issue the flush/FUA barrier or it is no better than write. Each arrow is a place where, historically, some layer cheated for performance and lost people’s data.
What fsync and fdatasync Actually Promise
The fsync(2) man page states the contract precisely: fsync() “transfers (‘flushes’) all modified in-core data of … the file referred to by the file descriptor fd to the disk device … so that all changed information can be retrieved even if the system crashes or is rebooted. This includes writing through or flushing a disk cache if present. The call blocks until the device reports that the transfer has completed.” Crucially it also flushes the file’s metadata — the inode (size, block pointers, timestamps) — because data you cannot find via its inode is data you have lost (fsync(2)).
fdatasync(2) “does not flush modified metadata unless that metadata is needed in order to allow a subsequent data retrieval to be correctly handled.” The canonical example from the man page: a change to st_mtime (last-modified time) does not need flushing — you can still read the data without an up-to-date mtime — but a change to st_size (the file length, e.g. after appending) does, because if the on-disk size still says the file is shorter, the appended block is unreachable after a crash. So fdatasync of an append still costs a metadata write (the size grew), but fdatasync of an in-place overwrite of already-allocated blocks can skip the inode update entirely, saving one I/O and often one seek. That single saved I/O is why database write-ahead logs, which overwrite preallocated log files in place, prefer fdatasync.
Uncertain
Verify: the precise claim that an append’s
fdatasyncalways writes the inode in ext4 6.12 (vs. coalescing it into the journal commit it would do anyway). Reason: the man page describes the POSIX-level guarantee, not ext4’s exact I/O count; ext4 journals metadata, so the size update rides in the same transaction commit as the data sync rather than a separate inode write. To resolve: traceext4_sync_file → ext4_fsync_journalfor a size-changing inode and count the distinct I/Os. uncertain
A Subtle Gotcha: fsync Does Not Sync the Directory
The man page is explicit and this trips up nearly everyone: “Calling fsync() does not necessarily ensure that the entry in the directory containing the file has also reached disk. For that an explicit fsync() on a file descriptor for the directory is also needed.” So the safe pattern for creating a new file durably is: write the file, fsync the file, then open the parent directory and fsync it so the new directory entry (the name pointing at the inode) is also durable. Skip the directory fsync and after a crash the file’s data may be on disk but the name may not be — the file effectively vanishes. The standard atomic-replace recipe (write tmp; fsync tmp; rename tmp final; fsync dir) exists precisely because of this, and the Pillai et al. crash-consistency study (discussed in Crash Consistency and fsck) found that real applications routinely get this wrong.
The Code Path — vfs_fsync and the Device Flush
The syscall plumbing in fs/sync.c is short. fsync(2) and fdatasync(2) differ by exactly one argument:
/* fs/sync.c, v6.12 */
SYSCALL_DEFINE1(fsync, unsigned int, fd) { return do_fsync(fd, 0); }
SYSCALL_DEFINE1(fdatasync, unsigned int, fd) { return do_fsync(fd, 1); }
int vfs_fsync(struct file *file, int datasync)
{
return vfs_fsync_range(file, 0, LLONG_MAX, datasync); /* whole file */
}
int vfs_fsync_range(struct file *file, loff_t start, loff_t end, int datasync)
{
struct inode *inode = file->f_mapping->host;
if (!file->f_op->fsync)
return -EINVAL; /* pipes/sockets: nothing to sync */
if (!datasync && (inode->i_state & I_DIRTY_TIME))
mark_inode_dirty_sync(inode); /* fsync (not fdatasync) forces lazytime out */
return file->f_op->fsync(file, start, end, datasync); /* into the filesystem */
}The datasync flag is the whole fsync-vs-fdatasync distinction: it is threaded down to the per-filesystem ->fsync method, which decides what metadata to skip. Note the EINVAL for files with no ->fsync op — a pipe, FIFO, or socket cannot be synced, which is why fsync on them returns EINVAL (fs/sync.c, v6.12). The I_DIRTY_TIME line is the lazytime interaction: a full fsync forces even a timestamp-only dirty inode to disk, while fdatasync leaves it lazy.
The real work happens in the filesystem’s ->fsync. The generic implementation for simple filesystems, generic_file_fsync() in fs/libfs.c, makes the structure obvious:
/* fs/libfs.c, v6.12, generic_file_fsync */
err = __generic_file_fsync(file, start, end, datasync); /* write data + inode, wait */
if (err)
return err;
return blkdev_issue_flush(inode->i_sb->s_bdev); /* ← the device cache flush */__generic_file_fsync writes the dirty pages and the inode and waits for them; then blkdev_issue_flush() issues a REQ_OP_FLUSH to the block device, telling the drive to empty its volatile write cache to stable media. Without that final line, the data would sit in the drive’s DRAM and be lost on power failure even though every page-cache page was “written.” This is the single most important line in the whole durability story, and it is exactly what older/buggy fsync implementations historically omitted — the man page’s HISTORY note warns that “the fsync() implementations in older kernels and lesser used filesystems do not know how to flush disk caches” (fsync(2)).
ext4: fsync as a Journal Commit Plus a Barrier
On ext4 the ->fsync op is ext4_sync_file() (fs/ext4/fsync.c), and it is the clearest worked example of the three barriers. With a journal present:
/* fs/ext4/fsync.c, v6.12, ext4_sync_file (journaled path, simplified) */
ret = file_write_and_wait_range(file, start, end); /* 1. flush dirty data pages, wait */
if (ret) goto out;
ret = ext4_fsync_journal(inode, datasync, &needs_barrier); /* 2. commit the metadata txn */
issue_flush:
if (needs_barrier)
err = blkdev_issue_flush(inode->i_sb->s_bdev); /* 3. flush device cache */
out:
err = file_check_and_advance_wb_err(file); /* 4. report any writeback error */Step 1 pushes the file’s data out via the page cache and waits. Step 2 hands off to jbd2, which commits the transaction carrying this inode’s metadata. Step 3 issues the device cache flush. Step 4 — file_check_and_advance_wb_err — is the error-reporting hook that the fsyncgate fix added, discussed below.
The FLUSH+FUA pair that the task names lives one level deeper, in the jbd2 commit. When jbd2 writes the commit record (the block that makes a journal transaction “real”), it sets both flags so the commit is genuinely durable:
/* fs/jbd2/commit.c, v6.12, journal_submit_commit_record */
blk_opf_t write_flags = REQ_OP_WRITE | JBD2_JOURNAL_REQ_FLAGS;
...
if (journal->j_flags & JBD2_BARRIER &&
!jbd2_has_feature_async_commit(journal))
write_flags |= REQ_PREFLUSH | REQ_FUA; /* ← the two barriers */
submit_bh(write_flags, bh);REQ_PREFLUSH tells the device to flush everything already in its cache to stable media before this write — guaranteeing all the journal’s data/metadata blocks landed first. REQ_FUA (Force Unit Access) tells the device that this write must itself go straight to stable media, not be acknowledged from volatile cache. Together they enforce the ordering that journaling depends on: every block of the transaction is durable before the commit record, and the commit record is durable before fsync returns. This is why mounting ext4 with barrier=0 (or nobarrier) is dangerous — it strips these flags, making fsync fast but allowing the drive to reorder the commit record ahead of the data it certifies, which on a crash leaves a “committed” transaction whose data never reached disk (fs/jbd2/commit.c, v6.12).
O_SYNC and O_DSYNC — Synchronous-on-Every-Write
fsync is pull durability: the application decides when to force data out. O_SYNC and O_DSYNC are push durability — passed to open(2), they make every subsequent write() durable before it returns, as if each write were followed by fsync (O_SYNC) or fdatasync (O_DSYNC). The open(2) man page defines them in terms of POSIX “synchronized I/O completion”:
O_SYNCprovides synchronized I/O file integrity completion: “write operations will flush data and all associated metadata to the underlying hardware.” It is the per-write equivalent offsync.O_DSYNCprovides synchronized I/O data integrity completion: “write operations will flush data to the underlying hardware, but will only flush metadata updates that are required to allow a subsequent read operation to complete successfully.” It is the per-write equivalent offdatasync.
The man page gives the same st_mtime-vs-file-length example: all writes update mtime, but only an extending write changes the length; O_DSYNC will flush the length update (needed to read the data back) but may skip the mtime update, while O_SYNC flushes both. Linux implements O_SYNC and O_DSYNC but not O_RSYNC (read-synchronized I/O); glibc somewhat incorrectly defines O_RSYNC to equal O_SYNC (open(2)). When _POSIX_SYNCHRONIZED_IO is defined > 0 in <unistd.h> (which it is on Linux), fdatasync and these flags are guaranteed available (fsync(2)).
The trade-off is throughput: O_SYNC collapses all the batching that makes buffered I/O fast — every write now blocks for a full round-trip to the medium including a cache flush. It is the right tool only when per-write durability latency is acceptable (e.g. small, infrequent, must-not-lose records) and the wrong tool for bulk writes, where one explicit fsync after many writes amortizes a single flush across all of them. Note also that O_SYNC is independent of O_DIRECT: O_DIRECT bypasses the page cache but, on its own, does not guarantee the data and metadata reach stable storage — the man page warns “To guarantee synchronous I/O, O_SYNC must be used in addition to O_DIRECT.” (See Direct IO and O_DIRECT.)
sync, syncfs, and sync_file_range
Beyond a single file, fs/sync.c offers whole-system and whole-filesystem flushes:
-
sync(2)—SYSCALL_DEFINE0(sync)→ksys_sync()flushes all dirty data and metadata on all mounted filesystems. It first wakes the flusher threads (wakeup_flusher_threads(WB_REASON_SYNC)) so writeback runs on every device in parallel, then does a reliable two-phase per-superblock sync (sync_inodes_one_sb, thensync_fs_one_sbfirst withnowait=0thenwait=1), then flushes the block devices (sync_bdevs(false)thensync_bdevs(true)). The kernel comment explains the block-device pass exists because “some filesystems (e.g. ext2) just write metadata … to block device page cache and do not sync it on their own.” On Linux, unlike the bare POSIX spec,sync()waits for the I/O to complete — the man page notes “Linux waits for I/O completions, and thussync()orsyncfs()provide the same guarantees asfsync()called on every file in the system or filesystem respectively.” Since glibc 2.2.2 the prototype isvoid sync(void); it cannot report errors (sync(2)). -
syncfs(fd)—SYSCALL_DEFINE1(syncfs, int, fd)issyncscoped to the one filesystem containingfd. It callssync_filesystem(sb)and — critically for the fsyncgate fix — then callserrseq_check_and_advance(&sb->s_wb_err, &fd_file(f)->f_sb_err)to report any write-back error recorded on the superblock since this fd last checked. The man page documents the version boundary: “In mainline kernel versions prior to Linux 5.8,syncfs()will fail only when passed a bad file descriptor (EBADF). Since Linux 5.8,syncfs()will also report an error if one or more inodes failed to be written back since the lastsyncfs()call.” This superblock-level error reporting is exactly the polling mechanism the PostgreSQL/LSFMM discussion proposed (below). -
sync_file_range(2)flushes only a byte range of one file. The kernel comment is emphatic that it is not a durability primitive: “disk caches are not flushed by this call, so there are no guarantees here that the data will be available on disk after a crash,” and “none of these operations write out the file’s metadata.” Its man page calls naïve use “extremely dangerous.” It exists for hinting — start writeback early on a range so a laterfsyncfinds less to do — not for durability (sync_file_range(2)). PostgreSQL’s checkpointer usedsync_file_range(SYNC_FILE_RANGE_WRITE)to pre-clean pages, which is part of how it stumbled into fsyncgate.
fsyncgate 2018 — When a Successful fsync Lost Your Data
The most consequential durability bug of the decade was not about flushing too little data — it was about silently swallowing the error. In late March 2018 PostgreSQL developer Craig Ringer reported to pgsql-hackers that Linux could lose data with no error surfaced to the application, and LWN’s coverage (“PostgreSQL’s fsync() surprise,” Jonathan Corbet, April 2018) laid out the mechanism (LWN 752063).
The setup. PostgreSQL, like most databases, assumed a successful fsync() meant all data written since the last successful fsync is durable. That is the natural reading of the contract — and it is wrong on Linux in one specific way. When a buffered write-back fails at the hardware level (a bad sector, a yanked USB drive, a full thin-provisioned volume), filesystems “respond differently, but that behavior usually includes discarding the data in the affected pages and marking them as being clean.” The dirty page is dropped and marked clean, so a later read returns stale data — and the error indication itself could be lost.
Why the error vanished. Pre-4.13, the write-back error was recorded as a flag (AS_EIO) on the inode’s address_space, and the first caller to check it cleared it. So if a background sync(), or pdflush, or any other process consumed the error first, the next fsync saw success. Worse, PostgreSQL’s architecture made this nearly guaranteed to fail: it runs as many processes, but a single checkpointer process calls fsync. The checkpointer does not keep every file open — it opens a file just before fsyncing it. As Robert Haas explained in the thread, “even in 4.13 and later kernels, the checkpointer will not see any errors that happened before it opened the file. If something bad happens before the checkpointer’s open() call, the subsequent fsync() call will return successfully.” The error happened during background writeback; by the time the checkpointer opened the file and called fsync, the error state was gone. Tom Lane called it “kernel brain damage”; Robert Haas called it “100% unreasonable” (LWN 752063).
Why Linux drops the pages. Ted Ts’o explained the kernel’s side: the most common cause of write-back errors, by far, is a user pulling a USB drive at the wrong time. If the kernel kept those dirty pages around (as PostgreSQL initially asked), a large interrupted copy would accumulate un-drainable dirty pages until the system ran out of memory and became unusable. So the pages must be dropped to keep the system alive. The disagreement was never really about keeping the data — PostgreSQL quickly conceded that point — it was about reliably learning that something went wrong so the database could refuse to mark the checkpoint complete (LWN 752063).
The errseq_t Fix — and Why It First Made Things Worse
The kernel’s structural answer, designed by Jeff Layton and merged across 4.13–4.16, replaced the single check-and-clear AS_EIO flag with a new datatype, errseq_t (lib/errseq.c, documented in Documentation/core-api/errseq.rst). The idea: record the error in one place, but let every file descriptor independently observe whether an error has occurred since it last checked — so consuming the error in one fd no longer hides it from another.
An errseq_t is a single u32 packing three things (errseq.rst, v6.12):
31 .. 13 12 11 .. 0
+-----------+------+-----------+
| counter | SF | errno |
+-----------+------+-----------+The low 12 bits hold an errno (up to MAX_ERRNO), bit 12 is the “seen” flag (ERRSEQ_SEEN), and the upper 19 bits are a counter. The mechanism, from lib/errseq.c:
errseq_set(eseq, -EIO)records an error. It overwrites any existing errno and — only if the current value’sSEENflag is set (i.e. someone has observed the previous error) — increments the counter (ERRSEQ_CTR_INC). The counter is what makes two consecutive identical errors distinguishable: without it, anEIOfollowed by anotherEIOwould look unchanged.errseq_sample(eseq)grabs the current value as a starting cursor; if no error has been seen yet it returns 0 so a brand-new watcher does not inherit an old, already-observed error.errseq_check_and_advance(eseq, since)is whatfsynccalls. It compares the live value against the watcher’s savedsincecursor; if they differ, an error occurred since the watcher last looked, so it sets theSEENflag, advances*sinceto the current value, and returns the storederrno. If they match, it returns 0.
Each struct file carries its own cursor, f_wb_err, sampled at open time; file_check_and_advance_wb_err(file) (the last line of ext4_sync_file above) calls errseq_check_and_advance(&inode->i_mapping->wb_err, &file->f_wb_err). The man page reflects the outcome: “Since Linux 4.13, errors from write-back will be reported to all file descriptors that might have written the data which triggered the error” (fsync(2)).
But for PostgreSQL it initially got worse. At the second-day LSFMM 2018 session, PostgreSQL’s Andres Freund explained that reliability “was never reliable, but it got a bit worse for PostgreSQL after the introduction of errseq_t” (LWN 752952). The reason is the cursor semantics: errseq_sample deliberately ignores errors that occurred before the fd was opened (so a fresh fd does not see a stale, already-seen error). That is correct in general — but it is fatal for PostgreSQL’s open-then-fsync checkpointer, whose fd is opened after the failing writeback, so its f_wb_err cursor starts at the post-error value and errseq_check_and_advance reports nothing changed. The errseq machinery firewalls each fd to errors after its own open, which is exactly the window PostgreSQL was missing. The real fix on Linux’s side requires an fd that “stays open from the earliest write,” which PostgreSQL could not guarantee.
The Resolutions
The discussion (LWN 752063, 752952) and the years after produced fixes on both sides:
- Kernel: superblock-level error reporting via
syncfs. Jeff Layton’s idea — set an error flag on the filesystem superblock and letsyncfs()report and clear it — landed assb->s_wb_errplus the per-filef_sb_errcursor (visible insyncfs’serrseq_check_and_advance(&sb->s_wb_err, ...)above) in Linux 5.8 (2020). This lets a process poll an entire filesystem for “did any writeback fail anywhere” without holding every file open, which is the mechanism PostgreSQL needed (sync(2) VERSIONS). - PostgreSQL: panic on fsync failure (“the PANIC patch”). Because PostgreSQL could not reliably retry, it adopted the only safe response to an
fsyncerror: treat it as unrecoverable,PANIC(crash the server), and recover from the write-ahead log on restart — never re-fsyncand risk a “success” that hides discarded data. This shipped in PostgreSQL 12 (2019) and was back-patched. - The long-term answer: direct I/O. Both Dave Chinner and Ted Ts’o argued the real fix is
O_DIRECT, which gives the application precise control over which I/O failed. Freund agreed it is “the best long-term solution” but “a metric ton of work.” PostgreSQL has since been moving toward direct I/O and asynchronous I/O via io_uring over several major releases.
Uncertain
Verify: the exact release numbers —
s_wb_err/f_sb_errsuperblock error reporting in Linux 5.8 (thesync(2)man page confirmssyncfserror reporting “Since Linux 5.8”), and PostgreSQL’sdata_sync_retry/PANIC-on-fsync-failure first shipping in PostgreSQL 12. Reason: the kernel boundary is pinned by the man page, but the PostgreSQL version is from the discussion’s aftermath, not re-verified against PostgreSQL release notes during this task. To resolve: check the PostgreSQL 12 release notes and commit9ccdd7f6(the fsync-failure handling change). uncertain
Failure Modes and Diagnosis
“I called write and lost data on power loss.” Expected: write only dirties the page cache. The loss window for un-fsynced data is bounded by the writeback knobs (dirty_expire_centisecs/dirty_writeback_centisecs, ≈30 s by default) but is real. Fix: fsync/fdatasync before reporting a commit durable.
“fsync returned 0 but the data is corrupt after a crash.” The classic fsyncgate symptom on pre-5.8 kernels, or with an open-then-fsync pattern: the error was consumed elsewhere, or the fd was opened after the failure. Mitigation: keep a long-lived fd open, poll with syncfs on 5.8+, and treat any fsync error as fatal (do not retry-and-trust).
“fsync is fast but data still lost — drive lies about flushes.” Some consumer SSDs ignore FLUSH/FUA or have inadequate power-loss protection; mounting with nobarrier/barrier=0 deliberately strips the REQ_PREFLUSH|REQ_FUA flags. Symptom: corruption only after hard power loss, never after clean reboots. Diagnose: check mount options (mount | grep barrier), and for the drive, look for power-loss-protection (PLP) capacitors in the datasheet.
“fsync storms — one fsync stalls everything.” On ext4 data=ordered, an fsync forces the whole current journal transaction, dragging in unrelated dirty data committed in the same transaction. Symptom: fsync latency spikes correlated with other writers’ activity. The data-mode trade-offs are covered in Crash Consistency and fsck and The jbd2 Journaling Layer.
Alternatives and When to Choose Them
- Buffered
write+ periodicfsync— the default; batches many writes behind one flush. Best for throughput-oriented bulk writers that checkpoint occasionally. fdatasyncinstead offsync— for write-ahead logs that overwrite preallocated blocks in place (no size change), saving one inode I/O per commit.O_DSYNC— when every record must be durable on return and you do not want to remember to callfdatasync; pays a per-write flush.O_DIRECT(+O_SYNC) — for self-caching databases that want to manage their own buffering and know exactly which I/O failed; see Direct IO and O_DIRECT.syncfspolling — to check an entire filesystem for writeback errors without holding every file open (the post-fsyncgate pattern, Linux 5.8+).
Production Notes
The PostgreSQL fsyncgate is the canonical real-world cautionary tale: a “100% unreasonable” mismatch between the POSIX fsync contract as applications read it and what Linux actually guaranteed, which silently corrupted production databases. Its lessons are now industry folklore: (1) a successful fsync is only meaningful for errors that occurred while your fd was open — keep durability-critical fds open; (2) treat any fsync/fdatasync error as unrecoverable and do not retry-then-trust, because the pages may already be discarded; (3) O_DIRECT is the path to precise error attribution. Filesystem developers, in turn, hardened the model with per-fd errseq_t reporting (4.13+) and superblock-level reporting via syncfs (5.8+). MySQL/InnoDB, SQLite (which uses fullfsync on macOS and careful fsync/dir-fsync sequencing), and etcd all carry similar hard-won fsync discipline. The deeper lesson — that durability is a staircase of barriers (page cache → device cache → medium), each of which some layer has historically cheated — is why “did write succeed?” and “is my data safe?” are entirely different questions.
See Also
- Dirty Pages and Writeback — the deferred machinery that makes
writefast andfsyncnecessary; theWB_SYNC_ALLpath and the writeback knobs that bound the loss window. - Crash Consistency and fsck — the filesystem-internal sibling problem: keeping the filesystem consistent across a crash (journaling/CoW), and why
fsyncof a new file also needs a directoryfsync. - The jbd2 Journaling Layer — where the ext4
fsyncmetadata commit and theREQ_PREFLUSH|REQ_FUAcommit-record barrier actually happen. - The Page Cache and address_space — where dirty file data lives before
fsyncforces it out. - Direct IO and O_DIRECT — bypassing the page cache; why
O_DIRECTalone is not durable. - io_uring and the File Path — async I/O, the direction PostgreSQL moved to escape fsyncgate.
- UP: Linux Filesystems and VFS MOC §5 (The Page Cache, address_space, and Writeback).