* [PATCH v4 01/10] NFSD: cut a misaligned direct-mode WRITE at page boundaries
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 02/10] NFSD: mark the direct middle of a split WRITE IOCB_DONTCACHE as well Mike Snitzer
` (10 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
nfsd_write_dio_iters_init() splits a misaligned direct-mode WRITE into a
direct middle and buffered ends so that as little of it as possible goes
through the page cache. That only works if direct and buffered I/O never
meet inside a page: the direct middle invalidates the page cache over its
range, so a middle that starts or stops inside the page a buffered end
writes has that page dropped, and whichever of the two neighbouring
WRITEs comes second has to read it back from the device.
The middle was cut at the file system's direct I/O offset alignment:
prefix_end = round_up(offset, offset_align);
middle_end = round_down(orig_end, offset_align);
On 4 KiB pages over 4096-byte blocks that alignment is the page size,
the cut is a page boundary, and the problem cannot arise. It is broken
wherever the page is larger than the alignment: 16 and 64 KiB pages over
4096-byte blocks, and 4 KiB pages over 512-byte blocks. There a
misaligned WRITE costs a read per shared page on top of its three I/Os.
Cut each end of a WRITE on its own:
- an end that is already an offset_align multiple needs no buffered
segment, and the middle starts or stops there, mid-page or not. A
WRITE aligned to the device stays entirely direct at every page size;
- a misaligned end needs a buffered segment whatever is done, and that
segment now runs to the nearest page boundary inside the WRITE.
That is one rule: a misaligned end is cut at a multiple of the larger of
offset_align and PAGE_SIZE, which are both powers of two.
The segments still tile exactly the bytes of the WRITE: a buffered end
grows inward, at the middle's expense, never outward. A misaligned WRITE
that holds no whole page has no middle and is issued as one buffered
segment. Where the offset alignment is a page-size multiple nothing
changes.
nfsd-io-modes.rst said that DONTCACHE is not used for the misaligned
segments, which the write loop already contradicted: every segment
without IOCB_DIRECT gets IOCB_DONTCACHE when the file system supports
it. Replace that sentence with one that says so, next to one that
describes the new cut.
Fixes: 06c5c97293e3 ("NFSD: Implement NFSD_IO_DIRECT for NFS WRITE")
Cc: stable@vger.kernel.org
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Documentation/filesystems/nfs/nfsd-io-modes.rst | 9 ++++++---
fs/nfsd/vfs.c | 13 +++++++++++++
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index 0fd6e82478fe6..1916b2eee58ad 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -126,9 +126,12 @@ Misaligned WRITE:
middle and end as needed. The large middle segment is DIO-aligned
and the start and/or end are misaligned. Buffered IO is used for the
misaligned segments and O_DIRECT is used for the middle DIO-aligned
- segment. DONTCACHE buffered IO is _not_ used for the misaligned
- segments because using normal buffered IO offers significant RMW
- performance benefit when handling streaming misaligned WRITEs.
+ segment. A misaligned start or end segment runs to the nearest page
+ boundary inside the WRITE, not just to the nearest DIO-aligned
+ offset, so the O_DIRECT segment never shares a page with a buffered
+ one; a start or end that is already DIO-aligned gets no buffered
+ segment at all. If the filesystem supports FOP_DONTCACHE, the
+ misaligned segments are marked DONTCACHE.
Tracing:
The nfsd_read_direct trace event shows how NFSD expands any
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index f4069da853725..ed5903c3becac 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -1324,6 +1324,7 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
size_t prefix, middle, suffix;
loff_t offset = iocb->ki_pos;
unsigned int nsegs = 0;
+ u32 cut;
/*
* Check if direct I/O is feasible for this write request.
@@ -1338,6 +1339,18 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
orig_end = offset + total;
middle_end = round_down(orig_end, offset_align);
+ /*
+ * The direct middle invalidates the page cache over its range, so it
+ * must not share a page with a buffered end. An aligned end needs no
+ * buffered segment; a misaligned one is cut on a page boundary even
+ * where the offset alignment is finer than a page.
+ */
+ cut = max_t(u32, offset_align, PAGE_SIZE);
+ if (prefix_end != offset)
+ prefix_end = min(round_up(offset, cut), orig_end);
+ if (middle_end != orig_end)
+ middle_end = max(round_down(orig_end, cut), prefix_end);
+
prefix = prefix_end - offset;
middle = middle_end - prefix_end;
suffix = orig_end - middle_end;
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH v4 02/10] NFSD: mark the direct middle of a split WRITE IOCB_DONTCACHE as well
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 01/10] NFSD: cut a misaligned direct-mode WRITE at page boundaries Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 03/10] NFSD: only split a direct-mode WRITE for a worthwhile direct middle Mike Snitzer
` (9 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
nfsd_write_dio_iters_init() marks the DIO-aligned middle of a misaligned
WRITE IOCB_DIRECT, and nfsd_direct_write() issues it that way. A file
system may service that request with buffered I/O instead:
xfs_file_write_iter() retries through xfs_file_buffered_write() with the
same kiocb when xfs_file_dio_write() returns -ENOTBLK, which
iomap_dio_rw() does when it cannot invalidate page cache overlapping the
range.
Since the previous commit, the middle of a split WRITE shares no page
with the buffered ends of its neighbours, so a streaming misaligned
WRITE workload no longer causes this by itself. It still happens
whenever something dirties a page in the middle's range between its
writeback and its invalidation without taking the lock the file system
holds across a direct write: a local process writing the exported file
through mmap, which XFS's IOLOCK does not keep out, or, on a file system
that does not hold such a lock for the length of a direct write, an
overlapping WRITE that is issued buffered as a whole (too small to
split, or its payload misaligned in memory) or a local buffered writer.
All of the middle, nearly the whole WRITE, is then re-issued as a cached
buffered write that nothing ever drops.
Set IOCB_DONTCACHE on the middle segment alongside IOCB_DIRECT when
the file system supports FOP_DONTCACHE. The direct path ignores the
flag; a buffered fallback now drops its pages once written back. Say
so in nfsd-io-modes.rst.
Fixes: 06c5c97293e3 ("NFSD: Implement NFSD_IO_DIRECT for NFS WRITE")
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Documentation/filesystems/nfs/nfsd-io-modes.rst | 4 ++++
fs/nfsd/vfs.c | 3 +++
2 files changed, 7 insertions(+)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index 1916b2eee58ad..f9b63f1a33990 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -132,6 +132,10 @@ Misaligned WRITE:
one; a start or end that is already DIO-aligned gets no buffered
segment at all. If the filesystem supports FOP_DONTCACHE, the
misaligned segments are marked DONTCACHE.
+ So is the O_DIRECT segment, where the flag has no effect unless the
+ filesystem services it with buffered IO instead, as XFS does when it
+ cannot invalidate page cache that overlaps the segment. The
+ iomap_dio_invalidate_fail trace event reports such a fallback.
Tracing:
The nfsd_read_direct trace event shows how NFSD expands any
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index ed5903c3becac..bd3dab36e2962 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -1375,6 +1375,9 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
if (iov_iter_bvec_offset(&segments[nsegs].iter) & (mem_align - 1))
goto no_dio;
segments[nsegs].flags |= IOCB_DIRECT;
+ /* In case the file system falls back to buffered I/O (-ENOTBLK). */
+ if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
+ segments[nsegs].flags |= IOCB_DONTCACHE;
nsegs++;
if (suffix)
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH v4 03/10] NFSD: only split a direct-mode WRITE for a worthwhile direct middle
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 01/10] NFSD: cut a misaligned direct-mode WRITE at page boundaries Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 02/10] NFSD: mark the direct middle of a split WRITE IOCB_DONTCACHE as well Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-16 11:03 ` Jeff Layton
2026-10-15 23:33 ` [PATCH v4 04/10] NFSD: add direct_misaligned_dontcache debugfs knob Mike Snitzer
` (8 subsequent siblings)
11 siblings, 1 reply; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm,
Christoph Hellwig
Splitting a misaligned direct-mode WRITE issues it as two or three
writes instead of one, and the direct write first writes back and
invalidates the page cache over its range. When the DIO-aligned middle
is small, the WRITE is mostly its buffered prefix and suffix anyway, and
the direct middle only adds that overhead.
Add direct_misaligned_min_bytes (debugfs, default 8192) and do not split
a WRITE that has a misaligned start or end and a middle smaller than that
many bytes. Such a WRITE is issued as a single buffered segment, like
the other WRITEs NFSD does not split.
The gate counts bytes, not pages. What it weighs, the cost of three I/Os
against one, does not change with the page size, and since a misaligned
WRITE's middle is cut at page boundaries, the middle of such a WRITE is
whole pages: a gate of two pages would demand a 32 KiB middle on 16 KiB
pages and 128 KiB on 64 KiB pages before splitting at all. On 4 KiB
pages the default is two pages.
Also decide each segment's IOCB_* flags in nfsd_write_dio_iters_init()
instead of in the write loop of nfsd_direct_write(). The flags do not
change: the loop already set IOCB_DONTCACHE on every segment without
IOCB_DIRECT when the file system supports FOP_DONTCACHE.
The threshold, counted there in pages, and the move of the flag
decisions come from Christoph's suggested diff [1]. Two of its choices
are not taken. It issued the prefix, the suffix and any WRITE it did
not split, whether smaller than its alignment or with a middle below the
threshold, as cached buffered I/O, to leave partial pages in place for
read-modify-write; this patch keeps the DONTCACHE policy already in the
tree, and a later commit makes that policy selectable. It also split a
WRITE whose payload is misaligned in memory and issued the middle as
DONTCACHE buffered I/O, so that a large middle stayed out of the page
cache while the cached ends stayed in it. Here the prefix and suffix
are DONTCACHE as well, so that split would issue the same uncached
buffered I/O as two or three writes instead of one; such a WRITE is
issued whole, like every other WRITE NFSD does not split.
Document in nfsd-io-modes.rst when NFSD does not split a WRITE.
Link: https://lore.kernel.org/linux-nfs/aRShjU_Ti7f2Ci7I@infradead.org/ [1]
Suggested-by: Christoph Hellwig <hch@infradead.org>
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
.../filesystems/nfs/nfsd-io-modes.rst | 13 ++++++
fs/nfsd/debugfs.c | 12 +++++
fs/nfsd/nfsd.h | 1 +
fs/nfsd/vfs.c | 46 +++++++++++--------
4 files changed, 53 insertions(+), 19 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index f9b63f1a33990..eab35a9bbdb93 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -137,6 +137,19 @@ Misaligned WRITE:
cannot invalidate page cache that overlaps the segment. The
iomap_dio_invalidate_fail trace event reports such a fallback.
+ If NFSD does not split a misaligned WRITE, it issues the whole WRITE
+ as a single DONTCACHE buffered IO (normal buffered IO if the
+ filesystem lacks FOP_DONTCACHE). NFSD does not split a WRITE when:
+
+ - the filesystem advertises no DIO alignment;
+ - the WRITE is smaller than the larger of the offset and memory
+ alignments;
+ - the WRITE has a misaligned start or end and its DIO-aligned middle
+ is smaller than /sys/kernel/debug/nfsd/direct_misaligned_min_bytes
+ bytes (default 8192);
+ - the WRITE payload is not aligned in memory to the block device's
+ dma_alignment, so the middle cannot be O_DIRECT either.
+
Tracing:
The nfsd_read_direct trace event shows how NFSD expands any
misaligned READ to the next DIO-aligned block (on either end of the
diff --git a/fs/nfsd/debugfs.c b/fs/nfsd/debugfs.c
index 386fd1c54f527..02b0369f17648 100644
--- a/fs/nfsd/debugfs.c
+++ b/fs/nfsd/debugfs.c
@@ -140,6 +140,18 @@ void nfsd_debugfs_init(void)
debugfs_create_file("io_cache_write", 0644, nfsd_top_dir, NULL,
&nfsd_io_cache_write_fops);
+
+ /*
+ * /sys/kernel/debug/nfsd/direct_misaligned_min_bytes
+ *
+ * Minimum size, in bytes, of the DIO-aligned middle segment for NFSD
+ * to split a misaligned direct-mode WRITE. A WRITE with a misaligned
+ * start or end and a smaller middle is issued as a single buffered
+ * segment. It is in bytes so that it means the same on every page
+ * size. The default value of this setting is 8192.
+ */
+ debugfs_create_u32("direct_misaligned_min_bytes", 0644, nfsd_top_dir,
+ &nfsd_direct_misaligned_min_bytes);
#ifdef CONFIG_NFSD_V4
debugfs_create_bool("delegated_timestamps", 0644, nfsd_top_dir,
&nfsd_delegts_enabled);
diff --git a/fs/nfsd/nfsd.h b/fs/nfsd/nfsd.h
index d824d2c390294..db81620bd5168 100644
--- a/fs/nfsd/nfsd.h
+++ b/fs/nfsd/nfsd.h
@@ -142,6 +142,7 @@ enum {
extern u64 nfsd_io_cache_read __read_mostly;
extern u64 nfsd_io_cache_write __read_mostly;
+extern u32 nfsd_direct_misaligned_min_bytes __read_mostly;
bool nfsd_v4client(struct svc_rqst *rqstp);
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index bd3dab36e2962..a419eee401d9d 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -54,6 +54,7 @@
bool nfsd_disable_splice_read __read_mostly;
u64 nfsd_io_cache_read __read_mostly = NFSD_IO_BUFFERED;
u64 nfsd_io_cache_write __read_mostly = NFSD_IO_BUFFERED;
+u32 nfsd_direct_misaligned_min_bytes __read_mostly = 8192;
/**
* nfserrno - Map Linux errnos to NFS errnos
@@ -1323,9 +1324,13 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
u32 mem_align = nf->nf_dio_mem_align;
size_t prefix, middle, suffix;
loff_t offset = iocb->ki_pos;
+ unsigned int dontcache_flags = 0;
unsigned int nsegs = 0;
u32 cut;
+ if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
+ dontcache_flags = IOCB_DONTCACHE;
+
/*
* Check if direct I/O is feasible for this write request.
* If alignments are not available, the write is too small,
@@ -1355,12 +1360,16 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
middle = middle_end - prefix_end;
suffix = orig_end - middle_end;
- if (!middle)
+ if (!middle ||
+ ((prefix || suffix) &&
+ middle < nfsd_direct_misaligned_min_bytes))
goto no_dio;
- if (prefix)
- nfsd_write_dio_seg_init(&segments[nsegs++], bvec,
+ if (prefix) {
+ nfsd_write_dio_seg_init(&segments[nsegs], bvec,
nvecs, total, 0, prefix, iocb);
+ segments[nsegs++].flags |= dontcache_flags;
+ }
nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs,
total, prefix, middle, iocb);
@@ -1374,22 +1383,28 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
*/
if (iov_iter_bvec_offset(&segments[nsegs].iter) & (mem_align - 1))
goto no_dio;
- segments[nsegs].flags |= IOCB_DIRECT;
- /* In case the file system falls back to buffered I/O (-ENOTBLK). */
- if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
- segments[nsegs].flags |= IOCB_DONTCACHE;
- nsegs++;
+ /*
+ * IOCB_DONTCACHE matters only if the file system falls back to
+ * buffered I/O (-ENOTBLK).
+ */
+ segments[nsegs++].flags |= IOCB_DIRECT | dontcache_flags;
- if (suffix)
- nfsd_write_dio_seg_init(&segments[nsegs++], bvec, nvecs, total,
+ if (suffix) {
+ nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs, total,
prefix + middle, suffix, iocb);
+ segments[nsegs++].flags |= dontcache_flags;
+ }
return nsegs;
no_dio:
- /* No DIO alignment possible - pack into single non-DIO segment. */
+ /*
+ * Issue the whole WRITE as a single buffered segment, uncached when
+ * the file system supports FOP_DONTCACHE.
+ */
nfsd_write_dio_seg_init(&segments[0], bvec, nvecs, total, 0,
total, iocb);
+ segments[0].flags |= dontcache_flags;
return 1;
}
@@ -1413,16 +1428,9 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
if (kiocb->ki_flags & IOCB_DIRECT)
trace_nfsd_write_direct(rqstp, fhp, kiocb->ki_pos,
segments[i].iter.count);
- else {
+ else
trace_nfsd_write_vector(rqstp, fhp, kiocb->ki_pos,
segments[i].iter.count);
- /*
- * Mark the I/O buffer as evict-able to reduce
- * memory contention.
- */
- if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
- kiocb->ki_flags |= IOCB_DONTCACHE;
- }
expected = iov_iter_count(&segments[i].iter);
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH v4 03/10] NFSD: only split a direct-mode WRITE for a worthwhile direct middle
2026-10-15 23:33 ` [PATCH v4 03/10] NFSD: only split a direct-mode WRITE for a worthwhile direct middle Mike Snitzer
@ 2026-10-16 11:03 ` Jeff Layton
0 siblings, 0 replies; 17+ messages in thread
From: Jeff Layton @ 2026-10-16 11:03 UTC (permalink / raw)
To: Mike Snitzer, Chuck Lever
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm,
Christoph Hellwig
On Thu, 2026-10-15 at 19:33 -0400, Mike Snitzer wrote:
> Splitting a misaligned direct-mode WRITE issues it as two or three
> writes instead of one, and the direct write first writes back and
> invalidates the page cache over its range. When the DIO-aligned middle
> is small, the WRITE is mostly its buffered prefix and suffix anyway, and
> the direct middle only adds that overhead.
>
> Add direct_misaligned_min_bytes (debugfs, default 8192) and do not split
> a WRITE that has a misaligned start or end and a middle smaller than that
> many bytes. Such a WRITE is issued as a single buffered segment, like
> the other WRITEs NFSD does not split.
>
> The gate counts bytes, not pages. What it weighs, the cost of three I/Os
> against one, does not change with the page size, and since a misaligned
> WRITE's middle is cut at page boundaries, the middle of such a WRITE is
> whole pages: a gate of two pages would demand a 32 KiB middle on 16 KiB
> pages and 128 KiB on 64 KiB pages before splitting at all. On 4 KiB
> pages the default is two pages.
>
> Also decide each segment's IOCB_* flags in nfsd_write_dio_iters_init()
> instead of in the write loop of nfsd_direct_write(). The flags do not
> change: the loop already set IOCB_DONTCACHE on every segment without
> IOCB_DIRECT when the file system supports FOP_DONTCACHE.
>
> The threshold, counted there in pages, and the move of the flag
> decisions come from Christoph's suggested diff [1]. Two of its choices
> are not taken. It issued the prefix, the suffix and any WRITE it did
> not split, whether smaller than its alignment or with a middle below the
> threshold, as cached buffered I/O, to leave partial pages in place for
> read-modify-write; this patch keeps the DONTCACHE policy already in the
> tree, and a later commit makes that policy selectable. It also split a
> WRITE whose payload is misaligned in memory and issued the middle as
> DONTCACHE buffered I/O, so that a large middle stayed out of the page
> cache while the cached ends stayed in it. Here the prefix and suffix
> are DONTCACHE as well, so that split would issue the same uncached
> buffered I/O as two or three writes instead of one; such a WRITE is
> issued whole, like every other WRITE NFSD does not split.
>
> Document in nfsd-io-modes.rst when NFSD does not split a WRITE.
>
> Link: https://lore.kernel.org/linux-nfs/aRShjU_Ti7f2Ci7I@infradead.org/ [1]
> Suggested-by: Christoph Hellwig <hch@infradead.org>
> Assisted-by: LLM
> Signed-off-by: Mike Snitzer <snitzer@kernel.org>
> ---
> .../filesystems/nfs/nfsd-io-modes.rst | 13 ++++++
> fs/nfsd/debugfs.c | 12 +++++
> fs/nfsd/nfsd.h | 1 +
> fs/nfsd/vfs.c | 46 +++++++++++--------
> 4 files changed, 53 insertions(+), 19 deletions(-)
>
> diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
> index f9b63f1a33990..eab35a9bbdb93 100644
> --- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
> +++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
> @@ -137,6 +137,19 @@ Misaligned WRITE:
> cannot invalidate page cache that overlaps the segment. The
> iomap_dio_invalidate_fail trace event reports such a fallback.
>
> + If NFSD does not split a misaligned WRITE, it issues the whole WRITE
> + as a single DONTCACHE buffered IO (normal buffered IO if the
> + filesystem lacks FOP_DONTCACHE). NFSD does not split a WRITE when:
> +
> + - the filesystem advertises no DIO alignment;
> + - the WRITE is smaller than the larger of the offset and memory
> + alignments;
> + - the WRITE has a misaligned start or end and its DIO-aligned middle
> + is smaller than /sys/kernel/debug/nfsd/direct_misaligned_min_bytes
> + bytes (default 8192);
> + - the WRITE payload is not aligned in memory to the block device's
> + dma_alignment, so the middle cannot be O_DIRECT either.
> +
> Tracing:
> The nfsd_read_direct trace event shows how NFSD expands any
> misaligned READ to the next DIO-aligned block (on either end of the
> diff --git a/fs/nfsd/debugfs.c b/fs/nfsd/debugfs.c
> index 386fd1c54f527..02b0369f17648 100644
> --- a/fs/nfsd/debugfs.c
> +++ b/fs/nfsd/debugfs.c
> @@ -140,6 +140,18 @@ void nfsd_debugfs_init(void)
>
> debugfs_create_file("io_cache_write", 0644, nfsd_top_dir, NULL,
> &nfsd_io_cache_write_fops);
> +
> + /*
> + * /sys/kernel/debug/nfsd/direct_misaligned_min_bytes
> + *
> + * Minimum size, in bytes, of the DIO-aligned middle segment for NFSD
> + * to split a misaligned direct-mode WRITE. A WRITE with a misaligned
> + * start or end and a smaller middle is issued as a single buffered
> + * segment. It is in bytes so that it means the same on every page
> + * size. The default value of this setting is 8192.
> + */
> + debugfs_create_u32("direct_misaligned_min_bytes", 0644, nfsd_top_dir,
> + &nfsd_direct_misaligned_min_bytes);
This is fine for now, but (FYI) I'm working on a patch series to add
r/wiomode= export options, so we'll be able to set the iomode on a per-
export basis in the future.
At that point, we may want to move this and the io_cache_*= settings to
netlink and nfsdctl, so admins will be able to define it in
/etc/nfs.conf. That may also be an argument for making this a per-net
setting instead of global
> #ifdef CONFIG_NFSD_V4
> debugfs_create_bool("delegated_timestamps", 0644, nfsd_top_dir,
> &nfsd_delegts_enabled);
> diff --git a/fs/nfsd/nfsd.h b/fs/nfsd/nfsd.h
> index d824d2c390294..db81620bd5168 100644
> --- a/fs/nfsd/nfsd.h
> +++ b/fs/nfsd/nfsd.h
> @@ -142,6 +142,7 @@ enum {
>
> extern u64 nfsd_io_cache_read __read_mostly;
> extern u64 nfsd_io_cache_write __read_mostly;
> +extern u32 nfsd_direct_misaligned_min_bytes __read_mostly;
>
> bool nfsd_v4client(struct svc_rqst *rqstp);
>
> diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
> index bd3dab36e2962..a419eee401d9d 100644
> --- a/fs/nfsd/vfs.c
> +++ b/fs/nfsd/vfs.c
> @@ -54,6 +54,7 @@
> bool nfsd_disable_splice_read __read_mostly;
> u64 nfsd_io_cache_read __read_mostly = NFSD_IO_BUFFERED;
> u64 nfsd_io_cache_write __read_mostly = NFSD_IO_BUFFERED;
> +u32 nfsd_direct_misaligned_min_bytes __read_mostly = 8192;
>
> /**
> * nfserrno - Map Linux errnos to NFS errnos
> @@ -1323,9 +1324,13 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
> u32 mem_align = nf->nf_dio_mem_align;
> size_t prefix, middle, suffix;
> loff_t offset = iocb->ki_pos;
> + unsigned int dontcache_flags = 0;
> unsigned int nsegs = 0;
> u32 cut;
>
> + if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
> + dontcache_flags = IOCB_DONTCACHE;
> +
> /*
> * Check if direct I/O is feasible for this write request.
> * If alignments are not available, the write is too small,
> @@ -1355,12 +1360,16 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
> middle = middle_end - prefix_end;
> suffix = orig_end - middle_end;
>
> - if (!middle)
> + if (!middle ||
> + ((prefix || suffix) &&
> + middle < nfsd_direct_misaligned_min_bytes))
> goto no_dio;
>
> - if (prefix)
> - nfsd_write_dio_seg_init(&segments[nsegs++], bvec,
> + if (prefix) {
> + nfsd_write_dio_seg_init(&segments[nsegs], bvec,
> nvecs, total, 0, prefix, iocb);
> + segments[nsegs++].flags |= dontcache_flags;
> + }
>
> nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs,
> total, prefix, middle, iocb);
> @@ -1374,22 +1383,28 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
> */
> if (iov_iter_bvec_offset(&segments[nsegs].iter) & (mem_align - 1))
> goto no_dio;
> - segments[nsegs].flags |= IOCB_DIRECT;
> - /* In case the file system falls back to buffered I/O (-ENOTBLK). */
> - if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
> - segments[nsegs].flags |= IOCB_DONTCACHE;
> - nsegs++;
> + /*
> + * IOCB_DONTCACHE matters only if the file system falls back to
> + * buffered I/O (-ENOTBLK).
> + */
> + segments[nsegs++].flags |= IOCB_DIRECT | dontcache_flags;
>
> - if (suffix)
> - nfsd_write_dio_seg_init(&segments[nsegs++], bvec, nvecs, total,
> + if (suffix) {
> + nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs, total,
> prefix + middle, suffix, iocb);
> + segments[nsegs++].flags |= dontcache_flags;
> + }
>
> return nsegs;
>
> no_dio:
> - /* No DIO alignment possible - pack into single non-DIO segment. */
> + /*
> + * Issue the whole WRITE as a single buffered segment, uncached when
> + * the file system supports FOP_DONTCACHE.
> + */
> nfsd_write_dio_seg_init(&segments[0], bvec, nvecs, total, 0,
> total, iocb);
> + segments[0].flags |= dontcache_flags;
> return 1;
> }
>
> @@ -1413,16 +1428,9 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
> if (kiocb->ki_flags & IOCB_DIRECT)
> trace_nfsd_write_direct(rqstp, fhp, kiocb->ki_pos,
> segments[i].iter.count);
> - else {
> + else
> trace_nfsd_write_vector(rqstp, fhp, kiocb->ki_pos,
> segments[i].iter.count);
> - /*
> - * Mark the I/O buffer as evict-able to reduce
> - * memory contention.
> - */
> - if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
> - kiocb->ki_flags |= IOCB_DONTCACHE;
> - }
>
> expected = iov_iter_count(&segments[i].iter);
>
--
Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v4 04/10] NFSD: add direct_misaligned_dontcache debugfs knob
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (2 preceding siblings ...)
2026-10-15 23:33 ` [PATCH v4 03/10] NFSD: only split a direct-mode WRITE for a worthwhile direct middle Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 05/10] NFSD: do not use direct I/O for a READ smaller than its alignment Mike Snitzer
` (7 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm,
Jonathan Flynn
From: Jonathan Flynn <jonathan.flynn@hammerspace.com>
The parts of a direct-mode WRITE that are not O_DIRECT (the misaligned
start and end of a split WRITE, or the whole of a WRITE that is not
split) are issued IOCB_DONTCACHE when the file system sets
FOP_DONTCACHE. That suits the workloads a direct mode is chosen for,
but it is a policy, not a requirement: a workload that reads back or
rewrites what it just wrote is better served by those pages staying
cached.
Add /sys/kernel/debug/nfsd/direct_misaligned_dontcache (default Y). Set
to N, those parts are issued as normal buffered I/O. The O_DIRECT
middle keeps IOCB_DONTCACHE either way. With N, a WRITE that is not
split, including one whose payload is misaligned in memory, is cached in
full. The knob sits beside direct_misaligned_min_bytes, which decides
whether a misaligned WRITE is split at all; this knob decides how the
parts that are not O_DIRECT are cached.
Signed-off-by: Jonathan Flynn <jonathan.flynn@hammerspace.com>
[snitzer: switched from a module parameter to a debugfs knob, moved
next to direct_misaligned_min_bytes, documented in nfsd-io-modes.rst]
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
.../filesystems/nfs/nfsd-io-modes.rst | 19 +++++++++++++------
fs/nfsd/debugfs.c | 13 +++++++++++++
fs/nfsd/nfsd.h | 1 +
fs/nfsd/vfs.c | 12 ++++++++----
4 files changed, 35 insertions(+), 10 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index eab35a9bbdb93..2679d85152dfc 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -131,15 +131,17 @@ Misaligned WRITE:
offset, so the O_DIRECT segment never shares a page with a buffered
one; a start or end that is already DIO-aligned gets no buffered
segment at all. If the filesystem supports FOP_DONTCACHE, the
- misaligned segments are marked DONTCACHE.
- So is the O_DIRECT segment, where the flag has no effect unless the
- filesystem services it with buffered IO instead, as XFS does when it
- cannot invalidate page cache that overlaps the segment. The
- iomap_dio_invalidate_fail trace event reports such a fallback.
+ O_DIRECT segment is marked DONTCACHE, and so are the misaligned
+ segments unless direct_misaligned_dontcache is N (below). The flag
+ has no effect on the O_DIRECT segment unless the filesystem services
+ it with buffered IO instead, as XFS does when it cannot invalidate
+ page cache that overlaps the segment. The iomap_dio_invalidate_fail
+ trace event reports such a fallback.
If NFSD does not split a misaligned WRITE, it issues the whole WRITE
as a single DONTCACHE buffered IO (normal buffered IO if the
- filesystem lacks FOP_DONTCACHE). NFSD does not split a WRITE when:
+ filesystem lacks FOP_DONTCACHE or direct_misaligned_dontcache is N).
+ NFSD does not split a WRITE when:
- the filesystem advertises no DIO alignment;
- the WRITE is smaller than the larger of the offset and memory
@@ -150,6 +152,11 @@ Misaligned WRITE:
- the WRITE payload is not aligned in memory to the block device's
dma_alignment, so the middle cannot be O_DIRECT either.
+ Writing N to /sys/kernel/debug/nfsd/direct_misaligned_dontcache
+ (default Y) issues the start and end segments, and a WRITE that is
+ not split, as normal buffered IO instead of DONTCACHE, which suits a
+ workload that reads back or rewrites what it just wrote.
+
Tracing:
The nfsd_read_direct trace event shows how NFSD expands any
misaligned READ to the next DIO-aligned block (on either end of the
diff --git a/fs/nfsd/debugfs.c b/fs/nfsd/debugfs.c
index 02b0369f17648..80f53a4ada7bf 100644
--- a/fs/nfsd/debugfs.c
+++ b/fs/nfsd/debugfs.c
@@ -152,6 +152,19 @@ void nfsd_debugfs_init(void)
*/
debugfs_create_u32("direct_misaligned_min_bytes", 0644, nfsd_top_dir,
&nfsd_direct_misaligned_min_bytes);
+
+ /*
+ * /sys/kernel/debug/nfsd/direct_misaligned_dontcache
+ *
+ * Y: the start and end segments of a split direct-mode WRITE, and
+ * a WRITE that is not split, use DONTCACHE buffered IO when the
+ * file system supports FOP_DONTCACHE
+ * N: those segments use normal buffered IO
+ *
+ * The default value of this setting is Y.
+ */
+ debugfs_create_bool("direct_misaligned_dontcache", 0644, nfsd_top_dir,
+ &nfsd_direct_misaligned_dontcache);
#ifdef CONFIG_NFSD_V4
debugfs_create_bool("delegated_timestamps", 0644, nfsd_top_dir,
&nfsd_delegts_enabled);
diff --git a/fs/nfsd/nfsd.h b/fs/nfsd/nfsd.h
index db81620bd5168..b520b51692b09 100644
--- a/fs/nfsd/nfsd.h
+++ b/fs/nfsd/nfsd.h
@@ -143,6 +143,7 @@ enum {
extern u64 nfsd_io_cache_read __read_mostly;
extern u64 nfsd_io_cache_write __read_mostly;
extern u32 nfsd_direct_misaligned_min_bytes __read_mostly;
+extern bool nfsd_direct_misaligned_dontcache __read_mostly;
bool nfsd_v4client(struct svc_rqst *rqstp);
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index a419eee401d9d..b596bd61f8cb7 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -55,6 +55,7 @@ bool nfsd_disable_splice_read __read_mostly;
u64 nfsd_io_cache_read __read_mostly = NFSD_IO_BUFFERED;
u64 nfsd_io_cache_write __read_mostly = NFSD_IO_BUFFERED;
u32 nfsd_direct_misaligned_min_bytes __read_mostly = 8192;
+bool nfsd_direct_misaligned_dontcache __read_mostly = true;
/**
* nfserrno - Map Linux errnos to NFS errnos
@@ -1325,11 +1326,13 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
size_t prefix, middle, suffix;
loff_t offset = iocb->ki_pos;
unsigned int dontcache_flags = 0;
+ unsigned int buffered_flags;
unsigned int nsegs = 0;
u32 cut;
if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
dontcache_flags = IOCB_DONTCACHE;
+ buffered_flags = nfsd_direct_misaligned_dontcache ? dontcache_flags : 0;
/*
* Check if direct I/O is feasible for this write request.
@@ -1368,7 +1371,7 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
if (prefix) {
nfsd_write_dio_seg_init(&segments[nsegs], bvec,
nvecs, total, 0, prefix, iocb);
- segments[nsegs++].flags |= dontcache_flags;
+ segments[nsegs++].flags |= buffered_flags;
}
nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs,
@@ -1392,7 +1395,7 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
if (suffix) {
nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs, total,
prefix + middle, suffix, iocb);
- segments[nsegs++].flags |= dontcache_flags;
+ segments[nsegs++].flags |= buffered_flags;
}
return nsegs;
@@ -1400,11 +1403,12 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
no_dio:
/*
* Issue the whole WRITE as a single buffered segment, uncached when
- * the file system supports FOP_DONTCACHE.
+ * the file system supports FOP_DONTCACHE and
+ * direct_misaligned_dontcache is set.
*/
nfsd_write_dio_seg_init(&segments[0], bvec, nvecs, total, 0,
total, iocb);
- segments[0].flags |= dontcache_flags;
+ segments[0].flags |= buffered_flags;
return 1;
}
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH v4 05/10] NFSD: do not use direct I/O for a READ smaller than its alignment
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (3 preceding siblings ...)
2026-10-15 23:33 ` [PATCH v4 04/10] NFSD: add direct_misaligned_dontcache debugfs knob Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 06/10] NFSD: persist a synchronous direct-mode WRITE once, after all of its segments Mike Snitzer
` (6 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
nfsd_direct_read() expands a misaligned READ out to DIO-aligned
boundaries: it reads from round_down(offset, dio_read_offset_align) to
round_up(offset + count, dio_read_offset_align) and returns only the
requested bytes from within that window. When the READ is smaller than
the alignment, that window is always at least one full alignment unit,
and two when the READ straddles a boundary, so a few hundred bytes of
payload can cost a 4K or 64K device read.
Decline direct I/O for those. A READ smaller than
dio_read_offset_align now falls through to the DONTCACHE path, which
issues DONTCACHE buffered I/O when the file system supports
FOP_DONTCACHE and normal buffered I/O otherwise. This mirrors the
WRITE side, which already declines direct I/O for a WRITE smaller than
the larger of its offset and memory alignments.
Only dio_read_offset_align is consulted: nfsd_direct_read() lands the
data in whole rq_pages pages at offset zero, so dio_mem_align is always
satisfied and only the file offset and length can be misaligned. The
threshold only bites when the file system advertises a large alignment;
where it reports 512 almost no READ is excluded.
Document it in the "Misaligned READ" section of nfsd-io-modes.rst.
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Documentation/filesystems/nfs/nfsd-io-modes.rst | 5 +++++
fs/nfsd/vfs.c | 11 +++++++++--
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index 2679d85152dfc..90bf1038453cc 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -121,6 +121,11 @@ Misaligned READ:
verified to have proper offset/len (logical_block_size) and
dma_alignment checking.
+ A READ smaller than dio_read_offset_align is not expanded: it would
+ read one or two whole alignment units to return less than one unit
+ of data. It is issued as DONTCACHE buffered IO instead (normal
+ buffered IO if the filesystem lacks FOP_DONTCACHE).
+
Misaligned WRITE:
If NFSD_IO_DIRECT is used, split any misaligned WRITE into a start,
middle and end as needed. The large middle segment is DIO-aligned
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index b596bd61f8cb7..7e65328de7c86 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -1166,11 +1166,18 @@ nfsd_direct_read(struct svc_rqst *rqstp, struct svc_fh *fhp,
eof, host_err);
}
-static bool nfsd_direct_read_ok(struct svc_rqst *rqstp, struct nfsd_file *nf)
+static bool nfsd_direct_read_ok(struct svc_rqst *rqstp, struct nfsd_file *nf,
+ unsigned long count)
{
/* When dio_read_offset_align is zero, dio is not supported */
if (!nf->nf_dio_read_offset_align)
return false;
+ /*
+ * A READ smaller than that alignment is not worth expanding to one
+ * or two whole alignment units.
+ */
+ if (count < nf->nf_dio_read_offset_align)
+ return false;
if (rqstp->rq_res.page_len)
return false;
#ifdef CONFIG_NFSD_V4
@@ -1222,7 +1229,7 @@ __be32 nfsd_iter_read(struct svc_rqst *rqstp, struct svc_fh *fhp,
case NFSD_IO_BUFFERED:
break;
case NFSD_IO_DIRECT:
- if (nfsd_direct_read_ok(rqstp, nf))
+ if (nfsd_direct_read_ok(rqstp, nf, *count))
return nfsd_direct_read(rqstp, fhp, nf, offset,
count, page, eof);
fallthrough;
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH v4 06/10] NFSD: persist a synchronous direct-mode WRITE once, after all of its segments
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (4 preceding siblings ...)
2026-10-15 23:33 ` [PATCH v4 05/10] NFSD: do not use direct I/O for a READ smaller than its alignment Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 07/10] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
` (5 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
nfsd_direct_write() may issue a WRITE as up to three segments: a
buffered prefix, a direct middle and a buffered suffix. For a
FILE_SYNC or DATA_SYNC WRITE the kiocb carries IOCB_DSYNC and every
segment inherits it, so generic_write_sync() runs a range fsync after
each segment: up to three cache flushes and log forces per WRITE.
Strip IOCB_DSYNC and IOCB_SYNC from the per-segment flags of a split
WRITE, and of an unsplit buffered WRITE, and persist the WRITE once with
vfs_fsync_range() over the bytes actually written, after the last
segment. Durability is unchanged: the reply is not sent until the
fsync completes, and datasync mirrors the previous per-segment choice
(IOCB_SYNC present means metadata too). An fsync failure is returned
like a write failure.
A WRITE issued whole as direct I/O keeps its flags: iomap already
completes it in one step, and can complete a DATA_SYNC overwrite of
allocated blocks with FUA writes and no cache flush.
Besides the fewer flushes, the sync now runs after the last segment is
written. A later commit relies on that order: it marks the page shared
with a neighbouring WRITE for dropping only after writing it, and the
WRITE's own fsync is then the writeback that drops the page.
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
.../filesystems/nfs/nfsd-io-modes.rst | 4 +++-
fs/nfsd/vfs.c | 20 ++++++++++++++++++-
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index 90bf1038453cc..c1cc974640cfe 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -141,7 +141,9 @@ Misaligned WRITE:
has no effect on the O_DIRECT segment unless the filesystem services
it with buffered IO instead, as XFS does when it cannot invalidate
page cache that overlaps the segment. The iomap_dio_invalidate_fail
- trace event reports such a fallback.
+ trace event reports such a fallback. A split FILE_SYNC or DATA_SYNC
+ WRITE is persisted once, after all of its segments are written, not
+ once per segment.
If NFSD does not split a misaligned WRITE, it issues the whole WRITE
as a single DONTCACHE buffered IO (normal buffered IO if the
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index 7e65328de7c86..249358256d2f5 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -1426,16 +1426,27 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
{
struct nfsd_write_dio_seg segments[3];
struct file *file = nf->nf_file;
+ loff_t start = kiocb->ki_pos;
unsigned int nsegs, i;
+ int sync_flags;
ssize_t host_err;
size_t expected;
nsegs = nfsd_write_dio_iters_init(nf, rqstp->rq_bvec, nvecs,
kiocb, *cnt, segments);
+ /*
+ * Persist a synchronous WRITE once, after all of its segments. A
+ * WRITE issued whole as direct I/O keeps IOCB_DSYNC, so iomap can
+ * still complete a DATA_SYNC overwrite with FUA writes.
+ */
+ sync_flags = kiocb->ki_flags & (IOCB_DSYNC | IOCB_SYNC);
+ if (nsegs == 1 && (segments[0].flags & IOCB_DIRECT))
+ sync_flags = 0;
+
*cnt = 0;
for (i = 0; i < nsegs; i++) {
- kiocb->ki_flags = segments[i].flags;
+ kiocb->ki_flags = segments[i].flags & ~sync_flags;
if (kiocb->ki_flags & IOCB_DIRECT)
trace_nfsd_write_direct(rqstp, fhp, kiocb->ki_pos,
segments[i].iter.count);
@@ -1453,6 +1464,13 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
break; /* partial write */
}
+ if (sync_flags && *cnt) {
+ host_err = vfs_fsync_range(file, start, start + *cnt - 1,
+ !(sync_flags & IOCB_SYNC));
+ if (host_err < 0)
+ return host_err;
+ }
+
return 0;
}
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH v4 07/10] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (5 preceding siblings ...)
2026-10-15 23:33 ` [PATCH v4 06/10] NFSD: persist a synchronous direct-mode WRITE once, after all of its segments Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-16 11:08 ` Jeff Layton
2026-10-15 23:33 ` [PATCH v4 08/10] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
` (4 subsequent siblings)
11 siblings, 1 reply; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
A folio allocated by an RWF_DONTCACHE write is marked dropbehind before
it is dirtied, so folio_account_dirtied() adds it to WB_DONTCACHE_DIRTY
and folio_clear_dirty_for_io() or folio_account_cleaned() subtracts it
when it is cleaned. Nothing marks a folio that already exists. A
caller that sets PG_dropbehind on a folio that is already dirty
unbalances the counter: the clean subtracts what was never added, and
once the counter goes negative, wb_stat_sum() reads it as zero and the
DONTCACHE writeback kick stops writing anything back for every writer
on that bdi_writeback.
Add folio_mark_dropbehind(), the inverse of the clear in
__filemap_get_folio_mpol(): with the folio locked, mark it and, if it is
dirty, add it to WB_DONTCACHE_DIRTY. The folio lock orders this
against dirtying and against folio_clear_dirty_for_io(). A concurrent
lookup without FGP_DONTCACHE that clears the flag again subtracts what
this added.
The name follows folio_set_dirty() and folio_mark_dirty(): the first
only sets PG_dirty, the second also accounts the dirty folio.
folio_set_dropbehind() likewise only sets PG_dropbehind, and
folio_mark_dropbehind() also accounts it in WB_DONTCACHE_DIRTY.
A later commit uses it in NFSD to drop a page shared by two misaligned
WRITEs once both have written it.
Cc: Jeff Layton <jlayton@kernel.org>
Cc: Jens Axboe <axboe@kernel.dk>
Cc: Matthew Wilcox (Oracle) <willy@infradead.org>
Cc: linux-mm@kvack.org
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
include/linux/pagemap.h | 1 +
mm/filemap.c | 31 +++++++++++++++++++++++++++++++
2 files changed, 32 insertions(+)
diff --git a/include/linux/pagemap.h b/include/linux/pagemap.h
index 0adfa6605653d..3012bd95812a1 100644
--- a/include/linux/pagemap.h
+++ b/include/linux/pagemap.h
@@ -1310,6 +1310,7 @@ void end_page_writeback(struct page *page);
void folio_end_writeback(struct folio *folio);
void folio_end_writeback_no_dropbehind(struct folio *folio);
void folio_end_dropbehind(struct folio *folio);
+void folio_mark_dropbehind(struct folio *folio);
void folio_wait_stable(struct folio *folio);
void __folio_mark_dirty(struct folio *folio, struct address_space *, int warn);
void folio_account_cleaned(struct folio *folio, struct bdi_writeback *wb);
diff --git a/mm/filemap.c b/mm/filemap.c
index 00fd89cf6f550..ed7e4dbea653f 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1644,6 +1644,37 @@ void folio_end_dropbehind(struct folio *folio)
}
EXPORT_SYMBOL_GPL(folio_end_dropbehind);
+/**
+ * folio_mark_dropbehind - mark an existing folio dropbehind
+ * @folio: the folio, locked by the caller
+ *
+ * Mark @folio so that the writeback that cleans it drops it. A folio that
+ * is already dirty is added to WB_DONTCACHE_DIRTY, as folio_account_dirtied()
+ * would have added it had it been marked when dirtied, so that the clean
+ * that subtracts it leaves the counter balanced.
+ */
+void folio_mark_dropbehind(struct folio *folio)
+{
+ struct address_space *mapping = folio->mapping;
+
+ VM_WARN_ON_ONCE_FOLIO(!folio_test_locked(folio), folio);
+
+ if (folio_test_dropbehind(folio))
+ return;
+ folio_set_dropbehind(folio);
+ if (mapping && folio_test_dirty(folio) &&
+ mapping_can_writeback(mapping)) {
+ struct inode *inode = mapping->host;
+ struct bdi_writeback *wb;
+ struct wb_lock_cookie cookie = {};
+
+ wb = unlocked_inode_to_wb_begin(inode, &cookie);
+ wb_stat_mod(wb, WB_DONTCACHE_DIRTY, folio_nr_pages(folio));
+ unlocked_inode_to_wb_end(inode, &cookie);
+ }
+}
+EXPORT_SYMBOL_GPL(folio_mark_dropbehind);
+
/**
* folio_end_writeback_no_dropbehind - End writeback against a folio.
* @folio: The folio.
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH v4 07/10] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it
2026-10-15 23:33 ` [PATCH v4 07/10] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
@ 2026-10-16 11:08 ` Jeff Layton
2026-10-16 11:54 ` Matthew Wilcox
0 siblings, 1 reply; 17+ messages in thread
From: Jeff Layton @ 2026-10-16 11:08 UTC (permalink / raw)
To: Mike Snitzer, Chuck Lever
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
On Thu, 2026-10-15 at 19:33 -0400, Mike Snitzer wrote:
> A folio allocated by an RWF_DONTCACHE write is marked dropbehind before
> it is dirtied, so folio_account_dirtied() adds it to WB_DONTCACHE_DIRTY
> and folio_clear_dirty_for_io() or folio_account_cleaned() subtracts it
> when it is cleaned. Nothing marks a folio that already exists. A
> caller that sets PG_dropbehind on a folio that is already dirty
> unbalances the counter: the clean subtracts what was never added, and
> once the counter goes negative, wb_stat_sum() reads it as zero and the
> DONTCACHE writeback kick stops writing anything back for every writer
> on that bdi_writeback.
>
> Add folio_mark_dropbehind(), the inverse of the clear in
> __filemap_get_folio_mpol(): with the folio locked, mark it and, if it is
> dirty, add it to WB_DONTCACHE_DIRTY. The folio lock orders this
> against dirtying and against folio_clear_dirty_for_io(). A concurrent
> lookup without FGP_DONTCACHE that clears the flag again subtracts what
> this added.
>
> The name follows folio_set_dirty() and folio_mark_dirty(): the first
> only sets PG_dirty, the second also accounts the dirty folio.
> folio_set_dropbehind() likewise only sets PG_dropbehind, and
> folio_mark_dropbehind() also accounts it in WB_DONTCACHE_DIRTY.
>
> A later commit uses it in NFSD to drop a page shared by two misaligned
> WRITEs once both have written it.
>
> Cc: Jeff Layton <jlayton@kernel.org>
> Cc: Jens Axboe <axboe@kernel.dk>
> Cc: Matthew Wilcox (Oracle) <willy@infradead.org>
> Cc: linux-mm@kvack.org
> Assisted-by: LLM
> Signed-off-by: Mike Snitzer <snitzer@kernel.org>
> ---
> include/linux/pagemap.h | 1 +
> mm/filemap.c | 31 +++++++++++++++++++++++++++++++
> 2 files changed, 32 insertions(+)
>
> diff --git a/include/linux/pagemap.h b/include/linux/pagemap.h
> index 0adfa6605653d..3012bd95812a1 100644
> --- a/include/linux/pagemap.h
> +++ b/include/linux/pagemap.h
> @@ -1310,6 +1310,7 @@ void end_page_writeback(struct page *page);
> void folio_end_writeback(struct folio *folio);
> void folio_end_writeback_no_dropbehind(struct folio *folio);
> void folio_end_dropbehind(struct folio *folio);
> +void folio_mark_dropbehind(struct folio *folio);
> void folio_wait_stable(struct folio *folio);
> void __folio_mark_dirty(struct folio *folio, struct address_space *, int warn);
> void folio_account_cleaned(struct folio *folio, struct bdi_writeback *wb);
> diff --git a/mm/filemap.c b/mm/filemap.c
> index 00fd89cf6f550..ed7e4dbea653f 100644
> --- a/mm/filemap.c
> +++ b/mm/filemap.c
> @@ -1644,6 +1644,37 @@ void folio_end_dropbehind(struct folio *folio)
> }
> EXPORT_SYMBOL_GPL(folio_end_dropbehind);
>
> +/**
> + * folio_mark_dropbehind - mark an existing folio dropbehind
> + * @folio: the folio, locked by the caller
> + *
> + * Mark @folio so that the writeback that cleans it drops it. A folio that
> + * is already dirty is added to WB_DONTCACHE_DIRTY, as folio_account_dirtied()
> + * would have added it had it been marked when dirtied, so that the clean
> + * that subtracts it leaves the counter balanced.
> + */
> +void folio_mark_dropbehind(struct folio *folio)
> +{
> + struct address_space *mapping = folio->mapping;
> +
> + VM_WARN_ON_ONCE_FOLIO(!folio_test_locked(folio), folio);
> +
> + if (folio_test_dropbehind(folio))
> + return;
> + folio_set_dropbehind(folio);
> + if (mapping && folio_test_dirty(folio) &&
> + mapping_can_writeback(mapping)) {
> + struct inode *inode = mapping->host;
> + struct bdi_writeback *wb;
> + struct wb_lock_cookie cookie = {};
> +
> + wb = unlocked_inode_to_wb_begin(inode, &cookie);
> + wb_stat_mod(wb, WB_DONTCACHE_DIRTY, folio_nr_pages(folio));
> + unlocked_inode_to_wb_end(inode, &cookie);
> + }
> +}
> +EXPORT_SYMBOL_GPL(folio_mark_dropbehind);
> +
> /**
> * folio_end_writeback_no_dropbehind - End writeback against a folio.
> * @folio: The folio.
This patch and #8 need to be split out and merged via the mm tree.
They're related but the rest of the series doesn't specifically depend
on them, and I doubt Chuck wants to carry them.
--
Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v4 07/10] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it
2026-10-16 11:08 ` Jeff Layton
@ 2026-10-16 11:54 ` Matthew Wilcox
2026-10-16 18:36 ` Chuck Lever
0 siblings, 1 reply; 17+ messages in thread
From: Matthew Wilcox @ 2026-10-16 11:54 UTC (permalink / raw)
To: Jeff Layton
Cc: Mike Snitzer, Chuck Lever, linux-nfs, NeilBrown,
Olga Kornievskaia, Dai Ngo, Tom Talpey, Christoph Hellwig,
Jens Axboe, linux-mm
On Fri, Oct 16, 2026 at 07:08:41AM -0400, Jeff Layton wrote:
> This patch and #8 need to be split out and merged via the mm tree.
> They're related but the rest of the series doesn't specifically depend
> on them, and I doubt Chuck wants to carry them.
Patch 9 calls folio_mark_dropbehind()
^ permalink raw reply [flat|nested] 17+ messages in thread
* Re: [PATCH v4 07/10] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it
2026-10-16 11:54 ` Matthew Wilcox
@ 2026-10-16 18:36 ` Chuck Lever
0 siblings, 0 replies; 17+ messages in thread
From: Chuck Lever @ 2026-10-16 18:36 UTC (permalink / raw)
To: Matthew Wilcox (Oracle), Jeff Layton
Cc: Mike Snitzer, linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Christoph Hellwig, Jens Axboe, linux-mm
On Fri, Oct 16, 2026, at 7:54 AM, Matthew Wilcox wrote:
> On Fri, Oct 16, 2026 at 07:08:41AM -0400, Jeff Layton wrote:
>> This patch and #8 need to be split out and merged via the mm tree.
>> They're related but the rest of the series doesn't specifically depend
>> on them, and I doubt Chuck wants to carry them.
>
> Patch 9 calls folio_mark_dropbehind()
Seems like the best plan would be for me to take 1-6 and let the
MM folks take 7-9 with an ACK from Jeff and me when we are happy
with 9.
--
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
^ permalink raw reply [flat|nested] 17+ messages in thread
* [PATCH v4 08/10] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (6 preceding siblings ...)
2026-10-15 23:33 ` [PATCH v4 07/10] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 09/10] NFSD: keep boundary page of a split direct-mode WRITE until both writers complete Mike Snitzer
` (3 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
The per-bdi_writeback WB_DONTCACHE_DIRTY counter drives how much the
flusher writes back in response to an IOCB_DONTCACHE kick, but it is
not reported anywhere, so the DONTCACHE dirty backlog can only be
inferred from MemFree/Cached movement.
Report it as BdiDontcacheDirty in /sys/kernel/debug/bdi/<dev>/stats
and as WbDontcacheDirty in the per-wb stats, next to the
Reclaimable line. Use wb_stat_sum() rather than wb_stat() to fold in
the per-cpu batches: the flusher targets the summed value and small
backlogs would otherwise read as zero.
Fixes: 88d6f128d06d ("mm: track DONTCACHE dirty pages per bdi_writeback")
Cc: Jeff Layton <jlayton@kernel.org>
Cc: Jens Axboe <axboe@kernel.dk>
Cc: Matthew Wilcox (Oracle) <willy@infradead.org>
Cc: linux-mm@kvack.org
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
mm/backing-dev.c | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/mm/backing-dev.c b/mm/backing-dev.c
index 18e999053bae0..b8a2c27e07fac 100644
--- a/mm/backing-dev.c
+++ b/mm/backing-dev.c
@@ -46,6 +46,7 @@ struct wb_stats {
unsigned long nr_dirty_time;
unsigned long nr_writeback;
unsigned long nr_reclaimable;
+ unsigned long nr_dontcache_dirty;
unsigned long nr_dirtied;
unsigned long nr_written;
unsigned long dirty_thresh;
@@ -78,6 +79,12 @@ static void collect_wb_stats(struct wb_stats *stats,
stats->nr_writeback += wb_stat(wb, WB_WRITEBACK);
stats->nr_reclaimable += wb_stat(wb, WB_RECLAIMABLE);
+ /*
+ * Sum the per-cpu batches: the dontcache flusher targets this
+ * counter via wb_stat_sum() and small backlogs would otherwise
+ * read as zero here.
+ */
+ stats->nr_dontcache_dirty += wb_stat_sum(wb, WB_DONTCACHE_DIRTY);
stats->nr_dirtied += wb_stat(wb, WB_DIRTIED);
stats->nr_written += wb_stat(wb, WB_WRITTEN);
stats->wb_thresh += wb_calc_thresh(wb, stats->dirty_thresh);
@@ -125,6 +132,7 @@ static int bdi_debug_stats_show(struct seq_file *m, void *v)
seq_printf(m,
"BdiWriteback: %10lu kB\n"
"BdiReclaimable: %10lu kB\n"
+ "BdiDontcacheDirty: %10lu kB\n"
"BdiDirtyThresh: %10lu kB\n"
"DirtyThresh: %10lu kB\n"
"BackgroundThresh: %10lu kB\n"
@@ -139,6 +147,7 @@ static int bdi_debug_stats_show(struct seq_file *m, void *v)
"state: %10lx\n",
K(stats.nr_writeback),
K(stats.nr_reclaimable),
+ K(stats.nr_dontcache_dirty),
K(stats.wb_thresh),
K(dirty_thresh),
K(background_thresh),
@@ -163,6 +172,7 @@ static void wb_stats_show(struct seq_file *m, struct bdi_writeback *wb,
"WbCgIno: %10lu\n"
"WbWriteback: %10lu kB\n"
"WbReclaimable: %10lu kB\n"
+ "WbDontcacheDirty: %10lu kB\n"
"WbDirtyThresh: %10lu kB\n"
"WbDirtied: %10lu kB\n"
"WbWritten: %10lu kB\n"
@@ -179,6 +189,7 @@ static void wb_stats_show(struct seq_file *m, struct bdi_writeback *wb,
#endif
K(stats->nr_writeback),
K(stats->nr_reclaimable),
+ K(stats->nr_dontcache_dirty),
K(stats->wb_thresh),
K(stats->nr_dirtied),
K(stats->nr_written),
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH v4 09/10] NFSD: keep boundary page of a split direct-mode WRITE until both writers complete
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (7 preceding siblings ...)
2026-10-15 23:33 ` [PATCH v4 08/10] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-15 23:33 ` [PATCH v4 10/10] NFSD: add tracing for how direct-mode READ and WRITE are serviced Mike Snitzer
` (2 subsequent siblings)
11 siblings, 0 replies; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm,
Jonathan Flynn
A direct-mode WRITE whose ends are not aligned for direct I/O is split
into a buffered prefix, a direct middle and a buffered suffix, and the
prefix and suffix are issued IOCB_DONTCACHE so their pages are dropped
once written back. The page holding such a segment is shared by two
WRITEs, the one ending in it and the one starting in it, which may
arrive in either order, from different clients, at the same time. If
it is dropped as soon as the first writer's data is written back, the
second has to read the block from disk before it can complete it: one
4 KiB read per WRITE. With many clients interleaving small misaligned
O_DIRECT records into one shared file, that was 0.81 device reads per
record (585 GiB read while writing 8.1 TiB).
Keep the page until both writers have had it, with no state in NFSD.
Immediately before a buffered DONTCACHE segment is written,
nfsd_write_dio_boundary_claim() puts an empty folio in the page cache
at each end of the segment that covers its page only partly, if there
is not one there already. The folio is inserted unmarked: a DONTCACHE
write marks only a folio it allocates itself, so neither writer marks
this one and no writeback completion drops it.
The add is atomic, so of two concurrent partners exactly one gets the
folio. The other finds it (-EEXIST), is therefore the second of the
two, and once its data is in the page nfsd_write_dio_boundary_complete()
marks the folio with folio_mark_dropbehind(). The mark must follow
the write: set while the first writer's data is still dirty or under
writeback, it would have that writeback drop the folio before the
second writer's data is in it.
A WRITE marks its shared pages only once it is complete. A FILE_SYNC or
DATA_SYNC WRITE marks them after its own fsync, when they are clean, and
nfsd_write_dio_boundary_complete() drops them at once. Marking them
while still dirty would count them in WB_DONTCACHE_DIRTY and wake the
DONTCACHE writeback kick, only to write back what the fsync is about to
write. An UNSTABLE WRITE marks them dirty, and the writeback that
cleans them, by the flusher or for the client's COMMIT, drops them.
A WRITE that is not split is issued as one buffered DONTCACHE segment,
and its partly covered first and last pages are claimed the same way.
A WRITE smaller than a page has one page, which may be shared with both
neighbours; it is kept until the second of them has written it.
The state lives in the page cache. A claimed page whose partner never
arrives, such as the last page of the file, or that is removed before
the partner arrives, by truncate, reclaim or another client's direct
WRITE, is never marked and stays cached until it is reclaimed. The
direct middle invalidates the page cache over its range, but a split
WRITE's misaligned ends are cut on page boundaries, so the middle never
reaches a claimed page. Nothing is claimed when the buffered segments
are normal buffered I/O (direct_misaligned_dontcache is N, or the file
system lacks FOP_DONTCACHE), or when the mapping's minimum folio size is
larger than a page.
Measured with 32 interleaved writers over an emulated 4Kn NVMe with 4
KiB pages, direct_misaligned_dontcache=Y and every WRITE persisted as
FILE_SYNC before its reply, each arm starting from a freshly made file
system. 47008-byte records, which split: 704 device reads for 42895
WRITEs, against 45144 for 45664 WRITEs without this patch. 6000-byte
records, which are not split: 385 reads against 196589. 81 of the
written file's 524066 pages are still resident afterwards. With
47008-byte records the DONTCACHE writeback kick ran 692 times in 15 s
with the pages marked after the fsync, against 24800 times when they
were marked as soon as they were written.
On a four-server pNFS flexfiles rig (x86_64, 4 KiB logical blocks),
three clients writing 47008-byte records for 240 s against 640 nfsd
threads per server: device reads fall from 0.014-0.015 per record to
0.003, page-cache growth over the write phase falls from about 11 GiB to
2.9 GiB, and write throughput rises by 2.4% to 4.9% whether the WRITEs
are answered UNSTABLE, DATA_SYNC or FILE_SYNC, with read throughput
unchanged. Keeping every boundary page cached instead, with
direct_misaligned_dontcache=N, grows the page cache by about 630 GiB
over the same runs.
Reported-by: Jonathan Flynn <jonathan.flynn@hammerspace.com>
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
.../filesystems/nfs/nfsd-io-modes.rst | 15 ++
fs/nfsd/vfs.c | 141 ++++++++++++++++--
2 files changed, 145 insertions(+), 11 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index c1cc974640cfe..278c0487b5bb2 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -164,6 +164,21 @@ Misaligned WRITE:
not split, as normal buffered IO instead of DONTCACHE, which suits a
workload that reads back or rewrites what it just wrote.
+ The page holding a start or end segment is shared with the
+ neighbouring WRITE, which may arrive before, after or at the same
+ time, from any client. So that the second WRITE to such a page does
+ not have to read it back from disk, NFSD puts an empty page there
+ immediately before writing a start or end segment, if there is not
+ one already, without marking it DONTCACHE. The WRITE that finds the
+ page already there marks it DONTCACHE once it has written it, and
+ the writeback that then cleans it drops it. A page whose neighbouring
+ WRITE never arrives stays cached until it is reclaimed. The first and
+ last pages of a WRITE that is not split are handled the same way.
+
+ None of this applies when the start and end segments are normal
+ buffered IO, or when the filesystem's block size is larger than the
+ page size.
+
Tracing:
The nfsd_read_direct trace event shows how NFSD expands any
misaligned READ to the next DIO-aligned block (on either end of the
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index 249358256d2f5..d899d00714b1a 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -26,6 +26,7 @@
#include <linux/xattr.h>
#include <linux/jhash.h>
#include <linux/pagemap.h>
+#include <linux/backing-dev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/exportfs.h>
@@ -1300,6 +1301,7 @@ static int wait_for_concurrent_writes(struct file *file)
struct nfsd_write_dio_seg {
struct iov_iter iter;
int flags;
+ bool claim; /* keep shared pages */
};
static unsigned long
@@ -1319,6 +1321,110 @@ nfsd_write_dio_seg_init(struct nfsd_write_dio_seg *segment,
iov_iter_advance(&segment->iter, start);
iov_iter_truncate(&segment->iter, len);
segment->flags = iocb->ki_flags;
+ segment->claim = false;
+}
+
+/*
+ * A page that a buffered segment covers only partly is shared with the
+ * neighbouring WRITE. Put an unmarked folio there if there is none: a
+ * DONTCACHE write marks only a folio it allocates, so neither WRITE marks
+ * this one and no writeback drops it before the other WRITE has written it.
+ *
+ * Return: true if a folio was already there. This WRITE is then the
+ * second of the two and calls nfsd_write_dio_boundary_complete() after
+ * writing the page.
+ */
+static bool
+nfsd_write_dio_boundary_claim(struct file *file, loff_t pos)
+{
+ struct address_space *mapping = file->f_mapping;
+ gfp_t gfp = mapping_gfp_mask(mapping);
+ struct folio *folio;
+ int err;
+
+ /* A folio larger than a page holds more than the shared page. */
+ if (mapping_min_folio_order(mapping))
+ return false;
+ if (mapping_can_writeback(mapping))
+ gfp |= __GFP_WRITE;
+ folio = filemap_alloc_folio(gfp, 0, NULL);
+ if (!folio)
+ return false;
+ err = filemap_add_folio(mapping, folio, pos >> PAGE_SHIFT, gfp);
+ if (!err)
+ folio_unlock(folio);
+ folio_put(folio);
+ return err == -EEXIST;
+}
+
+/*
+ * Both WRITEs have written the page: mark it so that the writeback that
+ * cleans it drops it.
+ */
+static void
+nfsd_write_dio_boundary_complete(struct file *file, loff_t pos)
+{
+ struct folio *folio;
+
+ folio = __filemap_get_folio(file->f_mapping, pos >> PAGE_SHIFT,
+ FGP_LOCK | FGP_DONTCACHE, 0);
+ if (IS_ERR(folio))
+ return;
+ folio_mark_dropbehind(folio);
+ folio_unlock(folio);
+ /* Already written back: drop it now, as its writeback would have. */
+ folio_end_dropbehind(folio);
+ folio_put(folio);
+}
+
+/* The shared pages a segment claimed, and how much of it was written. */
+struct nfsd_write_dio_hold {
+ loff_t first, last;
+ bool complete_first, complete_last;
+ size_t written;
+};
+
+/*
+ * Claim the pages @seg shares with the neighbouring WRITEs, immediately
+ * before writing it: an earlier claim gives the partner WRITE time to
+ * complete the page and have it dropped first.
+ */
+static void
+nfsd_write_dio_seg_hold(struct file *file, const struct nfsd_write_dio_seg *seg,
+ loff_t pos, size_t len, struct nfsd_write_dio_hold *hold)
+{
+ hold->first = pos;
+ hold->last = pos + len - 1;
+ hold->complete_first = false;
+ hold->complete_last = false;
+ hold->written = 0;
+ if (seg->claim && len) {
+ bool one_page = (hold->first >> PAGE_SHIFT) ==
+ (hold->last >> PAGE_SHIFT);
+ bool head = hold->first & ~PAGE_MASK;
+ bool tail = (hold->last + 1) & ~PAGE_MASK;
+
+ /* Only a page the segment covers partly is shared. */
+ if (head || (one_page && tail))
+ hold->complete_first =
+ nfsd_write_dio_boundary_claim(file, hold->first);
+ if (!one_page && tail)
+ hold->complete_last =
+ nfsd_write_dio_boundary_claim(file, hold->last);
+ }
+}
+
+/* Mark the shared pages this WRITE was the second of two to write. */
+static void
+nfsd_write_dio_seg_release(struct file *file,
+ const struct nfsd_write_dio_hold *hold)
+{
+ if (hold->complete_first && hold->written)
+ nfsd_write_dio_boundary_complete(file, hold->first);
+ /* A short write did not reach the last page. */
+ if (hold->complete_last &&
+ hold->written == hold->last - hold->first + 1)
+ nfsd_write_dio_boundary_complete(file, hold->last);
}
static unsigned int
@@ -1378,7 +1484,8 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
if (prefix) {
nfsd_write_dio_seg_init(&segments[nsegs], bvec,
nvecs, total, 0, prefix, iocb);
- segments[nsegs++].flags |= buffered_flags;
+ segments[nsegs].flags |= buffered_flags;
+ segments[nsegs++].claim = !!buffered_flags;
}
nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs,
@@ -1402,7 +1509,8 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
if (suffix) {
nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs, total,
prefix + middle, suffix, iocb);
- segments[nsegs++].flags |= buffered_flags;
+ segments[nsegs].flags |= buffered_flags;
+ segments[nsegs++].claim = !!buffered_flags;
}
return nsegs;
@@ -1416,6 +1524,7 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
nfsd_write_dio_seg_init(&segments[0], bvec, nvecs, total, 0,
total, iocb);
segments[0].flags |= buffered_flags;
+ segments[0].claim = !!buffered_flags;
return 1;
}
@@ -1424,12 +1533,13 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
struct nfsd_file *nf, unsigned int nvecs,
unsigned long *cnt, struct kiocb *kiocb)
{
+ struct nfsd_write_dio_hold holds[3];
struct nfsd_write_dio_seg segments[3];
struct file *file = nf->nf_file;
loff_t start = kiocb->ki_pos;
- unsigned int nsegs, i;
+ unsigned int nsegs, nheld = 0, i;
+ ssize_t host_err = 0;
int sync_flags;
- ssize_t host_err;
size_t expected;
nsegs = nfsd_write_dio_iters_init(nf, rqstp->rq_bvec, nvecs,
@@ -1455,23 +1565,32 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
segments[i].iter.count);
expected = iov_iter_count(&segments[i].iter);
-
+ nfsd_write_dio_seg_hold(file, &segments[i], kiocb->ki_pos,
+ expected, &holds[nheld]);
host_err = vfs_iocb_iter_write(file, kiocb, &segments[i].iter);
if (host_err < 0)
- return host_err;
+ break;
+ holds[nheld++].written = host_err;
*cnt += host_err;
if (host_err < (ssize_t)expected)
break; /* partial write */
}
- if (sync_flags && *cnt) {
+ if (host_err >= 0 && sync_flags && *cnt)
host_err = vfs_fsync_range(file, start, start + *cnt - 1,
!(sync_flags & IOCB_SYNC));
- if (host_err < 0)
- return host_err;
- }
- return 0;
+ /*
+ * Mark the pages this WRITE shares with its neighbours for dropping
+ * only once the WRITE is complete, including its fsync if it has one.
+ * A page marked while still dirty is counted as DONTCACHE dirty and
+ * wakes the DONTCACHE writeback kick; a page marked once clean is
+ * simply dropped.
+ */
+ for (i = 0; i < nheld; i++)
+ nfsd_write_dio_seg_release(file, &holds[i]);
+
+ return host_err < 0 ? host_err : 0;
}
/**
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* [PATCH v4 10/10] NFSD: add tracing for how direct-mode READ and WRITE are serviced
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (8 preceding siblings ...)
2026-10-15 23:33 ` [PATCH v4 09/10] NFSD: keep boundary page of a split direct-mode WRITE until both writers complete Mike Snitzer
@ 2026-10-15 23:33 ` Mike Snitzer
2026-10-16 11:18 ` [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Jeff Layton
2026-10-16 19:23 ` [PATCH v4 0/10] " Chuck Lever
11 siblings, 0 replies; 17+ messages in thread
From: Mike Snitzer @ 2026-10-15 23:33 UTC (permalink / raw)
To: Chuck Lever, Jeff Layton
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
When io_cache_read or io_cache_write selects a direct I/O mode, a
request may still be serviced with DONTCACHE buffered or normal
buffered I/O, and a WRITE may be split into up to three segments that
each take a different path depending on the request's offset/length
alignment, the payload's memory alignment, the filesystem's
FOP_DONTCACHE support, direct_misaligned_min_bytes and
direct_misaligned_dontcache. Until now the only visibility was
nfsd_{read,write}_direct for O_DIRECT and nfsd_{read,write}_vector for
everything else, so the DONTCACHE and cached buffered cases were
indistinguishable and the reason a WRITE was not issued as direct I/O
was not recorded at all.
Add:
- nfsd_write_dio_split, emitted once per direct-mode WRITE before any
segment is issued. It records the advertised offset and memory
alignments, the payload's offset within its first page, the start,
middle and end segment sizes, a disposition naming the path taken
(direct, no_alignment, too_small, no_middle, mem_misaligned), and
whether buffered segments are issued IOCB_DONTCACHE.
- nfsd_write_dontcache and nfsd_read_dontcache, emitted for WRITE
segments and READs serviced with IOCB_DONTCACHE. nfsd_write_vector
and nfsd_read_vector now fire only for normal buffered I/O.
The disposition enum lives in vfs.h, which trace.h already includes.
Document the new events in nfsd-io-modes.rst, together with iomap's
iomap_dio_invalidate_fail event, which reports an O_DIRECT segment that
the filesystem serviced with buffered I/O instead.
Assisted-by: LLM
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
.../filesystems/nfs/nfsd-io-modes.rst | 38 ++++++++-
fs/nfsd/trace.h | 77 +++++++++++++++++++
fs/nfsd/vfs.c | 47 ++++++++---
fs/nfsd/vfs.h | 9 +++
4 files changed, 156 insertions(+), 15 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index 278c0487b5bb2..2a648cedf2588 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -182,21 +182,53 @@ Misaligned WRITE:
Tracing:
The nfsd_read_direct trace event shows how NFSD expands any
misaligned READ to the next DIO-aligned block (on either end of the
- original READ, as needed).
+ original READ, as needed). A READ that is serviced with buffered IO
+ instead emits nfsd_read_dontcache (DONTCACHE buffered IO) or
+ nfsd_read_vector (normal buffered IO).
This combination of trace events is useful for READs::
echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_read_vector/enable
+ echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_read_dontcache/enable
echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_read_direct/enable
echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_read_io_done/enable
echo 1 > /sys/kernel/tracing/events/xfs/xfs_file_direct_read/enable
- The nfsd_write_direct trace event shows how NFSD splits a given
- misaligned WRITE into a DIO-aligned middle segment.
+ The nfsd_write_dio_split trace event is emitted once per WRITE in a
+ DIRECT IO mode, before any IO is issued. It records the alignments
+ the filesystem advertised, the payload's offset in its first page,
+ the sizes of the start, middle and end segments, and a
+ disposition::
+
+ direct the aligned middle segment uses O_DIRECT
+ mem_misaligned payload memory is misaligned; not split
+ no_alignment the filesystem advertises no DIO alignment; not split
+ too_small smaller than the larger of the offset and memory
+ alignments; not split
+ no_middle no aligned middle, or a misaligned start or end
+ with a middle smaller than
+ direct_misaligned_min_bytes bytes; not split
+
+ dontcache=1 means any buffered segment is issued DONTCACHE: the start
+ and end segments of a split WRITE, or the whole of a WRITE that is
+ not split. It is 0 when direct_misaligned_dontcache is N or the
+ filesystem lacks FOP_DONTCACHE, and is reported even for a WRITE
+ that has no buffered segment.
+
+ Each segment then emits nfsd_write_direct (O_DIRECT),
+ nfsd_write_dontcache (DONTCACHE buffered IO) or nfsd_write_vector
+ (normal buffered IO).
This combination of trace events is useful for WRITEs::
echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_write_opened/enable
+ echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_write_dio_split/enable
echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_write_direct/enable
+ echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_write_dontcache/enable
+ echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_write_vector/enable
echo 1 > /sys/kernel/tracing/events/nfsd/nfsd_write_io_done/enable
echo 1 > /sys/kernel/tracing/events/xfs/xfs_file_direct_write/enable
+ echo 1 > /sys/kernel/tracing/events/iomap/iomap_dio_invalidate_fail/enable
+
+ iomap_dio_invalidate_fail reports an O_DIRECT middle segment that
+ the filesystem serviced with buffered IO instead.
diff --git a/fs/nfsd/trace.h b/fs/nfsd/trace.h
index e5d19c6f48a2c..8783c092b6338 100644
--- a/fs/nfsd/trace.h
+++ b/fs/nfsd/trace.h
@@ -501,6 +501,7 @@ DEFINE_EVENT(nfsd_io_class, nfsd_##name, \
DEFINE_NFSD_IO_EVENT(read_start);
DEFINE_NFSD_IO_EVENT(read_splice);
DEFINE_NFSD_IO_EVENT(read_vector);
+DEFINE_NFSD_IO_EVENT(read_dontcache);
DEFINE_NFSD_IO_EVENT(read_direct);
DEFINE_NFSD_IO_EVENT(read_io_done);
DEFINE_NFSD_IO_EVENT(read_done);
@@ -508,11 +509,87 @@ DEFINE_NFSD_IO_EVENT(write_start);
DEFINE_NFSD_IO_EVENT(write_opened);
DEFINE_NFSD_IO_EVENT(write_direct);
DEFINE_NFSD_IO_EVENT(write_vector);
+DEFINE_NFSD_IO_EVENT(write_dontcache);
DEFINE_NFSD_IO_EVENT(write_io_done);
DEFINE_NFSD_IO_EVENT(write_done);
DEFINE_NFSD_IO_EVENT(commit_start);
DEFINE_NFSD_IO_EVENT(commit_done);
+TRACE_DEFINE_ENUM(NFSD_WRITE_DIO_DIRECT);
+TRACE_DEFINE_ENUM(NFSD_WRITE_DIO_MEM_MISALIGNED);
+TRACE_DEFINE_ENUM(NFSD_WRITE_DIO_NO_ALIGNMENT);
+TRACE_DEFINE_ENUM(NFSD_WRITE_DIO_TOO_SMALL);
+TRACE_DEFINE_ENUM(NFSD_WRITE_DIO_NO_MIDDLE);
+
+#define show_nfsd_write_dio_disposition(x) \
+ __print_symbolic(x, \
+ { NFSD_WRITE_DIO_DIRECT, "direct" }, \
+ { NFSD_WRITE_DIO_MEM_MISALIGNED, "mem_misaligned" }, \
+ { NFSD_WRITE_DIO_NO_ALIGNMENT, "no_alignment" }, \
+ { NFSD_WRITE_DIO_TOO_SMALL, "too_small" }, \
+ { NFSD_WRITE_DIO_NO_MIDDLE, "no_middle" })
+
+/*
+ * Emitted once per direct-mode WRITE, before any segment is issued.
+ * prefix, middle and suffix are zero when not computed. mem_offset is
+ * the offset of the payload within its first page.
+ */
+TRACE_EVENT(nfsd_write_dio_split,
+ TP_PROTO(struct svc_rqst *rqstp,
+ struct svc_fh *fhp,
+ u64 offset,
+ u32 len,
+ u32 offset_align,
+ u32 mem_align,
+ u32 mem_offset,
+ u32 prefix,
+ u32 middle,
+ u32 suffix,
+ enum nfsd_write_dio_disposition disposition,
+ bool dontcache),
+ TP_ARGS(rqstp, fhp, offset, len, offset_align, mem_align, mem_offset,
+ prefix, middle, suffix, disposition, dontcache),
+ TP_STRUCT__entry(
+ __field(u32, xid)
+ __field(u32, fh_hash)
+ __field(u64, offset)
+ __field(u32, len)
+ __field(u32, offset_align)
+ __field(u32, mem_align)
+ __field(u32, mem_offset)
+ __field(u32, prefix)
+ __field(u32, middle)
+ __field(u32, suffix)
+ __field(unsigned int, disposition)
+ __field(bool, dontcache)
+ ),
+ TP_fast_assign(
+ __entry->xid = be32_to_cpu(rqstp->rq_xid);
+ __entry->fh_hash = knfsd_fh_hash(&fhp->fh_handle);
+ __entry->offset = offset;
+ __entry->len = len;
+ __entry->offset_align = offset_align;
+ __entry->mem_align = mem_align;
+ __entry->mem_offset = mem_offset;
+ __entry->prefix = prefix;
+ __entry->middle = middle;
+ __entry->suffix = suffix;
+ __entry->disposition = disposition;
+ __entry->dontcache = dontcache;
+ ),
+ TP_printk("xid=0x%08x fh_hash=0x%08x offset=%llu len=%u "
+ "offset_align=%u mem_align=%u mem_offset=%u "
+ "prefix=%u middle=%u suffix=%u disposition=%s "
+ "dontcache=%u",
+ __entry->xid, __entry->fh_hash,
+ __entry->offset, __entry->len,
+ __entry->offset_align, __entry->mem_align,
+ __entry->mem_offset,
+ __entry->prefix, __entry->middle, __entry->suffix,
+ show_nfsd_write_dio_disposition(__entry->disposition),
+ __entry->dontcache)
+);
+
DECLARE_EVENT_CLASS(nfsd_err_class,
TP_PROTO(struct svc_rqst *rqstp,
struct svc_fh *fhp,
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index d899d00714b1a..d56b2f01b1cfd 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -1255,7 +1255,10 @@ __be32 nfsd_iter_read(struct svc_rqst *rqstp, struct svc_fh *fhp,
}
rqstp->rq_next_page = page;
- trace_nfsd_read_vector(rqstp, fhp, offset, *count - total);
+ if (kiocb.ki_flags & IOCB_DONTCACHE)
+ trace_nfsd_read_dontcache(rqstp, fhp, offset, *count - total);
+ else
+ trace_nfsd_read_vector(rqstp, fhp, offset, *count - total);
iov_iter_bvec(&iter, ITER_DEST, rqstp->rq_bvec, v, *count - total);
host_err = vfs_iocb_iter_read(file, &kiocb, &iter);
return nfsd_finish_read(rqstp, fhp, file, offset, count, eof, host_err);
@@ -1428,7 +1431,8 @@ nfsd_write_dio_seg_release(struct file *file,
}
static unsigned int
-nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
+nfsd_write_dio_iters_init(struct svc_rqst *rqstp, struct svc_fh *fhp,
+ struct nfsd_file *nf, struct bio_vec *bvec,
unsigned int nvecs, struct kiocb *iocb,
unsigned long total,
struct nfsd_write_dio_seg segments[3])
@@ -1436,7 +1440,9 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
u32 offset_align = nf->nf_dio_offset_align;
loff_t prefix_end, orig_end, middle_end;
u32 mem_align = nf->nf_dio_mem_align;
- size_t prefix, middle, suffix;
+ size_t prefix = 0, middle = 0, suffix = 0;
+ enum nfsd_write_dio_disposition disposition;
+ u32 mem_offset = nvecs ? bvec->bv_offset : 0;
loff_t offset = iocb->ki_pos;
unsigned int dontcache_flags = 0;
unsigned int buffered_flags;
@@ -1452,9 +1458,14 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
* If alignments are not available, the write is too small,
* or no alignment can be found, fall back to buffered I/O.
*/
- if (unlikely(!mem_align || !offset_align) ||
- unlikely(total < max(offset_align, mem_align)))
+ if (unlikely(!mem_align || !offset_align)) {
+ disposition = NFSD_WRITE_DIO_NO_ALIGNMENT;
goto no_dio;
+ }
+ if (unlikely(total < max(offset_align, mem_align))) {
+ disposition = NFSD_WRITE_DIO_TOO_SMALL;
+ goto no_dio;
+ }
prefix_end = round_up(offset, offset_align);
orig_end = offset + total;
@@ -1478,8 +1489,10 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
if (!middle ||
((prefix || suffix) &&
- middle < nfsd_direct_misaligned_min_bytes))
+ middle < nfsd_direct_misaligned_min_bytes)) {
+ disposition = NFSD_WRITE_DIO_NO_MIDDLE;
goto no_dio;
+ }
if (prefix) {
nfsd_write_dio_seg_init(&segments[nsegs], bvec,
@@ -1498,13 +1511,16 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
* the first bvec, all subsequent bvecs start at bv_offset zero
* (page-aligned). Therefore, only the first bvec is checked.
*/
- if (iov_iter_bvec_offset(&segments[nsegs].iter) & (mem_align - 1))
+ if (iov_iter_bvec_offset(&segments[nsegs].iter) & (mem_align - 1)) {
+ disposition = NFSD_WRITE_DIO_MEM_MISALIGNED;
goto no_dio;
+ }
/*
* IOCB_DONTCACHE matters only if the file system falls back to
* buffered I/O (-ENOTBLK).
*/
segments[nsegs++].flags |= IOCB_DIRECT | dontcache_flags;
+ disposition = NFSD_WRITE_DIO_DIRECT;
if (suffix) {
nfsd_write_dio_seg_init(&segments[nsegs], bvec, nvecs, total,
@@ -1512,8 +1528,7 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
segments[nsegs].flags |= buffered_flags;
segments[nsegs++].claim = !!buffered_flags;
}
-
- return nsegs;
+ goto out;
no_dio:
/*
@@ -1525,7 +1540,12 @@ nfsd_write_dio_iters_init(struct nfsd_file *nf, struct bio_vec *bvec,
total, iocb);
segments[0].flags |= buffered_flags;
segments[0].claim = !!buffered_flags;
- return 1;
+ nsegs = 1;
+out:
+ trace_nfsd_write_dio_split(rqstp, fhp, offset, total, offset_align,
+ mem_align, mem_offset, prefix, middle,
+ suffix, disposition, !!buffered_flags);
+ return nsegs;
}
static noinline_for_stack int
@@ -1542,8 +1562,8 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
int sync_flags;
size_t expected;
- nsegs = nfsd_write_dio_iters_init(nf, rqstp->rq_bvec, nvecs,
- kiocb, *cnt, segments);
+ nsegs = nfsd_write_dio_iters_init(rqstp, fhp, nf, rqstp->rq_bvec,
+ nvecs, kiocb, *cnt, segments);
/*
* Persist a synchronous WRITE once, after all of its segments. A
@@ -1560,6 +1580,9 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
if (kiocb->ki_flags & IOCB_DIRECT)
trace_nfsd_write_direct(rqstp, fhp, kiocb->ki_pos,
segments[i].iter.count);
+ else if (kiocb->ki_flags & IOCB_DONTCACHE)
+ trace_nfsd_write_dontcache(rqstp, fhp, kiocb->ki_pos,
+ segments[i].iter.count);
else
trace_nfsd_write_vector(rqstp, fhp, kiocb->ki_pos,
segments[i].iter.count);
diff --git a/fs/nfsd/vfs.h b/fs/nfsd/vfs.h
index eb75e4c823185..8f6349233c892 100644
--- a/fs/nfsd/vfs.h
+++ b/fs/nfsd/vfs.h
@@ -229,4 +229,13 @@ __be32 nfsd_permission(struct svc_cred *cred, struct svc_export *exp,
void nfsd_filp_close(struct file *fp);
+/* The path nfsd_write_dio_iters_init() took for a direct-mode WRITE. */
+enum nfsd_write_dio_disposition {
+ NFSD_WRITE_DIO_DIRECT,
+ NFSD_WRITE_DIO_MEM_MISALIGNED,
+ NFSD_WRITE_DIO_NO_ALIGNMENT,
+ NFSD_WRITE_DIO_TOO_SMALL,
+ NFSD_WRITE_DIO_NO_MIDDLE,
+};
+
#endif /* LINUX_NFSD_VFS_H */
--
2.52.0
^ permalink raw reply related [flat|nested] 17+ messages in thread* Re: [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (9 preceding siblings ...)
2026-10-15 23:33 ` [PATCH v4 10/10] NFSD: add tracing for how direct-mode READ and WRITE are serviced Mike Snitzer
@ 2026-10-16 11:18 ` Jeff Layton
2026-10-16 19:23 ` [PATCH v4 0/10] " Chuck Lever
11 siblings, 0 replies; 17+ messages in thread
From: Jeff Layton @ 2026-10-16 11:18 UTC (permalink / raw)
To: Mike Snitzer, Chuck Lever
Cc: linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Matthew Wilcox, linux-mm
On Thu, 2026-10-15 at 19:32 -0400, Mike Snitzer wrote:
> Hi,
>
> This series refines the heuristics NFSD_IO_DIRECT already uses, mostly
> on the WRITE path and in one corner of the READ path: the io_cache_write
> and io_cache_read modes where an aligned request goes to the filesystem
> as O_DIRECT, with DONTCACHE for the parts a misaligned WRITE cannot issue
> directly.
>
> It does not knowingly undercut anything the existing upstream code is
> built on. An aligned WRITE is still one O_DIRECT write and an aligned
> READ is still one O_DIRECT read. A misaligned WRITE is still split into
> a direct middle and buffered ends, and by default those ends are still
> DONTCACHE where the filesystem supports it. io_cache_read and
> io_cache_write mean what they did, and nothing new is asked of the
> filesystem. What changes is where the split is cut and when it is worth
> making, which READs are worth expanding, how often a synchronous WRITE
> is persisted, and what happens to the page two misaligned WRITEs share.
>
> 1/10 is a stable fix. A misaligned WRITE is split into a direct middle
> and buffered ends, and the middle was cut at the filesystem's DIO offset
> alignment. Where the page is larger than that alignment (16 and 64 KiB
> pages over 4096-byte blocks, or 4 KiB pages over 512-byte blocks) the
> middle then starts or stops inside the page a buffered end writes, its
> invalidation drops that page, and whichever of the two neighbouring
> WRITEs comes second reads it back from the device. A misaligned end is
> now cut at a multiple of the larger of the alignment and the page size.
> An end that is already DIO-aligned gets no buffered segment and stays
> where it is, so a DIO-aligned WRITE is still issued direct on every page
> size. Such an end can share its page with a buffered neighbour, but
> NFSD's direct WRITEs are synchronous and XFS holds its IOLOCK across
> iomap_dio_rw(), the page cache invalidation after the write included,
> so the neighbour cannot dirty that page in between and make the
> invalidation fail.
>
> 2/10 also marks the direct middle IOCB_DONTCACHE, for when XFS
> re-issues it as buffered I/O (-ENOTBLK) because something else has
> dirtied page cache in its range, such as a local process writing the
> exported file through mmap, which XFS's IOLOCK does not keep out.
>
> 3/10 splits a misaligned WRITE only for a direct middle of at least
> direct_misaligned_min_bytes (debugfs, default 8192), and moves the
> segment flag decisions into nfsd_write_dio_iters_init(), both from
> Christoph's suggested diff. The threshold counts bytes: with 1/10 a
> split WRITE's middle is whole pages, and a threshold of two pages would
> demand a 32 KiB middle on 16 KiB pages and 128 KiB on 64 KiB pages.
>
> 4/10 (Jonathan) adds direct_misaligned_dontcache (default Y), which
> selects whether the parts that are not O_DIRECT are DONTCACHE or
> cached.
>
> 5/10 stops expanding a READ smaller than its alignment into one or two
> whole aligned device reads. The check sits in nfsd_direct_read_ok(),
> beside the last-operation-in-COMPOUND check now in nfsd-testing.
>
> 6/10 persists a FILE_SYNC or DATA_SYNC WRITE that is split, or issued
> buffered, once after all of its segments, instead of once per segment:
> one fsync where there were up to three. A WRITE issued whole as
> O_DIRECT keeps IOCB_DSYNC, so iomap can still complete a DATA_SYNC
> overwrite with FUA writes.
>
> 7/10 is an mm patch: folio_mark_dropbehind() marks an existing folio
> dropbehind and keeps WB_DONTCACHE_DIRTY balanced when the folio is
> already dirty, as folio_mark_dirty() does for PG_dirty where
> folio_set_dirty() only sets the flag. Marking an already dirty folio
> with folio_set_dropbehind() instead drives the counter negative, and
> the DONTCACHE writeback kick then stops writing anything back for every
> writer on that bdi_writeback.
>
> 8/10 is also mm: it reports WB_DONTCACHE_DIRTY in the bdi debugfs stats,
> as BdiDontcacheDirty and per bdi_writeback as WbDontcacheDirty, summed
> over the per-cpu batches since the flusher targets the summed value.
> The counter drives the DONTCACHE writeback kick but was not reported
> anywhere; this is how the balance 7/10 keeps was observed. It carries
> Fixes: for the commit that added the counter.
>
> 7/10 and 8/10 are Cc'd to linux-mm and need an mm ack.
>
> 9/10 calls folio_mark_dropbehind() to keep the page two misaligned
> WRITEs share until both have written it, with no state in NFSD: 704
> device reads for 42895 WRITEs where there were 45144 for 45664
> (47008-byte records, 32 interleaved writers, 4 KiB pages), and on a
> four-server pNFS flexfiles rig 0.003 device reads per record instead of
> 0.014-0.015, about 2.9 GiB of page-cache growth instead of 11 GiB, and
> 2.4% to 4.9% more write throughput.
>
> 10/10 adds tracepoints for how each direct-mode READ and WRITE was
> serviced, and why a WRITE was not issued direct.
>
> The series applies to cel/nfsd-testing (78c334bc5ae6) and each patch
> builds with W=1 with no warnings.
>
> Changes since v3:
> - Dropped "NFSD: Enable return of an updated stable_how to NFS
> clients" and "NFSD: add direct-mode WRITE settings that persist each
> WRITE" (v3 8/9 and 9/9). The rest of the series does not depend on
> them.
> - New 1/10, NFSD: cut a misaligned direct-mode WRITE at page
> boundaries (Cc: stable). v3 cut the direct middle where the DIO
> offset alignment falls, which is a page boundary only when that
> alignment is the page size.
> - Rebased onto the current cel/nfsd-testing. 5/10's size check moved
> into nfsd_direct_read_ok().
> - Descriptions, comments and nfsd-io-modes.rst corrected throughout,
> among them: 2/10 says what can still make XFS re-issue the middle
> buffered once 1/10 keeps it off the pages the buffered ends share;
> 3/10 says why a WRITE misaligned in memory is not split; 4/10 no
> longer misstates what the split threshold decides.
> - 3/10: the threshold is direct_misaligned_min_bytes (default 8192)
> instead of direct_misaligned_num_pages (default 2). On 4 KiB pages
> the default is unchanged. The debugfs file is renamed with its unit,
> so a value written to the old name is not read as bytes.
> - 6/10: a WRITE issued whole as O_DIRECT keeps IOCB_DSYNC; v3 turned a
> DATA_SYNC overwrite into a plain write plus an fsync.
> - New 7/10, mm: add folio_mark_dropbehind(). v3 marked an already
> dirty folio dropbehind directly; the clean that follows subtracts it
> from WB_DONTCACHE_DIRTY, which never counted it, so the counter
> drifted negative and stopped the DONTCACHE writeback kick for the
> whole bdi_writeback.
> - New 8/10, mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats. Reports
> the counter 7/10 keeps balanced; Fixes: Jeff's commit that added it.
> - 9/10: claims a page only where a buffered segment covers it partly, at
> either end of any segment, so a multi-page start or end segment and a
> sub-page WRITE are handled; claims for a split WRITE at any DIO offset
> alignment, since 1/10 keeps the direct middle out of the claimed
> pages; does not claim when the mapping's minimum folio is larger than
> a page; allocates with __GFP_WRITE; does not mark a page a short
> write did not reach; a FILE_SYNC or DATA_SYNC WRITE marks its shared
> pages after its own fsync, when they are clean, so they are not
> counted as DONTCACHE dirty and do not wake the writeback kick. The
> description no longer says marking must not be accounted or that the
> kick targets marked folios.
> - 10/10: the DONTCACHE state is its own tracepoint argument instead of a
> bit ORed into the disposition (nsegs is dropped; it follows from the
> segment sizes); the redundant #include "vfs.h" is gone;
> NFSD_WRITE_DIO_NO_ALIGN is renamed NFSD_WRITE_DIO_NO_ALIGNMENT to
> match its string; mem_offset is 0 for a WRITE with no payload.
>
> v3: https://lore.kernel.org/linux-nfs/20261001045502.48381-1-snitzer@kernel.org/
> v2: https://lore.kernel.org/linux-nfs/20260929231329.22018-1-snitzer@kernel.org/
> v1: https://lore.kernel.org/linux-nfs/20260929173423.16149-1-snitzer@kernel.org/
>
> Thanks,
> Mike
>
> Jonathan Flynn (1):
> NFSD: add direct_misaligned_dontcache debugfs knob
>
> Mike Snitzer (9):
> NFSD: cut a misaligned direct-mode WRITE at page boundaries
> NFSD: mark the direct middle of a split WRITE IOCB_DONTCACHE as well
> NFSD: only split a direct-mode WRITE for a worthwhile direct middle
> NFSD: do not use direct I/O for a READ smaller than its alignment
> NFSD: persist a synchronous direct-mode WRITE once, after all of its segments
> mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it
> mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats
> NFSD: keep boundary page of a split direct-mode WRITE until both writers complete
> NFSD: add tracing for how direct-mode READ and WRITE are serviced
>
> .../filesystems/nfs/nfsd-io-modes.rst | 93 +++++-
> fs/nfsd/debugfs.c | 25 ++
> fs/nfsd/nfsd.h | 2 +
> fs/nfsd/trace.h | 77 +++++
> fs/nfsd/vfs.c | 265 +++++++++++++++---
> fs/nfsd/vfs.h | 9 +
> include/linux/pagemap.h | 1 +
> mm/backing-dev.c | 11 +
> mm/filemap.c | 31 ++
> 9 files changed, 473 insertions(+), 41 deletions(-)
>
>
> base-commit: 78c334bc5ae61d23ca1cc2e43079c620f1d40866
I think this all looks good and should be good optimizations.
I take back what I said earlier about splitting the mm: patches out --
you do need the new helper for the later patches in the series. It's
fairly self-contained though so maybe it's not a big deal to carry #7
in the nfsd tree. Note that sashiko has some comments about a potential
race there too. #8 seems like it should go via mm tree though.
The other thing we should consider is whether to make the new controls
you're adding to debugfs into netlink controls instead. I see no reason
not to do that and it'll save us from having to convert them later.
--
Jeff Layton <jlayton@kernel.org>
^ permalink raw reply [flat|nested] 17+ messages in thread* Re: [PATCH v4 0/10] NFSD: keep direct-mode I/O out of the page cache
2026-10-15 23:32 [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Mike Snitzer
` (10 preceding siblings ...)
2026-10-16 11:18 ` [PATCH v4 00/10] NFSD: keep direct-mode I/O out of the page cache Jeff Layton
@ 2026-10-16 19:23 ` Chuck Lever
11 siblings, 0 replies; 17+ messages in thread
From: Chuck Lever @ 2026-10-16 19:23 UTC (permalink / raw)
To: Mike Snitzer
Cc: Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo,
Tom Talpey, Christoph Hellwig, Jens Axboe, Matthew Wilcox,
linux-mm
On Thu, 15 Oct 2026 19:32:59 -0400, Mike Snitzer wrote:
> 9/10 calls folio_mark_dropbehind() to keep the page two misaligned
> WRITEs share until both have written it, with no state in NFSD: 704
> device reads for 42895 WRITEs where there were 45144 for 45664
> (47008-byte records, 32 interleaved writers, 4 KiB pages), and on a
> four-server pNFS flexfiles rig 0.003 device reads per record instead of
> 0.014-0.015, about 2.9 GiB of page-cache growth instead of 11 GiB, and
> 2.4% to 4.9% more write throughput.
The direct-mode WRITE path started as one decision: aligned, go
direct; otherwise split and go buffered for the ends. This series
adds a page-boundary cut rule that depends on the relation between
offset_align and PAGE_SIZE, a minimum middle size, a knob for
whether the ends are DONTCACHE, a floor on READ size, a rule for
when the sync is issued, and then the shared-page pairing in 9/10.
Each is small and each has a measurement behind it. Taken together
they make it hard to say, for a given WRITE, what I/O NFSD will
issue and what the page cache will look like afterward, and that is
the property we need to hold onto when a corruption or a performance
report comes in. I'm not yet convinced that 1/10 through 6/10 buy
enough to give that up.
9/10 gives NFSD a semantic that DONTCACHE does not have: drop this
page after the second writer, not the first. It builds that out of
page-cache presence, with an unmarked folio standing in for "a
partner is coming" and -EEXIST standing in for "the partner has
been here".
Neither layer actually knows either of those things with certainty.
mm cannot know another WRITE is on its way, and NFSD can only guess:
a page left by a READ, by readahead, or by an earlier claim whose
partner never arrived makes the first writer look like the second,
and the description already lists the cases where a claimed page
stays cached until reclaim because nobody comes to complete it.
The 6000-byte numbers in 9/10 show the shared page is not a splitting
problem. Those records are never split, and they still cost a read
per record when the ends are DONTCACHE, because DONTCACHE drops the
page after the first writer's writeback. So the choice is really
between two things we already have: issue the ends and unsplit
WRITEs DONTCACHE and pay a read-modify-write per boundary, or issue
them as ordinary buffered I/O and let reclaim take the pages.
If ordinary buffered ends turn out to cost throughput with flexfiles
(or any NFSv3 I/O) other than just occupying memory, that is a
reclaim or DONTCACHE result worth bringing to linux-mm on its own,
and the right fix would be a DONTCACHE semantic that mm is willing
to define, not special state in NFSD.
Consider splitting this into two series (which I already proposed
for merge process-related reasons). 1-6 need careful scrutiny to
understand the trade-offs between scalability gains and
predictability. 7-9 need to be explored as an issue with DONTCACHE
that should be resolved below NFSD, not within it.
I'll study 1-6 more.
--
Chuck Lever (Come to NFS bake-a-thon! https://nfsv4bat.org)
^ permalink raw reply [flat|nested] 17+ messages in thread