A database reads an 8 KiB page into its own buffer pool. With ordinary file I/O, Linux may also retain the same file range in the page cache. The data can now occupy memory in both the database and the kernel.
That duplication is often useful for general applications because the page cache provides reuse, read-ahead, and buffered writes automatically. A database with its own carefully managed cache may prefer to make those decisions itself.
Linux provides O_DIRECT for this kind of specialized workload. Direct I/O tries to transfer file data between application memory and the storage path while minimizing page-cache involvement.
The word “direct” is easy to overinterpret. It does not mean the application talks directly to hardware, that no copies can occur anywhere, or that completed writes are automatically durable.
Direct I/O bypasses the page cache for file data; it does not bypass the operating system or the rest of the storage stack.
Choosing between buffered and direct I/O is therefore a choice about caching, memory ownership, alignment, and performance, not a choice between “slow” and “fast.”
A file write can pass through three distinct buffering layers before reaching persistent media.
Each layer solves a different problem.
An application or language runtime may collect data in user space before making a system call.
The C standard I/O library is a familiar example:
fwrite() may copy bytes into a buffer managed by the C library. When that buffer fills or the application flushes it, the library issues one or more kernel writes.
Application buffering can:
Flushing this layer means handing bytes to the kernel. It does not mean the device has stored them persistently.
Languages and frameworks expose similar buffering through buffered streams, writers, encoders, and logging libraries.
With ordinary buffered I/O, the kernel caches file contents in RAM.
A buffered write commonly copies bytes from the user buffer into the page cache and marks the affected cache state dirty. The system call can return before the changed pages are written to the device.
A buffered read first checks the page cache. A hit can be served from RAM, while a miss loads the requested file contents into the cache for possible reuse.
This layer provides caching across processes, read-ahead, write grouping, and memory-pressure reclaim without requiring application-specific code.
Storage controllers and devices can have their own memory or fast internal write regions. They use this capacity to queue commands, combine work, correct media behavior, and improve throughput.
The operating system does not manage this cache as part of the page cache. It communicates with the device through storage commands and relies on the device's reported behavior.
O_DIRECT does not bypass this device layer. It changes how file data travels through host memory, not how the controller internally manages its media.
Buffered I/O is the ordinary file I/O path on Linux. The application does not pass O_DIRECT when opening the file.
For a buffered read:
Both paths end with a copy into the caller's buffer. Only the miss path touches storage, and both leave the data cached for next time.
The cached copy remains available after the call. Repeated reads can avoid storage, and sequential access can benefit from read-ahead.
For a buffered write:
write(fd, user_buffer, length) is called.The page cache lets a short write burst run faster than the storage device. Sustained throughput must eventually match the device, and writers can be throttled when dirty memory grows too quickly.
Buffered I/O is particularly effective when:
The page cache also makes simple code perform reasonably across many storage devices. An application can use ordinary read() and write() without knowing the device's DMA or alignment requirements.
The same generality has costs.
Data normally moves between an application buffer and the page cache. Cached file data consumes RAM, even when an application maintains another copy. Read-ahead can fetch data the application never uses, and a large scan can displace a more valuable cached working set.
Write latency can also become less predictable. Early writes may be absorbed by memory, while later writes slow when dirty-page thresholds apply backpressure.
These are trade-offs, not defects. The page cache is optimized for broad system-wide usefulness rather than one application's private knowledge.
O_DIRECTOn Linux, an application requests direct I/O by passing O_DIRECT to open():
O_DIRECT is Linux-specific rather than a portable POSIX guarantee. Support and exact behavior vary by filesystem, file type, and kernel version.
For a direct read, the intended path is:
For a direct write, the arrows run from the application buffer toward the device.
File data is not first installed in the page cache as part of this direct transfer. The kernel still validates the request, resolves filesystem mappings, constructs block I/O, invokes a driver, and handles completion.
The user buffer contains virtual addresses meaningful inside the process. A device cannot safely receive an arbitrary pointer and access it without kernel involvement.
For direct I/O, the kernel may:
Direct I/O removes page-cache data buffering from the normal path. It does not remove kernel work.
A device may transfer data between storage and application-backed pages using DMA, avoiding a separate copy through the page cache.
That does not guarantee that no copy occurs anywhere. The kernel or a lower storage layer may need bounce buffers, transformations, or other intermediate work. Network filesystems and virtual storage can add more layers.
The accurate claim is narrower:
Direct I/O can avoid the ordinary application-to-page-cache copy and duplicate cached file data.
O_DIRECT describes the data path, not how the calling thread waits.
A process can issue a blocking pread() against an O_DIRECT descriptor, or use an asynchronous interface with direct I/O. Buffered I/O can also be submitted through synchronous or asynchronous APIs.
Caching policy and completion model are separate decisions.
Bypassing the page cache does not imply that a completed write has reached power-safe media. The device may have a volatile cache, and filesystem metadata may require separate handling.
O_DIRECT should be treated as a caching and performance option. Stronger persistence guarantees require separate ordering and completion semantics.
Direct I/O often requires alignment in three places:
Suppose a file reports:
Then this request is aligned:
These are not:
The invalid values are not multiples of the stated requirement.
There is no universal rule that every O_DIRECT request must use 4 KiB alignment.
Restrictions vary with:
A misaligned request may fail with EINVAL. On some filesystems or configurations, it may instead fall back to buffered I/O. Code must not assume that one observed behavior applies everywhere.
statxSince Linux 6.1, filesystems can report direct-I/O alignment through statx() and the STATX_DIOALIGN request bit.
When supported:
stx_dio_mem_align reports the required user-buffer alignment.stx_dio_offset_align reports the required file-offset and I/O-length alignment.A simplified query looks like:
Support varies by filesystem. A zero alignment can indicate that direct I/O is unsupported for that file. If the returned mask does not contain the requested information, the application needs filesystem-specific knowledge or a safe fallback.
Newer kernels can also expose a distinct alignment for direct reads on filesystems that support it. Portable application logic must check which fields the running kernel and filesystem actually return.
Ordinary malloc() provides alignment suitable for normal C objects, not necessarily for direct I/O.
posix_memalign() can request a stronger alignment. Assume the system has already reported 4096-byte memory and offset alignment for the target file:
Compile on Linux:
The target file must support O_DIRECT, be at least large enough for the requested read, and satisfy the assumed alignment requirements. Production code should discover or configure those requirements rather than hardcoding 4096.
The essential difference can be summarized as:
The direct path removes one copy and the cache along with it. Nothing is retained for a second reader.
And for writes:
The buffered write can return before any storage work happens. The direct write cannot defer it to a later stage in the same way.
The shorter arrows do not prove lower latency. Buffered I/O can complete from RAM, combine work, and anticipate sequential access. Direct I/O may have to wait for storage on a cache miss every time.
| Concern | Buffered I/O | Direct I/O |
|---|---|---|
| File-data page cache | Used | Bypassed for the direct transfer |
| Repeated-read caching | Automatic | Application must provide it if desired |
| Read-ahead | Kernel can provide it | Application plans its own access |
| Alignment | Ordinary byte-oriented API | File-specific restrictions often apply |
| Small writes | Can be absorbed and combined | Often require application batching |
| Memory ownership | Kernel and application may both cache data | Application has more explicit control |
| Device cache | May still be used | May still be used |
Neither column is universally better. The right path depends on which layer has the best information to manage caching and request shape.
Loading simulation...
Buffered I/O is normally the best default for general file access.
Applications that reread files benefit from the page cache automatically. Multiple processes can share cached file contents without coordinating their own cache.
Configuration files, executables, shared libraries, source trees, and frequently accessed static assets commonly benefit from this behavior.
An application can update a small byte range without arranging aligned memory or constructing a complete aligned block. The page cache preserves surrounding contents and later organizes storage work.
This simplicity matters for workloads that do not naturally operate in fixed-size aligned units.
The kernel can detect sequential reads and perform read-ahead. It can also group nearby dirty data during write-back.
Applications get these optimizations without managing queue depth and future requests directly.
If several processes access the same file, the page cache avoids maintaining one private copy per process.
A separate application cache can still exist, but it should provide value the system-wide file cache cannot, such as decoded objects or domain-specific indexing.
Direct I/O is useful when an application has enough information and engineering effort to replace the services it bypasses.
A database knows which pages belong to hot indexes, active transactions, scans, and background maintenance. It can use that semantic information to choose what remains in memory.
The kernel sees only file regions and access history. It cannot know that one 8 KiB page is a root index page while another belongs to a one-time scan.
If a database keeps a 20 GiB buffer pool and the kernel also caches much of the same database file, two layers compete for RAM while holding duplicate bytes.
Direct I/O can make the database buffer pool the primary cache for file data and leave more predictable memory for its chosen working set.
A backup or analytics scan may read a large dataset once. Direct I/O can prevent that one-time stream from displacing frequently reused file pages in the page cache.
This helps only if the application issues efficient requests itself. Thousands of tiny direct reads can be worse than a buffered sequential scan with read-ahead.
An application using direct I/O can choose request sizes, queue depth, and eviction policy together.
That control is useful for storage engines designed around fixed-size pages and asynchronous request submission. It also increases complexity: the application must handle alignment, batching, concurrency, and caching deliberately.
Buffered I/O allows the kernel to grow and reclaim file cache dynamically. Direct I/O reduces file-data page-cache residency for that path, making the application's own buffers a larger and more visible part of memory usage.
Metadata and other kernel caches still exist, so direct I/O does not make all kernel memory disappear.
O_DIRECT is not a performance upgrade switch.
It can lose several optimizations:
Direct I/O works best with appropriately sized, aligned, and concurrent requests. A single thread issuing one small direct read and waiting before issuing the next can leave a fast device underutilized.
Buffered I/O can be faster when the working set fits in memory, reuse is high, or the application would otherwise reproduce the kernel's caching poorly.
The only reliable comparison is under the real request sizes, concurrency, cache state, and read/write mix.
Database systems commonly manage a buffer pool containing fixed-size database pages.
The database knows:
This knowledge is richer than the kernel's observation that a file offset was recently accessed.
Using direct I/O can prevent the operating system from keeping a second page-cache copy of the same data. The database gains explicit control over memory capacity, eviction, and request concurrency.
That does not mean every database uses direct I/O for every file. Choices can differ among data files, logs, temporary files, operating systems, and deployment configurations. Some systems intentionally rely on the page cache.
The important principle is:
Direct I/O is valuable when application-managed caching is more informed than generic file caching and the application can meet the operational requirements.
It is not valuable merely because the application is called a database.
Using buffered and direct I/O on overlapping regions of the same file creates a coherence problem.
Suppose buffered I/O has a dirty page-cache copy while direct I/O targets the same file range. The kernel and filesystem must prevent the direct operation and cached copy from silently diverging.
Maintaining coherence can require waiting for write-back, invalidating cached pages, or serializing operations. These steps can eliminate the expected performance benefit.
Linux documentation therefore recommends avoiding mixtures of:
O_DIRECT and ordinary buffered I/O to overlapping file regionsEven when the filesystem preserves correctness, throughput can be worse than consistently using either path alone.
An application should choose clear ownership for a region rather than casually alternating modes.
fork() Edge CaseLinux places a special restriction on direct I/O that is outstanding while a process calls fork().
If an O_DIRECT operation uses a buffer from private memory, such as the heap, static storage, or a MAP_PRIVATE mapping, the operation should finish before another thread calls fork().
The kernel may have pinned and mapped those pages for device access. Concurrently creating copy-on-write process memory can make ownership ambiguous and can lead to undefined behavior or data corruption.
Safe designs do one of the following:
fork()MADV_DONTFORK when excluding them from the child is validThis edge case matters in servers that launch child processes while background threads perform direct I/O.
File I/O can be buffered at three layers: the application or runtime, the kernel page cache, and the storage device. Flushing or bypassing one layer does not bypass the others.
Buffered I/O uses the page cache to provide shared caching, read-ahead, write grouping, and flexible byte-oriented operations. Direct I/O with O_DIRECT minimizes page-cache effects and transfers file data using aligned application buffers, but the request still passes through the filesystem, block layer, driver, and controller.
Direct-I/O buffer addresses, file offsets, and lengths may need file-specific alignment. Linux can report requirements through STATX_DIOALIGN where supported. Direct I/O is most useful when an application, often a database, already manages caching, batching, and concurrency more deliberately than the generic page cache. It is a specialized performance tool, not an automatic speed or durability guarantee.
5 quizzes