Direct IO and O_DIRECT
O_DIRECTis the open flag that tells the kernel “do not cache this file’s data — move it by Direct Memory Access (DMA) straight between my user buffer and the storage device.” A normal bufferedread()/write()stages every byte through the page cache; anO_DIRECTI/O skips that staging entirely, so the bytes never occupy kernel page-cache memory and the CPU never does the page-cachememcpy. The trade is steep: the application loses all kernel caching, readahead, and write-behind, and in exchange must satisfy strict alignment rules (offset, length, and buffer address must be multiples of the device’s logical block size) or the I/O fails withEINVAL. Databases want this precisely because they implement a better cache than the kernel’s generic one and need predictable, double-buffer-free I/O. This note coversO_DIRECTsemantics, the alignment contract, the modern iomap direct-I/O engine (iomap_dio_rw) versus the legacyfs/direct-io.c, the per-I/ORWF_*flags, and the pitfalls — pinned to Linux 6.12 LTS (fs/iomap/direct-io.c, v6.12) andopen(2)/statx(2)as documented in the Linux man-pages.
Mental Model
The cleanest way to think about O_DIRECT is as a second, parallel data path that branches off the moment the read or write enters generic_file_read_iter / its write twin. The buffered path goes “syscall → page cache → (on miss) block layer,” with the page cache as an obligatory waypoint. The direct path goes “syscall → filesystem maps file range to device range → build a bio that points directly at the user’s pages → block layer,” with no page cache in between. The filesystem’s only job on the direct path is the mapping — translating the file offset to one or more on-device extents — which is exactly what the iomap library was built to do.
Two consequences fall straight out of this picture. First, because there is no intermediate kernel buffer, the user’s memory is the DMA target, so it must be aligned the way the hardware demands. Second, because the kernel is no longer the single source of truth for the file’s bytes, O_DIRECT and buffered I/O to the same file are not automatically coherent — you can have stale cached pages shadowing freshly direct-written blocks unless the kernel explicitly reconciles them (which it does, carefully, on the boundaries).
flowchart TD S["read()/write() on O_DIRECT fd<br/>(iocb->ki_flags |= IOCB_DIRECT)"] --> G["generic_file_read_iter /<br/>write twin"] G -->|"IOCB_DIRECT set"| WW["write back + wait on<br/>dirty cached pages over range<br/>(kiocb_write_and_wait)"] WW --> DIO["a_ops->direct_IO<br/>= iomap_dio_rw (modern)"] DIO --> IT["iomap_iter: map file range<br/>→ device extents"] IT --> AL{"pos|length aligned to<br/>logical_block_size?<br/>buffer aligned?"} AL -->|"no"| EINVAL["return -EINVAL"] AL -->|"yes"| BIO["build bio over USER pages<br/>(no page-cache copy)"] BIO --> BLK["block layer → device DMA"] G -. legacy fs .-> OLD["a_ops->direct_IO<br/>= __blockdev_direct_IO<br/>(fs/direct-io.c)"]
The direct-I/O path. What it shows: an O_DIRECT request branches at generic_file_read_iter on the IOCB_DIRECT flag, flushes any conflicting cached pages, then calls the filesystem’s direct_IO operation — iomap_dio_rw for modern filesystems, the legacy __blockdev_direct_IO for older ones — which maps the file range to device extents, enforces alignment (failing -EINVAL otherwise), and builds a bio aimed straight at the user’s pages. The insight to take: the page cache is genuinely absent from this path, which is the whole point and also the source of every O_DIRECT foot-gun.
Mechanical Walk-through
Where the branch happens
O_DIRECT is recorded on struct file at open time and surfaces on each I/O as iocb->ki_flags & IOCB_DIRECT. On the read side, generic_file_read_iter (mm/filemap.c, v6.12) checks that flag first:
if (iocb->ki_flags & IOCB_DIRECT) {
retval = kiocb_write_and_wait(iocb, count); // flush dirty pages in range
if (retval < 0) return retval;
file_accessed(file);
retval = mapping->a_ops->direct_IO(iocb, iter); // the direct path
...
if (retval < 0 || !count || IS_DAX(inode)) return retval;
if (iocb->ki_pos >= i_size_read(inode)) return retval;
}
return filemap_read(iocb, iter, retval); // possibly finish bufferedThe kiocb_write_and_wait call is the coherency reconciliation: before reading directly from the device, the kernel writes back and waits on any dirty page-cache folios covering the same range, so the direct read does not return stale on-disk bytes that a buffered writer had not yet flushed. Notice the fall-through: a direct read can come up short (Btrfs returns a short direct read across compressed extents) and the code then finishes the remainder through the buffered filemap_read. The write side mirrors this but additionally invalidates clobbered cache pages.
The modern engine: iomap_dio_rw
For ext4, XFS, Btrfs, F2FS, GFS2 and the other modern filesystems, a_ops->direct_IO ultimately drives iomap_dio_rw (fs/iomap/direct-io.c, v6.12; the kernel iomap operations docs define direct I/O as “file I/O that is issued directly to storage, bypassing the pagecache” and state that “the iomap_dio_rw function implements O_DIRECT (direct I/O) reads and writes for files”). For example ext4’s read path is literally iomap_dio_rw(iocb, to, &ext4_iomap_ops, NULL, 0, NULL, 0) (fs/ext4/file.c, v6.12). iomap_dio_rw is a thin wrapper over __iomap_dio_rw + iomap_dio_complete. __iomap_dio_rw sets up a struct iomap_dio and an iomap_iter with flags = IOMAP_DIRECT, then:
- For a read, it flags the dio
IOMAP_DIO_INLINE_COMP(reads can always complete inline), marks the user bufferIOMAP_DIO_DIRTYif it is user-backed (so completion knows to dirty the destination pages), and again callskiocb_write_and_waitto flush conflicting dirty cache. - For a write, it sets
IOMAP_WRITE | IOMAP_DIO_WRITE, decides whether the I/O needs a post-write flush (IOMAP_DIO_NEED_SYNCforO_DSYNC/O_SYNCfiles, with aWRITE_THROUGH/FUA optimization for datasync-only writes), and then invalidates the page cache over the write range withkiocb_invalidate_pages— if that invalidation fails because the pages are pinned, it returns-ENOTBLKso the caller falls back to buffered I/O. - It calls
inode_dio_begin(inode)(tracking outstanding direct I/Os so truncate can wait them out), then loopsiomap_iter(&iomi, ops), where the filesystem maps successive file ranges to device extents, and for each mapped chunkiomap_dio_bio_iterbuilds and submits abio.
The pivotal alignment check lives in iomap_dio_bio_iter:
if ((pos | length) & (bdev_logical_block_size(iomap->bdev) - 1) ||
!bdev_iter_is_aligned(iomap->bdev, dio->submit.iter))
return -EINVAL;This is the alignment gate. pos | length ORs the file offset and the chunk length; ANDing with (logical_block_size - 1) is zero only if both are multiples of the device’s logical block size (typically 512 bytes, sometimes 4096). Separately, bdev_iter_is_aligned verifies the memory buffer addresses in the iov_iter meet the device’s DMA alignment. If either fails, the direct I/O returns -EINVAL — the canonical “you misaligned an O_DIRECT” error. Sub-block heads/tails of an unwritten or newly allocated extent are zero-filled via iomap_dio_zero before the data bio, so a direct write into a hole does not expose uninitialized device contents.
When all bios are submitted, iomap_dio_complete reverts the iterator to the bytes actually transferred, issues the deferred fsync/cache-flush if IOMAP_DIO_NEED_SYNC was set, advances iocb->ki_pos, and returns the byte count (or -EIOCBQUEUED for an async submission completed later via iocb->ki_complete).
The synchronous vs asynchronous split is itself a key part of the engine. __iomap_dio_rw decides wait_for_completion = is_sync_kiocb(iocb) || (dio_flags & IOMAP_DIO_FORCE_WAIT). A plain blocking pread/pwrite on an O_DIRECT fd is a sync kiocb, so the thread sleeps until the device bios finish and the byte count is returned inline. An io_uring/AIO submission is not sync: the call returns -EIOCBQUEUED, and when the final bio completes, the block layer runs iomap_dio_bio_end_io, which (depending on flags) either completes the dio inline in the completion context (IOMAP_DIO_INLINE_COMP, used for reads), lets the submitter finish it to avoid a context switch (IOMAP_DIO_CALLER_COMP, an optimization for writes when the issuer “groks” deferred completion via IOCB_DIO_CALLER_COMP), or punts to a workqueue (inode->i_sb->s_dio_done_wq, lazily created by sb_init_dio_done_wq) when completion needs sleepable context — for example a post-write metadata update for a size-extending write. Either way it ultimately calls iocb->ki_complete(iocb, ret) to notify the async caller. This is why O_DIRECT pairs so naturally with io_uring: the direct path was built to complete asynchronously without ever blocking the submitting thread.
The legacy engine: fs/direct-io.c
Older filesystems — FAT, JFS, ReiserFS, OCFS2, NILFS2, AFFS and others — still route direct_IO through __blockdev_direct_IO in fs/direct-io.c (v6.12, a ~1,300-line library function). It predates iomap and does the same job in a more ad-hoc way: it walks the request a block at a time using the filesystem’s get_block callback to map each file block to a device block, accumulating contiguous device blocks into bios. The kernel is deliberately migrating filesystems off this code onto iomap because iomap maps whole extents per call (far fewer indirect-block lookups, better large-I/O performance) and centralizes the alignment and completion logic. As of 6.12 the legacy path is not removed — it remains the direct-I/O engine for filesystems that have not been converted. Treat “uses iomap” as a property of the specific filesystem, not of O_DIRECT in general.
Discovering the alignment requirement: STATX_DIOALIGN
Historically the only portable way to learn a file’s O_DIRECT alignment was to assume the worst (the filesystem block size) or use filesystem-specific ioctls like XFS’s XFS_IOC_DIOINFO. Since Linux 6.1, statx(2) with the STATX_DIOALIGN mask returns two fields (statx.2, man-pages): stx_dio_mem_align (required alignment of the user memory buffer) and stx_dio_offset_align (required alignment of the file offset and I/O length), each 0 if direct I/O is unsupported on that file. Per the man page, STATX_DIOALIGN works on block devices since 6.1 and on regular files for ext4, f2fs, and xfs since 6.1. This is the correct modern way to size aligned buffers — query it rather than hard-coding 512 or 4096.
Configuration and Code
A correct O_DIRECT reader, with posix_memalign for the buffer and statx to size the alignment:
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <linux/stat.h> /* struct statx, STATX_DIOALIGN */
int fd = open("/data/table.ibd", O_RDONLY | O_DIRECT); /* 1 */
struct statx stx;
statx(fd, "", AT_EMPTY_PATH, STATX_DIOALIGN, &stx); /* 2 */
size_t align = stx.stx_dio_offset_align; /* 3 */
size_t memalign = stx.stx_dio_mem_align;
size_t len = align ? align : 4096; /* 4 */
void *buf;
posix_memalign(&buf, memalign ? memalign : 4096, len); /* 5 */
/* offset MUST be a multiple of align; len MUST be a multiple of align;
buf MUST be aligned to memalign — else read() fails -EINVAL */
ssize_t n = pread(fd, buf, len, /*offset=*/ 0); /* 6 */- Line 1 —
O_DIRECTis OR’d into the open flags; it can be combined withO_RDONLY/O_WRONLY/O_RDWR. On a filesystem with noO_DIRECTsupport,openitself fails-EINVAL(see theopen(2)ERRORS). - Lines 2–4 — query the real alignment instead of guessing;
stx_dio_offset_aligngoverns both the offset and the length,stx_dio_mem_aligngoverns the buffer address. A zero means direct I/O is unsupported for this file. - Line 5 —
posix_memalign(oraligned_alloc, or a hugepage) gives a DMA-aligned buffer; a plainmallocbuffer will usually fail thebdev_iter_is_alignedcheck. - Line 6 — every
preadmust keepoffset,len, andbufaligned, on every call, not just the first.
Per-I/O control without a separate open is available through preadv2/pwritev2’s RWF_* flags (readv.2, man-pages). These are not O_DIRECT substitutes but adjust durability and blocking per call:
RWF_DSYNC(since 4.7) /RWF_SYNC(since 4.7) — per-write equivalents ofO_DSYNC/O_SYNC; force the data (and, forSYNC, metadata) durable before the call returns.RWF_HIPRI(since 4.6) — high-priority polled I/O; “usable only on a file descriptor opened using theO_DIRECTflag,” per the man page — the low-latency polling path is direct-I/O only.RWF_NOWAIT(since 4.14) — return-EAGAINrather than block; meaningful forpreadv2.RWF_APPEND(since 4.16) — per-writeO_APPEND.RWF_NOAPPENDandRWF_ATOMICare both defined ininclude/uapi/linux/fs.hat v6.12 (RWF_SUPPORTEDlistsRWF_HIPRI | RWF_DSYNC | RWF_SYNC | RWF_NOWAIT | RWF_APPEND | RWF_NOAPPEND | RWF_ATOMIC).RWF_ATOMICrequests torn-write protection — the write either lands whole or not at all — which is directly relevant to direct-I/O databases avoiding double-write buffers; it requires the underlying filesystem and device to advertise atomic-write support and is the newest member of this family.
Uncertain
Verify: that
RWF_HIPRIpolling remains restricted toO_DIRECTfile descriptors in 6.12 (the man-page text is from man-pages 6.06 and may predate behavior changes), and the precise filesystem/device support matrix forRWF_ATOMICin 6.12 (the bit is defined in the v6.12 uapi header, but which filesystems honor it, and the exact constraints — single-bio, alignment,statxreporting viaSTATX_WRITE_ATOMIC— were not exhaustively verified here). Reason: behavior detail beyond the consulted man-page/uapi snapshots. To resolve: read the iomap/block atomic-write series andstatx(2)STATX_WRITE_ATOMICdocs at v6.12. uncertain
Failure Modes and Pitfalls
EINVALon misalignment. The single most commonO_DIRECTbug: offset, length, or buffer not a multiple of the logical block size. The(pos | length) & (logical_block_size - 1)gate iniomap_dio_bio_iterrejects it. Peropen(2), misalignedO_DIRECTI/O “can either fail withEINVALor fall back to buffered I/O,” and the requirement was relaxed from the filesystem block size (Linux 2.4) to the device logical block size (Linux 2.6.0).- No caching, ever. A direct read of the same block twice issues two device I/Os; a direct write is not absorbed by write-behind. If the access pattern actually has reuse,
O_DIRECTmakes it slower.O_DIRECTis a win only when the application caches better than the kernel, or when cache pollution itself is the problem (a one-shot backup that would evict the working set). - Coherency with buffered I/O is fragile. Mixing
O_DIRECTand buffered access to the same file is explicitly discouraged in the kernel docs; the kernel reconciles on the I/O boundaries (kiocb_write_and_waitbefore direct reads,kiocb_invalidate_pagesbefore direct writes), but the man page warns the interaction is subtle and applications should not rely on it.mmapof anO_DIRECTfile is the sharpest edge. fork(2)data corruption.open(2)warns thatO_DIRECTI/O must never run concurrently withfork(2)when the buffer is a private (MAP_PRIVATE/heap) mapping: copy-on-write can move the page out from under an in-flight DMA, corrupting data in both parent and child. Complete all direct I/O beforefork, or use aMAP_SHARED/shmatbuffer.O_DIRECTdoes not implyO_SYNC. A successful direct write means the device accepted the data, not that it is durable: as Jeff Moyer’s LWN durability primer notes,O_DIRECTwrites go “directly to the storage” but “the storage may itself store the data in a write-back cache” (Moyer, LWN 2011). Durability still requiresfsync/fdatasyncorO_DSYNC/RWF_DSYNC. See fsync fdatasync and Durability.- Filesystem may silently fall back. Some filesystems (or
tmpfs, or files on filesystems without direct-I/O support) ignore or rejectO_DIRECT; portable code must handle bothopenfailing-EINVALand an absent-alignment (stx_dio_offset_align == 0) result.
Alternatives and When to Choose Them
- Buffered I/O (the default). Choose it for almost everything: the page cache, readahead (Readahead and Read Path), and write-behind are large wins for typical reuse-heavy workloads. Reach for
O_DIRECTonly with a concrete reason. posix_fadvise(POSIX_FADV_DONTNEED)— keep buffered I/O but drop cached pages after use; a softer alternative toO_DIRECTwhen the only goal is to avoid cache pollution, without taking on alignment constraints.O_DIRECT+io_uring— the high-throughput database/storage pattern: aligned buffers, direct I/O, batched and asynchronous via the ring, optionallyRWF_HIPRIpolling on NVMe. See Asynchronous IO Models in Linux and io_uring and the File Path.mmap— a different bypass: it maps file pages into the address space, still through the page cache, with faults driving I/O. Coherent with bufferedread/write, not withO_DIRECT.
Production Notes
O_DIRECT is the default-or-recommended I/O mode for the data files of databases that manage their own buffer pool: PostgreSQL (with io_method/direct-I/O work in recent releases), MySQL/InnoDB (innodb_flush_method=O_DIRECT), Oracle, and many embedded engines. Their reasoning is uniform: a database already keeps hot pages in a tuned buffer pool, so the kernel page cache would only double-buffer the same data, wasting RAM and adding a memcpy, while readahead and write-behind interfere with the engine’s own scheduling and durability guarantees. Kernel maintainers have long been skeptical of O_DIRECT as an interface, and the community’s preferred answer has been to make the buffered path good enough — better posix_fadvise, and an uncached buffered mode RWF_DONTCACHE (added after 6.12, in 6.14 — note it is not in the v6.12 RWF_SUPPORTED set this note verified) — so fewer applications need it; but for self-caching databases it remains the pragmatic choice. The migration of ext4/XFS/Btrfs onto iomap direct I/O (from the legacy fs/direct-io.c) measurably improved large sequential direct-I/O throughput by mapping whole extents per iteration instead of block-by-block. When debugging O_DIRECT performance, STATX_DIOALIGN plus blockdev --getss (logical sector size) tell you the alignment floor, and the iomap_dio_rw_begin/iomap_dio_complete tracepoints let you watch each direct I/O’s flags and completion path.
See Also
- Open Flags and Access Modes — where
O_DIRECTsits amongO_SYNC/O_DSYNC/O_PATH/O_TMPFILEand how the flag reachesiocb->ki_flags. - The iomap Library — the extent-mapping library that backs
iomap_dio_rw. - Asynchronous IO Models in Linux —
O_DIRECTis the natural partner ofio_uring/AIO;RWF_NOWAIT/RWF_HIPRIsemantics. - Readahead and Read Path — the buffered path that
O_DIRECTbypasses. - The Page Cache · The Page Cache and address_space — the cache that is absent on the direct path.
- fsync fdatasync and Durability — why
O_DIRECTstill needs explicit syncing on volatile-cache devices. - io_uring and the File Path — direct I/O’s high-throughput async front end.
- MOC: Linux Filesystems and VFS MOC