Filesystems · Filesystem Fundamentals

fsync, fdatasync, and What Survives a Power Loss

The durability calls promise different things about data and metadata, and a file entry needs a second call on its directory; where the guarantees stop on each filesystem.

A program writes a configuration file, calls fsync, and considers the data safe. The call returns, the machine loses power, and the file is empty or absent. Nothing in the sequence was a mistake: fsync did exactly what it promises for the object it was given, and the object it was given was not the whole of what needed to persist. The durability of a file operation is a property of several objects, and each one requires its own request.

What the call actually orders

fsync on a descriptor guarantees that the file’s data and the metadata required to retrieve that data have reached stable storage. The second clause is what distinguishes it from a flush of the page cache: the file’s size, its block map, and the timestamps needed to locate the data are included, because data written to blocks that no map points to is unreachable after a crash.

fdatasync is the cheaper variant. It guarantees the data and the metadata strictly necessary to read the data back, and it may omit metadata that affects only how the file appears, chiefly the modification and access times. The saving is real on a filesystem that journals metadata separately from data, because the call can skip a metadata transaction that the data write did not require.

The distinction is narrower than it looks. A write that extends the file changes the size, and the size must be durable for the data to be readable, so fdatasync after an extending write performs most of what fsync would. The saving applies to overwrites inside the existing length, where the block map and size are already durable and only the data blocks and possibly the modification time are in question.

Both calls are per-file, not per-filesystem, though a filesystem may implement them by flushing everything. The standard permits an implementation to make fsync a global barrier, and some do; the interface gives no way to ask which.

Neither call makes the file’s name durable. The name lives in a directory entry, the directory is a different file, and its blocks are not touched by a call on the file’s descriptor. The entry is a record in a directory file, described in Directory Entries as a Name-to-Inode Map.

The directory is a separate object

Creating, renaming, or unlinking a name modifies a directory’s data. That data reaches stable storage only when the directory is itself synced. The idiom for a durable file creation is therefore two calls:

