Opening a regular file with O_DIRECT can make the address of a user-space buffer, the file offset, and the transfer length observable parts of the file interface. A read() or write() that is otherwise valid may fail with EINVAL when one of those values violates the direct-I/O constraints for that file. On some combinations of filesystem and kernel behavior, a misaligned operation can instead use buffered I/O.
That boundary is easy to miss because ordinary buffered file I/O largely hides physical transfer geometry. The page cache and filesystem can accept an application buffer at an arbitrary address and mediate the transfer internally. Direct I/O reduces that mediation, so constraints that normally remain below the system-call boundary can become requirements on application memory and request shape.
O_DIRECT is therefore not a portable synonym for an uncached file descriptor. On Linux it is a file-status flag with filesystem-dependent behavior, alignment rules that can vary by file and kernel version, and no standalone durability guarantee.
Alignment is a property of the active I/O path
Linux documents three values that can be constrained for direct I/O: the memory address of each user-space buffer, the file offset, and the length of each I/O segment. The required multiples are not fixed by the O_DIRECT flag itself.
A program that embeds a constant such as 4096 into its allocation and request logic is encoding an assumption about the environment rather than a general Linux rule. Historical filesystem behavior, logical block sizes, filesystem implementations, and newer per-file reporting can produce different requirements. Some files can also lack direct-I/O support entirely.
The contract can be represented as three independent predicates:
buffer address % memory_alignment == 0
offset % offset_alignment == 0
length % offset_alignment == 0The exact values must come from the applicable interface rather than from that diagram. A buffer can satisfy the memory requirement while the requested offset does not, or an aligned offset can accompany a transfer length that violates the same offset/length boundary.
This makes alignment more than an allocator concern. Request slicing, retry logic, file-format layout, and tail handling can all affect whether a direct operation remains admissible.
statx() can expose the per-file contract
Since Linux 6.1, the STATX_DIOALIGN request can ask statx() for direct-I/O alignment information. When the filesystem supplies it, stx_dio_mem_align reports the required alignment for user memory and stx_dio_offset_align reports the required alignment for file offsets and I/O segment lengths.
A zero direct-I/O memory alignment indicates that direct I/O is not supported for the file through this reporting contract. Support for reporting these fields is itself filesystem-dependent, so absence of reported data is not a license to invent a fallback multiple.
This interface changes the shape of robust direct-I/O setup. The application can treat alignment as discovered file metadata when the filesystem exposes it, rather than coupling its buffer allocator to a machine-wide constant.
There is also a newer distinction for reads. Filesystems that support STATX_DIO_READ_ALIGN can report stx_dio_read_offset_align, allowing the offset and length requirement for direct reads to be less restrictive than the general direct-I/O offset alignment. A zero value in that field means the general offset alignment still applies to reads.
The important boundary is capability reporting. A program must distinguish a reported constraint from an assumed one, because the kernel interface does not define one universal direct-I/O alignment for every regular file.
Aligned allocation does not make arbitrary slices aligned
Allocating a suitably aligned region establishes a property of its base address. It does not preserve that property for every pointer derived from the region.
Suppose an allocator returns a buffer whose address is aligned to 4096 bytes:
void *base = NULL;
int rc = posix_memalign(&base, 4096, 8192);If 4096 is in fact the required memory alignment for the target file, base satisfies that requirement. The pointer (char *)base + 1 does not. A queue or parser that advances a buffer pointer by an arbitrary byte count can therefore turn an admissible direct-I/O buffer into a misaligned one without changing the underlying allocation.
The same issue appears when one large aligned arena is partitioned among concurrent requests. Each request start must preserve the required alignment. An arena with an aligned base does not make densely packed variable-size records suitable as direct-I/O buffers.
Scatter/gather interfaces add the same constraint at segment granularity where direct I/O requires aligned I/O segments. Buffer ownership and buffer geometry become coupled: code that is free to slice ordinary memory may need a stricter representation for buffers entering the direct-I/O path.
File tails expose the difference between logical size and transfer geometry
A file’s logical length does not have to be a multiple of its direct-I/O alignment. This creates a boundary at the final partial region.
For reads, an application cannot safely infer that every request ending at end-of-file can be expressed as an arbitrary short direct read. The requested offset and segment length still have to satisfy the applicable direct-I/O rules. The filesystem’s documented behavior and the reported alignment determine which requests are valid.
For writes, padding a request merely to satisfy alignment can change file contents or logical length if the application does not separate physical transfer shape from its data model. Storage formats that control their own block layout can account for this explicitly. General file editors and byte-oriented update paths often have a less natural fit.
A design that requires arbitrary byte-range mutation may therefore need a buffered path for boundary regions, a format whose extents are alignment-compatible, or another explicit policy. The choice is part of the data interface, not an incidental system-call flag.
Direct I/O and durability are separate contracts
O_DIRECT attempts to minimize cache effects for file I/O and transfers data directly between storage-facing kernel paths and user-space buffers where the filesystem supports that model. It does not provide the persistence guarantees associated with O_SYNC merely because the page cache is bypassed or reduced.
This distinction matters for write protocols. A successful direct write must not be promoted into a crash-durability claim unless the application also uses the synchronization semantics required for that claim. Linux documentation explicitly separates O_DIRECT from O_SYNC; applications that require synchronous persistence need the relevant synchronization contract in addition to direct I/O.
The two flags answer different questions:
O_DIRECT -> cache interaction and transfer path
O_SYNC -> synchronized completion semanticsCombining them can be appropriate for a particular storage design, but one does not substitute for the other.
Mixing buffered and direct access creates a coherence boundary
Linux documentation advises against mixing O_DIRECT and normal buffered I/O to the same file, especially for overlapping byte ranges. The filesystem has to coordinate direct transfers with cached state, and the resulting path can lose the simplicity that motivated direct I/O in the first place.
File-backed mappings introduce a related boundary. Mixing mmap() access with direct I/O to the same file requires coherence between mapped cached pages and direct transfers. Even where the filesystem maintains correct coherence, the access pattern is no longer equivalent to an isolated direct-I/O stream.
This is a semantic concern before it is a performance concern. If two components access the same bytes through different caching paths, each component’s assumptions about visibility must match the filesystem’s coherence behavior. A direct-I/O descriptor does not create a private version of the file.
The safest abstraction boundary is often ownership of a byte range or of the whole file. When one subsystem controls direct access, its alignment, caching, and synchronization policy can remain internally consistent. Shared access requires those policies to become part of the interface between subsystems.
Network filesystems can move the cache boundary
The name O_DIRECT can suggest a path directly to physical media, but that model does not hold across every filesystem. For NFS, the Linux client can bypass its own page cache while the remote server may still cache the request. The flag cannot be transmitted as a universal storage-device instruction through the protocol.
This illustrates the broader limit of the abstraction. Direct I/O describes behavior at a particular filesystem and kernel boundary. It does not establish the cache architecture of every layer beneath that boundary, including remote servers, storage controllers, or devices.
Claims about cache bypass must therefore name the layer being discussed. A client-side page-cache property is not automatically a server-side cache property, and neither property alone establishes persistence on stable media.
Failure handling must preserve the mode distinction
Misalignment is especially hazardous when an application treats direct I/O as a correctness property rather than an optimization. Linux behavior for misaligned direct I/O is not uniformly defined as one error path across filesystems and kernel versions: an operation can fail with EINVAL, while some cases can fall back to buffered I/O.
A design that requires a strict no-buffered-I/O invariant cannot infer that invariant solely from successful completion. It needs an environment and filesystem contract that supplies the required semantics, plus validation appropriate to that contract.
Conversely, an application using direct I/O only as an optional performance mode can treat unsupported capability or unsuitable alignment as a reason to select a buffered path. That policy should be explicit because the two modes have different allocation, slicing, and cache-interaction constraints.
The durable engineering boundary is not the spelling of O_DIRECT. It is the set of conditions under which a particular file, filesystem, kernel, buffer layout, offset, and request length form a valid direct-I/O operation. Once those conditions become explicit, alignment failures and mode changes become interface states rather than mysterious storage errors.