This question tests whether you can trace a request through the whole stack. The interviewer wants the page cache — not "the disk returns the bytes."
The mental model: a read is a request for bytes, and the kernel's job is to satisfy it with as little I/O as possible. The page cache is the layer that makes most reads never touch the disk at all.
Walk through the mechanism. fread hits libc's stdio buffer first; if the buffer has data, the kernel is never involved. On a miss, libc issues read(fd, buf, count). The syscall looks up the fd in the process's file descriptor table, which points to a struct file — holding the current offset and flags — which points to the inode for the file. The read is then: find the pages covering [offset, offset+count) in the page cache.
Cache hit: the pages are resident, the kernel copies them to your buffer with copy_to_user and returns. No I/O, no disk, roughly a microsecond per 4KiB page. Cache miss: the kernel must get the page from storage. It issues a read to the address space (readpage), which becomes a bio down through the block layer — I/O scheduler, device driver, DMA — and the thread sleeps until the completion interrupt wakes it. The data lands in the page cache first, then is copied out to the user buffer. Re-reads of the same bytes are then cache hits.
Reads don't just serve your request: the kernel does readahead — it predicts you'll keep reading sequentially and prefetches the next several pages into the cache in the background, so sequential reads amortize the latency of the first miss.
Tradeoffs and edge cases worth naming:
O_DIRECTbypasses the page cache: the DMA lands in your buffer. Zero copy, but you lose caching and readahead — right for large one-shot I/O, wrong for hot data.- The double copy: disk → page cache → user buffer.
mmapremoves the second copy by mapping the cache page directly into your address space. - Page cache eviction under memory pressure drops clean pages first, forcing later re-reads back to disk — which is why a cold cache is a different workload.
- Concurrent readers of the same file share cache pages, but their
struct fileoffsets are independent — a shared offset requirespreador locks.
A strong closing line: reading a file is a page-cache lookup in disguise — the syscall and fd machinery are just the route to the cache, and the disk is only involved when the cache misses.