int fd = open("state.db.tmp", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (write(fd, buf, len) != len)             /* loop until every byte is out */
    return -1;
fsync(fd);                     /* the file's data and block map */
close(fd);
rename("state.db.tmp", "state.db");
int dir = open(".", O_RDONLY | O_DIRECTORY);
fsync(dir);                    /* the entry rename is now durable */
close(dir);

The second fsync is the one that is usually missing. Without it, the rename may exist only in memory when power is lost, and recovery can leave the old file in place or, with the old name gone and the new one not yet recorded, no file at all under either name. Syncing the file before the rename is what makes the second case impossible: the worst outcome is then the old content under the old name, never a truncated new file under the new name.

The ordering requirement is not about the calls being issued in sequence. It is about the durability of one reaching storage before the other becomes visible. A filesystem that reorders writes internally must respect the barrier implied by fsync; the call does not return until the earlier data is on media, so the subsequent rename cannot be committed ahead of it.

Opening a directory read-only and calling fsync on it is permitted on Linux, and it is the standard way to make a directory’s entries durable. POSIX does not require fsync to work on a directory descriptor, so a program that needs portability across systems cannot rely on it; the calls that are portable are the ones on regular files.

O_SYNC and O_DSYNC at open time fold the barrier into each write, at a cost per write rather than per batch. O_DSYNC corresponds to fdatasync semantics and O_SYNC to fsync, and both turn a write into a round trip to the device. For a workload of many small writes the difference is the difference between one flush and one per write, which is why the flags are used for logs and audit trails rather than for bulk output.

The window between write and fsync

A write returns once the data is in the page cache. It does not imply that anything reached the device, and a crash immediately after the return can lose the write entirely. The kernel’s writeback machinery decides when dirty pages are sent, driven by age and by the dirty ratio; the default maximum age on Linux is measured in tens of seconds, which is the window in which a crash loses acknowledged writes.

That window is the reason fsync exists and the reason its cost is not the cost of writing the data. The data may already be on the device when the call is issued, in which case the call waits for the device’s cache to be flushed. On a device with a volatile write cache, the flush is a separate command, and the call waits for its completion. The device’s own cache is part of the durability boundary, and a device that acknowledges a flush without performing it breaks the guarantee in a way no software can detect.

fdatasync after a write of a small amount therefore costs at least one device round trip, and often two: one to write the data and one to flush the cache. On a spinning disk each of those is a seek plus a rotational delay, on the order of milliseconds; on a device with a battery-backed cache the flush completes without waiting for the media and the cost drops to the transport latency. The number that matters for a program is not the throughput of writes but the latency of the barrier, and a program that calls fsync per record pays that latency per record.

The standard alternative is to batch: accumulate records, call fsync once per batch, and accept that a crash loses the tail of the current batch. The trade is explicit: latency per record against the size of the loss window. It is the axis on which write-ahead logs, databases, and mail servers differ.

What fsync does not promise

Five gaps are worth naming because each produces a class of bug.

fsync does not flush the parent directory. A newly created file can be synced and still have no name after a crash, as described above.

fsync does not guarantee that the file’s extended attributes, ACLs, or other secondary metadata are durable, except where they are needed to read the data. On some filesystems those live in separate blocks with their own transactions, and a program that sets an attribute and syncs the file may find the attribute missing after a crash.

fsync on one file does not flush a rename of another file into the same directory, even though the two share a directory block. The unit of the guarantee is the object the call was made on, and the directory is a different object.

fsync does not make a write visible to other processes earlier or later than it would have been. Visibility is a page cache property and it is immediate; durability is a device property and it is what the call orders. A reader in another process sees the data as soon as the write returns, regardless of the sync.

fsync does not repair a filesystem that is already inconsistent. It orders writes that the filesystem has accepted; it does not reconcile structures that a previous crash left damaged.

Filesystems where the guarantee is weaker or stronger

Ext4’s behaviour depends on how it was mounted and on whether the file was newly created. In its default ordered mode, data blocks are written before the metadata that points to them, so a crash cannot leave a file whose block map references unwritten blocks containing stale data from another file. In writeback mode that ordering is not maintained, and the exposure is exactly that: a file extended and then crashed can contain the previous contents of the reused blocks. The mode is a mount option, and the guarantee a program relies on is a property of the mount rather than of the call.

The other end is the case of a file that is created, written, synced, and then renamed over an existing file whose data was never synced. Ext4 has a heuristic aimed at that pattern: when a rename replaces an existing inode, the filesystem can force the new file’s data to be allocated and written before the rename commits, so that a crash cannot leave a name pointing at a file with a stale tail. The behaviour is a filesystem policy rather than a POSIX guarantee, it can be disabled by mount option, and code that depends on it is depending on something the interface never promised.

Copy-on-write filesystems place the barrier differently. A write allocates new blocks and updates a tree, and fsync commits the tree root for the file. The commit is atomic, so the file is either entirely at the old version or entirely at the new one, and there is no intermediate state in which the block map points at unwritten data. The cost is write amplification: each commit writes a path through the tree, and a workload of many small syncs pays for the tree depth each time.

Network filesystems weaken the promise in a way that is visible from the client. fsync on an NFS-mounted file sends a commit request to the server, and the server’s own cache and stable storage determine whether the data survives. The client’s call returns when the server acknowledges the commit, so the guarantee extends only as far as the server’s honesty about its own cache. The details belong to the network filesystem layer rather than to this interface.

Making a set of files durable together

There is no interface for syncing several files atomically. A program that must persist a set of related changes has three options.

Sync each file, then sync the directory. The result is durable as a set only if a crash between the individual syncs is acceptable, which it usually is not: a crash after half the files are durable leaves a set that is internally inconsistent.

Write the set into a single file with an internal structure that makes partial states detectable, sync that one file, and treat it as the unit. This is what a database’s write-ahead log does, and it reduces the atomicity problem to a single object with a checksum.

Write to a new directory, sync its entries and its files, then rename the directory into place. The rename is atomic, but as noted in Rename Atomicity and the Replacement of a Directory Entry, replacing a non-empty directory is refused, so the old directory must be moved aside first and the two renames are not one operation.

The interface’s atomicity primitives are per-name and per-object. Durability is per-object as well. The combination of a set of names and a set of contents, all durable and all mutually consistent, is not expressible with these calls alone, which is why the structures that need it build a log or a generation number on top.

The cost model in one place

The calls sort by what they wait for:

CallWaits forIncludes metadata
writenothing (page cache)no
fdatasyncdata plus retrieval metadataminimal
fsyncdata plus all metadatayes
syncfsevery dirty object on the filesystemyes
directory fsyncthe directory’s entry blocksyes

syncfs is the filesystem-wide barrier, available on Linux through a descriptor on any file in the filesystem. It is the wrong tool for per-file durability and the right one for a checkpoint that must cover everything at once, at the cost of waiting for unrelated writers.

The choice between fsync and fdatasync is worth measuring rather than assuming. On a filesystem that journals metadata in the same transaction as data, the two may cost the same; on one that separates them, fdatasync can avoid a journal write. Neither is correct to use when the file’s size or block map changed in the interval, because in that case the metadata is required and the cheaper call does the same work.

The one rule that holds across every filesystem is that the durability of a name and the durability of its contents are separate requests. A program that syncs only the file has made the data durable and left the name provisional; a program that syncs only the directory has made the name durable and left the contents at the mercy of the writeback window. The pair of calls, in that order, is the minimum for a file that must survive a power loss with both its name and its bytes intact.

References

  1. fsync(2) — Linux manual page — man7.org
  2. The Open Group Base Specifications Issue 7, 2018 edition — fsync() — The Open Group
  3. Ext4 Data Structures and Algorithms — Journal (jbd2) — Linux kernel documentation