An order service appends a payment record to a file and receives a successful return from write(). It then tells the client that the payment is committed.
A few milliseconds later, the machine loses power. After reboot, the record is missing.
Nothing necessarily malfunctioned. With ordinary buffered I/O, write() can succeed after the kernel accepts the bytes into memory. The operating system is free to send those bytes to storage later.
Applications that promise durable state must therefore distinguish several different events:
These events are not interchangeable. fsync() and related operations exist to bridge the gap between accepted writes and durable state.
Storage code becomes easier to reason about when it names the guarantee it actually needs.
Accepted means that an I/O layer has taken responsibility for some bytes. For a successful buffered write(), this usually means that the kernel copied data into the page cache.
Visible means that a later read can observe the new state. A process may read its recently written bytes from memory even though storage has not persisted them.
Ordered means that two storage effects cannot become durable in the wrong order. This matters when one write describes or commits another.
Durable means that acknowledged state is expected to survive the failure model under consideration, such as an operating-system crash or sudden power loss.
Consider a log record followed by a commit marker:
The order of the system calls alone does not necessarily prove that the record will reach persistent media before the marker. Caches and queues can combine or reorder work. If recovery treats the marker as proof that the record is complete, the application needs an appropriate persistence boundary between them.
Visibility is also not durability:
The read can be served from the page cache. It proves that the kernel has the new bytes, not that persistent media has them.
Finally, durability is different from atomicity. An operation can appear indivisible to running processes without yet being crash-durable. A successful rename() is an important example: it can atomically change which file a name refers to, but persisting that name change requires an additional step.
Loading simulation...
A buffered write can pass through several layers:
Each arrow represents a different transfer of responsibility.
An application-level library can add another buffer before the system call. For example, C's fwrite() may leave data in a FILE buffer. Calling fflush() moves that data toward the kernel, but does not make it durable.
With ordinary buffered I/O, write() commonly copies data into the page cache and marks the affected pages dirty. Background write-back eventually sends those changes through the filesystem and block layer.
The storage device may then acknowledge writes into a volatile device cache. Such a cache loses its contents if power disappears, unless it has working power-loss protection.
A durability operation must account for all relevant layers. Moving data out of only one cache is not sufficient.
write() MeansThe return value from write() reports how many bytes the kernel accepted:
If written equals length, the entire request was accepted. That does not by itself mean the bytes reached persistent media.
write() can also return a positive value smaller than length. Signals, resource limits, lack of space, and other conditions can produce a partial write. Correct code must continue from the first unwritten byte:
Completing this loop answers one question: did the kernel accept all the bytes? It still does not answer whether they are durable.
Some storage errors are discovered only during later write-back. A write() may succeed while dirty data is in memory, followed by an error when the filesystem tries to send it to storage. Linux can report such a write-back error through a later write() or fsync().
This delayed reporting makes synchronization calls important for correctness as well as persistence. They are a point at which the application asks the kernel to finish outstanding work and report whether it succeeded.
close() is not enoughApplications sometimes assume that closing a file makes its contents durable:
Closing releases the file descriptor. It is not a general substitute for fsync(), and the kernel may continue write-back after close() returns.
Code should still check close() because it can report an error. However, successfully closing an ordinary file descriptor does not establish the same durability guarantee as successfully synchronizing it.
fflush() is not enoughFor a C standard I/O stream, there are two separate buffers to consider:
Two separate flushes are needed. fflush() only moves data from the library into the kernel, and fsync() is what pushes it past the durability boundary.
A typical sequence is:
fflush() is needed first so that bytes still held by the C library are handed to the kernel. fsync() can synchronize only data the kernel knows about.
fsync(): Synchronizing a FileThe interface is simple:
For a regular file, fsync() asks the kernel to write the file's modified data and associated metadata to persistent storage. It waits until the storage device reports completion, including the cache-flush operations needed under the device's advertised contract.
On success, it returns 0. On failure, it returns -1 and sets errno.
The important phrase is under the storage stack's contract. The kernel issues the operations required by the filesystem and the device's reported capabilities. If a device falsely reports that data is power-safe, software above it cannot manufacture a stronger guarantee. Remote filesystems and virtualized storage can also have additional persistence boundaries behind the system that receives fsync().
For normal local storage that correctly implements its advertised behavior, a successful fsync() is the application-visible operation for establishing file durability.
A file is more than its content bytes. Its metadata includes information such as size, ownership, permissions, timestamps, and the mapping from file offsets to allocated storage.
If an application extends a file from 4 KiB to 12 KiB, persisting only the new data blocks would not be useful if the durable file size remained 4 KiB. fsync() therefore includes the file metadata needed to preserve the synchronized file state, as well as other changed file metadata.
The call applies to the file referenced by the descriptor. It does not automatically make every surrounding filesystem change durable. In particular, synchronizing a file does not necessarily synchronize the directory entry that gives the file its name.
Durability-sensitive code must check the return value:
Possible failures include an I/O error or space-allocation failure discovered during write-back. After a failure, the application should not claim that the entire update is durable. Some effects may already have reached storage, so treating the operation as if it definitely did nothing is also unsafe.
This creates an uncertain outcome that the application must resolve using its own format, recovery procedure, or higher-level transaction protocol. Blindly retrying the same logical operation can duplicate a non-idempotent update.
fdatasync(): Synchronizing Data with Less Metadatafdatasync() has the same interface shape:
It synchronizes file data and the metadata required to retrieve that data correctly. It may omit metadata changes that are not necessary for data access.
Suppose an application overwrites bytes inside an existing file without changing its length. A timestamp update may not be required to retrieve those bytes after a crash. fdatasync() can avoid forcing that timestamp update.
If the application appends data and increases the file size, the new size is required to retrieve the appended bytes. fdatasync() must persist that metadata too. It is therefore inaccurate to say that fdatasync() writes data but never metadata.
The practical performance difference between fdatasync() and fsync() depends on the filesystem, workload, and which metadata changed. Applications should choose based on the guarantee they require and then measure on their actual storage stack.
| Operation | What successful completion establishes |
|---|---|
write() | The returned number of bytes was accepted; persistence is not implied |
fdatasync(file_fd) | File data and metadata required to retrieve it have been synchronized |
fsync(file_fd) | File data and associated changed file metadata have been synchronized |
fsync(directory_fd) | Changes to that directory, such as a created or renamed entry, have been synchronized |
Neither fsync() nor fdatasync() turns an arbitrary sequence of changes across multiple files into an atomic transaction. Each call has a specific object and persistence role.
Storage devices often use write-back caches to improve performance. A device can accept a write into fast internal memory, report completion, and place the data on its final media later.
That acknowledgment is sufficient only if the cache itself meets the required failure model. A cache protected by a battery or capacitor may count as nonvolatile under its documented guarantees. An ordinary volatile cache does not survive sudden power loss.
The operating system uses storage commands to manage this distinction. Two important concepts are cache flushes and force-unit-access writes.
A cache flush tells the device to make previously completed writes persistent before reporting the flush complete.
Conceptually:
Linux's block layer can attach a pre-flush requirement to an I/O operation. The block layer ensures that previously completed writes reach nonvolatile storage before the flagged operation begins.
A force-unit-access, or FUA, write asks the device to place that particular write on nonvolatile storage before reporting it complete. If a lower layer does not support FUA directly, the block layer may implement the required effect with an additional cache flush.
Filesystems combine ordering rules, flushes, and FUA as needed. An application normally requests the high-level guarantee through fsync() or fdatasync() rather than issuing raw device commands.
The term write barrier is commonly used for a constraint that separates groups of writes and prevents persistence ordering from crossing the boundary.
For example, a storage format may require:
A barrier is a semantic ordering requirement, not necessarily one literal command sent to every device. The filesystem and block layer translate the requirement into the operations supported by the underlying storage stack.
These ordering guarantees matter because “both writes eventually complete” is weaker than “the first write is persistent before the second is allowed to become persistent.”
A directory stores mappings from names to files. The file's contents and the directory entry that names it are separate persistent state.
Suppose a program creates orders.log, writes data, and synchronizes the file:
The file contents may be durable while the new orders.log directory entry is not. After a crash, the application could fail to find the file by that name.
To make the creation durable, synchronize the parent directory too:
On Linux, an application can open a directory and pass its descriptor to fsync():
The file synchronization and directory synchronization protect different facts:
Creating, deleting, or renaming a file changes a directory. If surviving a crash requires that namespace change, the relevant directory must be synchronized.
When a rename moves an entry between two different directories, both directory mappings change. Durability-sensitive code should synchronize both affected directories. If the source and destination are the same directory, one directory synchronization covers that directory's changes.
Directory synchronization support and remote-filesystem guarantees can vary. Robust code checks the result rather than assuming success.
Configuration files, checkpoints, and small state files are often replaced as a complete unit. Overwriting the destination in place can leave it truncated or partially updated after a crash.
A common Linux pattern is:
fsync() on the temporary file.fsync() on the parent directory.The sequence can be expressed with descriptor-relative operations:
Real code should generate an unpredictable or collision-resistant temporary name, clean up an unpublished temporary file when safe, preserve intended permissions, and close the directory descriptor. The temporary file must be on the same filesystem as the destination so that rename() can perform an atomic replacement.
Each step has a purpose:
fsync the temporary file, making the new contents durable.fsync the parent directory, making the new name mapping durable.Step 4 is the one most often skipped. Without it the rename can be lost even though the file contents survived.
The rename() provides runtime atomicity for the name replacement: observers do not see a moment when the destination name is simply absent. It does not, by itself, prove that the replacement will survive a crash.
Reversing the middle steps is unsafe. If the program renames the temporary file before synchronizing its contents, the durable directory entry could refer to data that was not made durable first.
Linux also provides open flags that request stronger completion behavior for each write.
O_SYNC gives writes synchronized I/O completion semantics that include the relevant file data and metadata. O_DSYNC focuses on file data and the metadata required for retrieving it, similar in intent to the distinction between fsync() and fdatasync().
These flags can simplify a workload that truly requires every individual write to cross a durability boundary. They can also be expensive because the application loses opportunities to combine several updates under one synchronization operation.
O_DIRECT is unrelated to this guarantee. It changes page-cache participation, not durability. A direct write can still finish while data is in a volatile device cache, so direct-I/O applications need appropriate synchronization semantics too.
Waiting for durable storage has a real latency cost. Calling fsync() after every small record serializes the workload around that cost:
An application can instead batch several records:
One successful synchronization can make all preceding writes to the file durable. This technique is often called group commit when multiple logical operations share one durability event.
Batching improves throughput, but it changes acknowledgment latency and the amount of recent work that can be lost before the synchronization completes. The application must define exactly when it tells each caller that an operation is committed.
For example, a service can queue ten requests, write their records, call fsync(), and acknowledge all ten only after it succeeds. It receives the throughput benefit without falsely acknowledging operations before the durability boundary.
The correct batch size is a policy decision based on latency goals, throughput, and the acceptable interval between durable commits. It should be measured on the actual filesystem and storage stack.
System-call tracing can confirm whether a program issues the operations its design requires:
For a durable replacement, the important portion might look like:
This trace verifies the application's system-call sequence. It does not prove that every physical device below the kernel honestly implements its advertised persistence behavior.
Timing the calls is also useful. A fast write() followed by a much slower fsync() is a normal sign that the write was initially absorbed by buffering and the synchronization call waited for outstanding storage work.
A successful write() reports how many bytes the kernel accepted. With buffered I/O, those bytes can remain dirty in the page cache, and a successful read can observe them before they are persistent.
fsync() synchronizes a file's data and associated metadata, while fdatasync() can omit metadata that is unnecessary for retrieving the data. Both rely on filesystem ordering, block-layer flushes, and device persistence mechanisms to cross volatile caches. Applications must check synchronization errors before reporting an update as durably committed.
File contents and directory entries are separate durability concerns. After creating or renaming a file, synchronize the affected parent directory. For durable replacement, write and synchronize a temporary file, rename it over the target, and then synchronize the directory.
5 quizzes