2.3 Open, Read, Write, and Writeback
| 中文 | English | Contents |
Chapter 2: FUSE Implementation Analysis · Article 3 of 6
This article follows fs/fuse/file.c through open, data-path selection, cached I/O, direct I/O, writeback, fsync, and release. The key is to understand where path selection occurs and how partial completion is reported upward.
1. fuse_file_operations
Important regular-file entries include:
.open = fuse_open
.read_iter = fuse_file_read_iter
.write_iter = fuse_file_write_iter
.mmap = fuse_file_mmap
.fsync = fuse_fsync
.flush = fuse_flush
.release = fuse_release
fuse_file_aops supplies page-cache read, readahead, writepages, and dirty/writeback behavior. A syscall and a background writeback worker can therefore generate protocol I/O from different entry points.
2. OPEN and per-open state
fuse_open() builds FUSE_OPEN. The daemon returns a file handle and flags in fuse_open_out; the kernel creates a struct fuse_file and attaches it to the VFS file.
Per-open state includes concepts such as:
- daemon
fh; - open flags and poll state;
- references needed by writeback or asynchronous I/O;
- release arguments that keep the daemon handle closable;
- passthrough and extension-specific state.
One inode may have many fuse_file objects, so per-open decisions cannot be modeled as inode-global state.
3. Data-path dispatch
The OPEN response can request direct I/O or cache behavior. The kernel combines those flags with inode DAX state, connection capabilities, and any established backing file.
Conceptually:
read_iter/write_iter
-> DAX inode? -> fuse_dax_*_iter
-> FOPEN_DIRECT_IO? -> FUSE direct-I/O path
-> passthrough available? -> backing-file I/O
-> otherwise -> page-cache path
The source order in fuse_file_read_iter() and fuse_file_write_iter() is authoritative. In the current tree, DAX is tested first, and FOPEN_DIRECT_IO overrides passthrough.
4. Cached read
fuse_cache_read_iter() enters the generic filemap path. Cached folios satisfy the read directly; a miss invokes address-space operations and eventually fuse_send_read().
read(2)
-> fuse_file_read_iter
-> fuse_cache_read_iter
-> filemap_read
-> hit: copy cached data
-> miss: read_folio/readahead
-> FUSE_READ
-> fill folios
Readahead may ask the daemon for more data than the current application request.
5. Cached write and writeback
fuse_cache_write_iter() copies data into the page cache while coordinating file size, timestamps, write serialization, and direct-I/O exclusion. With writeback cache, syscall success does not mean the daemon has received a WRITE.
Later writepages can merge small writes, split large ones, run from reclaim, and report errors only at fsync or another synchronization point. The daemon must honor offsets rather than assuming arrival order matches application syscall order.
6. Direct-I/O loop
fuse_direct_io() divides the iterator according to read/write limits and page capacity, prepares user pages and arguments, then sends each piece.
A simplified shape is:
while (iov_iter_count(iter)) {
nbytes = choose_request_size(iter, limits);
nres = send_one_read_or_write(pos, nbytes);
if (nres < 0)
break;
total += nres;
pos += nres;
if (nres != nbytes)
break;
}
return total ? total : error;
The real path also handles async completion, page release, dirty marking, append, locking, and parallel direct writes.
7. Partial completion hides a later error
Linux iterator I/O usually returns a positive byte count if earlier subrequests succeeded and a later one failed. The errno is returned directly only when zero bytes completed.
For an 8 MiB read split into 2 MiB pieces:
piece 1: 2 MiB success
piece 2: 2 MiB success
piece 3: -ETIMEDOUT
syscall result: 4 MiB, not -ETIMEDOUT
This is a short I/O result, not accidental error loss. The caller consumes the prefix and issues another operation for the remainder. It also means that an implementation cannot generally wait to see a timeout and then replay the original full request on another data path.
8. Short reads versus short writes
A short read can indicate EOF or an early daemon result. A positive short write proves that the prefix may already have side effects; retry must begin at the remaining suffix.
Fallback logic needs to preserve:
- original and current position;
- consumed iterator length;
- submitted and completed lengths;
- any asynchronous operation with unknown final outcome;
- inode size, timestamps, and dirty state already changed.
9. FLUSH, FSYNC, and RELEASE
These are not synonyms:
- FLUSH relates to closing one descriptor and may occur more than once;
- FSYNC waits for relevant dirty data and asks the daemon to persist it;
- RELEASE ends the daemon open-handle lifetime after the final reference.
fuse_release_common() coordinates outstanding writes, async requests, and handle references. VFS release entry does not authorize freeing state still reachable by a completion callback.
10. Writeback error propagation
A background write can fail after the originating write() returned. The kernel records the mapping error and exposes it through a later fsync or related synchronization point. Daemons should return stable diagnostic errnos, while applications must not assume close is the only possible error boundary.
11. Reading and debugging an I/O
Record together:
- VFS entry and
kiocbflags; - DAX/direct/passthrough/cache state;
- initial and final iterator count;
- offset, size, unique, and result of every FUSE piece;
- final syscall result;
- remaining background or asynchronous completions.
One FUSE READ or WRITE is not necessarily one syscall.
12. Summary
file.c dispatches between DAX, FUSE direct I/O, passthrough, and page cache. Readahead and writeback drive cached requests, while direct I/O performs its own splitting. A positive completed prefix normally takes precedence over a later error—the critical return-value rule for timeout fallback designs.
| Previous: Namespace and Metadata Operations | Next: Request Lifecycle and /dev/fuse Transport |