* [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages
@ 2026-11-03 21:17 Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 1/7] mm: move clearing the uncached flag of a found folio into a helper Mike Snitzer
` (7 more replies)
0 siblings, 8 replies; 12+ messages in thread
From: Mike Snitzer @ 2026-11-03 21:17 UTC (permalink / raw)
To: Matthew Wilcox, linux-mm, linux-fsdevel
Cc: Chuck Lever, Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey, Christoph Hellwig, Jens Axboe,
Jonathan Corbet
Hi,
An uncached (RWF_DONTCACHE) buffered write drops each folio it writes
once the folio is written back. A folio that the write covers only
partly is shared with the neighbouring write. If it is dropped before
the neighbour writes its part, the neighbour reads it back from disk
first. 32 threads writing interleaved records to one XFS file, 4 KiB
pages, before and after 5/7:
record pwritev2() flags 4 KiB device MiB/s
bytes reads per write
before after before after
47008 RWF_DONTCACHE 0.97 0.0005 831 2281
47008 RWF_DONTCACHE|RWF_DSYNC 0.99 0.0005 601 1227
6000 RWF_DONTCACHE 0.97 0.0001 121 488
6000 RWF_DONTCACHE|RWF_DSYNC 0.99 0.0002 90 206
The test program and script: https://lore.kernel.org/linux-nfs/atqJBS8MlYfeUU-t@kernel.org/
- 1/7 moves code into a helper, with no functional change.
- 2/7 fixes 88d6f128d06d ("mm: track DONTCACHE dirty pages per
bdi_writeback"), in v7.3: WB_DONTCACHE_DIRTY could drift.
- 3/7 reports WB_DONTCACHE_DIRTY in the bdi debugfs stats.
- 4/7 adds folio_mark_dropbehind() for a folio that is already dirty.
- 5/7 keeps a partly covered folio until the write that completes it.
- 6/7 lets a writer that syncs its own write complete those folios
itself, after the sync.
- 7/7 makes NFSD do that for the misaligned ends of a direct-mode WRITE.
Merging: Chuck suggested taking the series through the mm tree. 1/7 to
6/7 apply cleanly to each of these mm branches:
mm-unstable 0ea6dc8c4a21 ("fs/proc/task_mmu: fix lost maps entries above 2^63")
mm-new 74d73ffcffe0 ("riscv: mm: fix concurrency in mark_new_valid_map()")
mm-stable 6628e7d4c6b2 ("Linux 7.4-rc1")
7/7 needs the NFSD direct-mode changes that are in cel/nfsd-testing.
The whole series applies to cel/nfsd-testing at 693afd3ce9ae ("NFSD:
Defer the PUTFH export security flavor check"), where each patch builds
with W=1 with no warnings. So 7/7 could follow in the nfsd tree once
the mm part is merged.
Changes since v9, from Christoph's review:
- v9 1/6 is now two patches: 1/7 only moves the code, and 2/7 changes
it.
- Every commit message is rewritten to state the problem, the fix and
the result, without the earlier detail.
- The long code comments are shorter.
- Rebased on the current cel/nfsd-testing, which now has b16607436d31
("NFSD: cut a misaligned direct-mode WRITE at folio boundaries").
- Jeff's Reviewed-by of v9 is carried on every patch but 1/7, which is
new, because the code changed only in comments and in the split of v9
1/6.
The code is the same as in v9 apart from the comments, so the v9
results hold. Patches 2, 4, 5, 6 and 7 carry notes below the ---
line: questions from earlier reviews, and the code that answers each.
v9: https://lore.kernel.org/linux-nfs/20261024221152.6965-1-snitzer@kernel.org/
v8: https://lore.kernel.org/linux-nfs/20261023024548.71123-1-snitzer@kernel.org/
v7: https://lore.kernel.org/linux-nfs/20261020220346.28020-1-snitzer@kernel.org/
v6: https://lore.kernel.org/linux-nfs/atbfEbgMU6UGkn2X@kernel.org/
v5: https://lore.kernel.org/linux-nfs/20261019150725.1507-1-snitzer@kernel.org/
v4: https://lore.kernel.org/linux-nfs/20261015233309.42999-1-snitzer@kernel.org/
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
Mike Snitzer (7):
mm: move clearing the uncached flag of a found folio into a helper
mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there
mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats
mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it
mm, iomap: keep a partly covered page until an uncached write completes it
mm, iomap: let an uncached writer complete its shared pages itself
NFSD: drop a shared boundary page only once both WRITEs have written it
.../filesystems/nfs/nfsd-io-modes.rst | 17 ++
fs/iomap/buffered-io.c | 30 ++-
fs/iomap/trace.h | 3 +-
fs/nfsd/vfs.c | 106 ++++++++-
include/linux/fs.h | 5 +-
include/linux/iomap.h | 1 +
include/linux/pagemap.h | 11 +
mm/backing-dev.c | 6 +
mm/filemap.c | 208 ++++++++++++++++--
9 files changed, 356 insertions(+), 31 deletions(-)
base-commit: 693afd3ce9ae5cddb7225ec87975e6ef1443e502
--
2.52.0
^ permalink raw reply [flat|nested] 12+ messages in thread
* [PATCH v10 1/7] mm: move clearing the uncached flag of a found folio into a helper
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
@ 2026-11-03 21:17 ` Mike Snitzer
2026-11-04 9:17 ` Christoph Hellwig
2026-11-03 21:17 ` [PATCH v10 2/7] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there Mike Snitzer
` (6 subsequent siblings)
7 siblings, 1 reply; 12+ messages in thread
From: Mike Snitzer @ 2026-11-03 21:17 UTC (permalink / raw)
To: Matthew Wilcox, linux-mm, linux-fsdevel
Cc: Chuck Lever, Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey, Christoph Hellwig, Jens Axboe,
Jonathan Corbet
Move the code in __filemap_get_folio_mpol() that clears PG_dropbehind
on a folio found by a cached lookup, and takes the folio off
WB_DONTCACHE_DIRTY, into filemap_clear_dropbehind(). No functional
change. The next patch changes the helper.
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
mm/filemap.c | 33 ++++++++++++++++++++-------------
1 file changed, 20 insertions(+), 13 deletions(-)
diff --git a/mm/filemap.c b/mm/filemap.c
index b74bc1e5015c6..c75f49563b74b 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1945,6 +1945,24 @@ void *filemap_get_entry(struct address_space *mapping, pgoff_t index)
return folio;
}
+/* Clear the uncached flag of a folio that a cached lookup found. */
+static void filemap_clear_dropbehind(struct address_space *mapping,
+ struct folio *folio)
+{
+ if (!folio_test_clear_dropbehind(folio))
+ return;
+ if (folio_test_dirty(folio) && mapping_can_writeback(mapping)) {
+ struct inode *inode = mapping->host;
+ struct bdi_writeback *wb;
+ struct wb_lock_cookie cookie = {};
+ long nr = folio_nr_pages(folio);
+
+ wb = unlocked_inode_to_wb_begin(inode, &cookie);
+ wb_stat_mod(wb, WB_DONTCACHE_DIRTY, -nr);
+ unlocked_inode_to_wb_end(inode, &cookie);
+ }
+}
+
/**
* __filemap_get_folio_mpol - Find and get a reference to a folio.
* @mapping: The address_space to search.
@@ -2076,19 +2094,8 @@ struct folio *__filemap_get_folio_mpol(struct address_space *mapping,
if (!folio)
return ERR_PTR(-ENOENT);
/* not an uncached lookup, clear uncached if set */
- if (!(fgp_flags & FGP_DONTCACHE) && folio_test_clear_dropbehind(folio)) {
- if (folio_test_dirty(folio) &&
- mapping_can_writeback(mapping)) {
- struct inode *inode = mapping->host;
- struct bdi_writeback *wb;
- struct wb_lock_cookie cookie = {};
- long nr = folio_nr_pages(folio);
-
- wb = unlocked_inode_to_wb_begin(inode, &cookie);
- wb_stat_mod(wb, WB_DONTCACHE_DIRTY, -nr);
- unlocked_inode_to_wb_end(inode, &cookie);
- }
- }
+ if (!(fgp_flags & FGP_DONTCACHE))
+ filemap_clear_dropbehind(mapping, folio);
return folio;
}
EXPORT_SYMBOL(__filemap_get_folio_mpol);
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v10 2/7] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 1/7] mm: move clearing the uncached flag of a found folio into a helper Mike Snitzer
@ 2026-11-03 21:17 ` Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 3/7] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
` (5 subsequent siblings)
7 siblings, 0 replies; 12+ messages in thread
From: Mike Snitzer @ 2026-11-03 21:17 UTC (permalink / raw)
To: Matthew Wilcox, linux-mm, linux-fsdevel
Cc: Chuck Lever, Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey, Christoph Hellwig, Jens Axboe,
Jonathan Corbet
A cached lookup that finds a dropbehind folio clears the flag and
subtracts the folio from WB_DONTCACHE_DIRTY if PG_dirty is set. That
test races with the dirtying of the folio. filemap_dirty_folio() sets
PG_dirty before folio_account_dirtied() counts the folio, so a lookup in
between subtracts a folio that was never added. The lookup also does
not hold the folio lock. Every other path that subtracts a dirty folio
from the counter holds it, so the lookup can subtract the folio a second
time. Either way the counter drifts, and once it is negative the
DONTCACHE writeback kick writes nothing.
Fix this by having filemap_clear_dropbehind() clear the uncached flag of
a folio that a cached lookup found, and take the folio off
WB_DONTCACHE_DIRTY if it is counted there. With the folio locked and
the i_pages lock held, the folio is counted if both PG_dirty and the
dirty tag are set. A folio that another task has locked keeps the
flag.
Fixes: 88d6f128d06d ("mm: track DONTCACHE dirty pages per bdi_writeback")
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on questions from earlier reviews, with the code that answers each:
- Stale dirty tag. __folio_cancel_dirty() clears PG_dirty and takes
the folio off the counter in folio_account_cleaned(), but leaves
PAGECACHE_TAG_DIRTY set. filemap_clear_dropbehind() subtracts the
folio only when folio_test_dirty() and the tag are both true, so a
stale tag does not subtract it a second time.
- Locks. filemap_clear_dropbehind() holds the folio lock (FGP_LOCK,
or folio_trylock() and a check of folio->mapping) and the i_pages
lock. __folio_mark_dirty() counts the folio in
folio_account_dirtied() and sets the tag under the i_pages lock.
The paths that subtract a dirty folio (__folio_cancel_dirty(),
folio_clear_dirty_for_io(), filemap_unaccount_folio(), the folio
split) hold the folio lock. inode_do_switch_wbs() moves the count
under the i_pages lock. inode_to_wb() accepts the i_pages lock in
its lockdep check.
- Trylock failure. The folio keeps PG_dropbehind, as if the lookup
did not occur. The count does not change.
- Known window. A folio can be clean with a stale tag (for example
after iomap_writeback_handle_eof() skips a folio beyond EOF), and
an unlocked dirtier can set PG_dirty before it takes the i_pages
lock. A lookup between the two sees both bits and subtracts a
folio that is not counted yet. The two states look the same in
these bits. Only the counter, and so the size of a DONTCACHE
writeback kick, is affected; data and page state are not.
mm/filemap.c | 42 ++++++++++++++++++++++++++++--------------
1 file changed, 28 insertions(+), 14 deletions(-)
diff --git a/mm/filemap.c b/mm/filemap.c
index c75f49563b74b..3a13289c07dc4 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1945,22 +1945,36 @@ void *filemap_get_entry(struct address_space *mapping, pgoff_t index)
return folio;
}
-/* Clear the uncached flag of a folio that a cached lookup found. */
+/*
+ * Clear the uncached flag of a folio that a cached lookup found, and take the
+ * folio off WB_DONTCACHE_DIRTY if it is counted there. With the folio locked
+ * and the i_pages lock held, the folio is counted if both PG_dirty and the
+ * dirty tag are set. A folio that another task has locked keeps the flag.
+ */
static void filemap_clear_dropbehind(struct address_space *mapping,
- struct folio *folio)
+ struct folio *folio, bool locked)
{
- if (!folio_test_clear_dropbehind(folio))
- return;
- if (folio_test_dirty(folio) && mapping_can_writeback(mapping)) {
- struct inode *inode = mapping->host;
- struct bdi_writeback *wb;
- struct wb_lock_cookie cookie = {};
- long nr = folio_nr_pages(folio);
+ unsigned long flags;
- wb = unlocked_inode_to_wb_begin(inode, &cookie);
- wb_stat_mod(wb, WB_DONTCACHE_DIRTY, -nr);
- unlocked_inode_to_wb_end(inode, &cookie);
+ if (!locked) {
+ if (!folio_trylock(folio))
+ return;
+ if (unlikely(folio->mapping != mapping))
+ goto out;
}
+ if (!mapping_can_writeback(mapping)) {
+ folio_clear_dropbehind(folio);
+ goto out;
+ }
+ xa_lock_irqsave(&mapping->i_pages, flags);
+ if (folio_test_clear_dropbehind(folio) && folio_test_dirty(folio) &&
+ xa_get_mark(&mapping->i_pages, folio->index, PAGECACHE_TAG_DIRTY))
+ wb_stat_mod(inode_to_wb(mapping->host), WB_DONTCACHE_DIRTY,
+ -folio_nr_pages(folio));
+ xa_unlock_irqrestore(&mapping->i_pages, flags);
+out:
+ if (!locked)
+ folio_unlock(folio);
}
/**
@@ -2094,8 +2108,8 @@ struct folio *__filemap_get_folio_mpol(struct address_space *mapping,
if (!folio)
return ERR_PTR(-ENOENT);
/* not an uncached lookup, clear uncached if set */
- if (!(fgp_flags & FGP_DONTCACHE))
- filemap_clear_dropbehind(mapping, folio);
+ if (!(fgp_flags & FGP_DONTCACHE) && folio_test_dropbehind(folio))
+ filemap_clear_dropbehind(mapping, folio, fgp_flags & FGP_LOCK);
return folio;
}
EXPORT_SYMBOL(__filemap_get_folio_mpol);
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v10 3/7] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 1/7] mm: move clearing the uncached flag of a found folio into a helper Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 2/7] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there Mike Snitzer
@ 2026-11-03 21:17 ` Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 4/7] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
` (4 subsequent siblings)
7 siblings, 0 replies; 12+ messages in thread
From: Mike Snitzer @ 2026-11-03 21:17 UTC (permalink / raw)
To: Matthew Wilcox, linux-mm, linux-fsdevel
Cc: Chuck Lever, Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey, Christoph Hellwig, Jens Axboe,
Jonathan Corbet
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")
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
mm/backing-dev.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/mm/backing-dev.c b/mm/backing-dev.c
index fad1495d3b200..a09c21759fb9f 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,7 @@ 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);
+ 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 +127,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 +142,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 +167,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 +184,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] 12+ messages in thread
* [PATCH v10 4/7] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
` (2 preceding siblings ...)
2026-11-03 21:17 ` [PATCH v10 3/7] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
@ 2026-11-03 21:17 ` Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 5/7] mm, iomap: keep a partly covered page until an uncached write completes it Mike Snitzer
` (3 subsequent siblings)
7 siblings, 0 replies; 12+ messages in thread
From: Mike Snitzer @ 2026-11-03 21:17 UTC (permalink / raw)
To: Matthew Wilcox, linux-mm, linux-fsdevel
Cc: Chuck Lever, Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey, Christoph Hellwig, Jens Axboe,
Jonathan Corbet
WB_DONTCACHE_DIRTY counts the dirty folios that are marked dropbehind.
When a dropbehind folio is dirtied, folio_account_dirtied() adds it to
the counter. When the folio is cleaned, folio_clear_dirty_for_io() or
folio_account_cleaned() subtracts it again. This balances only if the
folio is marked before it is dirtied, as it is for the folios that an
RWF_DONTCACHE write creates.
To prepare for NFSD to mark a folio dropbehind once both writes that
share it have written their parts, handle a folio that is already dirty
when it is marked. folio_set_dropbehind() only sets the flag. With it,
the clean would subtract the folio from WB_DONTCACHE_DIRTY although it
was never added, and the counter would go negative.
Add folio_mark_dropbehind(), the inverse of filemap_clear_dropbehind().
With the folio locked, it sets PG_dropbehind. Under the i_pages lock,
it adds the folio to WB_DONTCACHE_DIRTY if both PG_dirty and the dirty
tag are set. A dirtier that has set PG_dirty but not yet counted the
folio finds the flag set and counts the folio itself.
The name follows folio_set_dirty() and folio_mark_dirty(): the set
variant only sets the flag, the mark variant also accounts for it.
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on questions from earlier reviews, with the code that answers each:
- Counting rule. The same rule as filemap_clear_dropbehind() in
2/7: folio_mark_dropbehind() counts the folio only when
folio_test_dirty() and PAGECACHE_TAG_DIRTY are both true. The
test runs under the folio lock (VM_WARN_ON_ONCE_FOLIO checks it)
and the i_pages lock. A stale tag on a clean folio adds no count.
- Double count. A folio that has PG_dropbehind returns at the first
test, before it changes the count.
- No writeback. A mapping without writeback gets only the flag.
folio_account_dirtied() does not count such a folio either
(mapping_can_writeback()).
- Known window. As in 2/7: a stale tag together with an unlocked
dirtier that has set PG_dirty but not yet taken the i_pages lock
can make this count a folio that the dirtier then counts again.
Only the counter is affected.
include/linux/pagemap.h | 1 +
mm/filemap.c | 32 ++++++++++++++++++++++++++++++++
2 files changed, 33 insertions(+)
diff --git a/include/linux/pagemap.h b/include/linux/pagemap.h
index 73af18a373670..3f5295d6fd73b 100644
--- a/include/linux/pagemap.h
+++ b/include/linux/pagemap.h
@@ -1306,6 +1306,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 3a13289c07dc4..5134227ba3a23 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1643,6 +1643,38 @@ 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. If the folio
+ * is already counted as dirty, which with the i_pages lock held means both
+ * PG_dirty and the dirty tag are set, add it to WB_DONTCACHE_DIRTY so that
+ * the clean that subtracts it leaves the counter balanced.
+ */
+void folio_mark_dropbehind(struct folio *folio)
+{
+ struct address_space *mapping = folio->mapping;
+ unsigned long flags;
+
+ VM_WARN_ON_ONCE_FOLIO(!folio_test_locked(folio), folio);
+
+ if (folio_test_dropbehind(folio))
+ return;
+ if (!mapping || !mapping_can_writeback(mapping)) {
+ folio_set_dropbehind(folio);
+ return;
+ }
+ xa_lock_irqsave(&mapping->i_pages, flags);
+ folio_set_dropbehind(folio);
+ if (folio_test_dirty(folio) &&
+ xa_get_mark(&mapping->i_pages, folio->index, PAGECACHE_TAG_DIRTY))
+ wb_stat_mod(inode_to_wb(mapping->host), WB_DONTCACHE_DIRTY,
+ folio_nr_pages(folio));
+ xa_unlock_irqrestore(&mapping->i_pages, flags);
+}
+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] 12+ messages in thread
* [PATCH v10 5/7] mm, iomap: keep a partly covered page until an uncached write completes it
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
` (3 preceding siblings ...)
2026-11-03 21:17 ` [PATCH v10 4/7] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
@ 2026-11-03 21:17 ` Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 6/7] mm, iomap: let an uncached writer complete its shared pages itself Mike Snitzer
` (2 subsequent siblings)
7 siblings, 0 replies; 12+ messages in thread
From: Mike Snitzer @ 2026-11-03 21:17 UTC (permalink / raw)
To: Matthew Wilcox, linux-mm, linux-fsdevel
Cc: Chuck Lever, Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey, Christoph Hellwig, Jens Axboe,
Jonathan Corbet
An RWF_DONTCACHE buffered write marks every folio it creates dropbehind,
so writeback drops the folio as soon as the write's data is on disk. A
folio that the write covers only partly is usually shared with the
neighbouring write. If writeback drops it before the neighbour writes
its part, the neighbour has to read it back from disk first: one read
per misaligned write.
Add FGP_DONTCACHE_EDGE for the lookup of such a folio. iomap sets it
when a write starts or ends inside the smallest folio the mapping
allows. A folio created for an edge is not marked dropbehind. A folio
found uptodate already holds the neighbour's data, so this write
completes it. Mark it with folio_mark_dropbehind() under the folio
lock, and the writeback that cleans both writes' data drops it. With
blocks smaller than the folio, the edge is not uptodate yet, so
folio_dontcache_edge_written() also accepts a folio whose blocks outside
this write are uptodate. A larger folio, such as one from readahead, is
left alone.
An edge whose neighbour never writes, such as the last page of a file,
stays cached until reclaim.
With this commit, using 32 threads writing interleaved 47008-byte
records to one XFS file with RWF_DONTCACHE: Device reads per write fell
from 0.97 to 0.0005, and throughput rose from 831 to 2281 MiB/s.
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on questions from earlier reviews, with the code that answers each:
- Blocks smaller than the folio. folio_dontcache_edge_written()
calls ->is_partially_uptodate() only for the blocks this write does
not cover in full. iomap_get_folio() returns the folio locked
(FGP_WRITEBEGIN includes FGP_LOCK), so the per-block state is read
under the folio lock. iomap_get_folio() tests IS_ERR() before it
uses the folio.
- Larger folios. filemap_dontcache_edge_unit() accepts only a folio
of mapping_min_folio_nrpages(). iomap_get_folio() limits len to
the unit, so fgf_set_order() asks for no more than one unit at an
edge.
- Lock for the mark. __filemap_get_folio_mpol() marks an edge only
with FGP_LOCK set, so folio_mark_dropbehind() always has the folio
locked.
- Stale dirty tag. folio_mark_dropbehind() tests PG_dirty and the
tag (4/7), so a clean folio with a stale tag is not counted.
- Mark before iomap validation. The lookup marks the folio before
iomap validates the iomap. If the iomap is stale and a fatal
signal stops the retry, the folio keeps a mark that this write did
not earn. Writeback still cleans the folio before it is dropped,
so the cost is one read on the application's retry, never data.
The lookup cannot undo the mark: a folio an earlier write marked
returns early, so the lookup cannot tell whether it set the mark.
- A partly covered folio that another task cached and that is
uptodate is now dropped after this write's writeback. Before, an
uncached write never dropped a folio it did not create.
- Only iomap file systems set FGP_DONTCACHE_EDGE.
write_begin_get_folio() users, such as ext4 and the NFS client, do
not change. With blocks smaller than a page, a write that crosses
an extent boundary inside a page looks the page up as two partly
covered halves, so that page stays cached until reclaim.
fs/iomap/buffered-io.c | 26 +++++++++++++++---
include/linux/pagemap.h | 4 +++
mm/filemap.c | 59 ++++++++++++++++++++++++++++++++++++++++-
3 files changed, 85 insertions(+), 4 deletions(-)
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index d6e2f1ded5ca6..919e4d6c3c972 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -798,16 +798,36 @@ EXPORT_SYMBOL_GPL(iomap_is_partially_uptodate);
*/
struct folio *iomap_get_folio(struct iomap_iter *iter, loff_t pos, size_t len)
{
+ struct address_space *mapping = iter->inode->i_mapping;
fgf_t fgp = FGP_WRITEBEGIN;
+ struct folio *folio;
if (iter->flags & IOMAP_NOWAIT)
fgp |= FGP_NOWAIT;
- if (iter->flags & IOMAP_DONTCACHE)
+ if (iter->flags & IOMAP_DONTCACHE) {
+ size_t unit = mapping_min_folio_nrbytes(mapping);
+ size_t off = pos & (unit - 1);
+
fgp |= FGP_DONTCACHE;
+ /* A partly covered unit is shared with the neighbour. */
+ if (off || len < unit) {
+ fgp |= FGP_DONTCACHE_EDGE;
+ len = min_t(size_t, len, unit - off);
+ }
+ }
fgp |= fgf_set_order(len);
- return __filemap_get_folio(iter->inode->i_mapping, pos >> PAGE_SHIFT,
- fgp, mapping_gfp_mask(iter->inode->i_mapping));
+ folio = __filemap_get_folio(mapping, pos >> PAGE_SHIFT, fgp,
+ mapping_gfp_mask(mapping));
+ /*
+ * With blocks smaller than the folio, an edge is not uptodate
+ * until this write completes it.
+ */
+ if ((fgp & FGP_DONTCACHE_EDGE) &&
+ !IS_ERR(folio) && !folio_test_uptodate(folio) &&
+ folio_dontcache_edge_written(folio, pos, len))
+ folio_mark_dropbehind(folio);
+ return folio;
}
EXPORT_SYMBOL_GPL(iomap_get_folio);
diff --git a/include/linux/pagemap.h b/include/linux/pagemap.h
index 3f5295d6fd73b..7a78f645de9f6 100644
--- a/include/linux/pagemap.h
+++ b/include/linux/pagemap.h
@@ -698,6 +698,8 @@ pgoff_t page_cache_prev_miss(struct address_space *mapping,
* * %FGP_NOWAIT - Don't block on the folio lock.
* * %FGP_STABLE - Wait for the folio to be stable (finished writeback)
* * %FGP_DONTCACHE - Uncached buffered IO
+ * * %FGP_DONTCACHE_EDGE - With %FGP_DONTCACHE: the write covers the folio
+ * only partly. A new folio is not marked dropbehind, one found uptodate is.
* * %FGP_WRITEBEGIN - The flags to use in a filesystem write_begin()
* implementation.
*/
@@ -712,6 +714,7 @@ typedef unsigned int __bitwise fgf_t;
#define FGP_FOR_MMAP ((__force fgf_t)0x00000040)
#define FGP_STABLE ((__force fgf_t)0x00000080)
#define FGP_DONTCACHE ((__force fgf_t)0x00000100)
+#define FGP_DONTCACHE_EDGE ((__force fgf_t)0x00000200)
#define FGF_GET_ORDER(fgf) (((__force unsigned)fgf) >> 26) /* top 6 bits */
#define FGP_WRITEBEGIN (FGP_LOCK | FGP_WRITE | FGP_CREAT | FGP_STABLE)
@@ -1307,6 +1310,7 @@ 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);
+bool folio_dontcache_edge_written(struct folio *folio, loff_t pos, size_t len);
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 5134227ba3a23..ec52e4c960849 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1675,6 +1675,52 @@ void folio_mark_dropbehind(struct folio *folio)
}
EXPORT_SYMBOL_GPL(folio_mark_dropbehind);
+/* The unit two writes share is the smallest folio the mapping allows. */
+static bool filemap_dontcache_edge_unit(struct address_space *mapping,
+ struct folio *folio)
+{
+ return folio_nr_pages(folio) == mapping_min_folio_nrpages(mapping);
+}
+
+/**
+ * folio_dontcache_edge_written - has the neighbouring write written its part?
+ * @folio: locked edge folio
+ * @pos: file offset of the first byte this write covers in @folio
+ * @len: bytes this write covers in @folio
+ *
+ * Return: true if @folio is an edge unit that is uptodate, or whose blocks
+ * that this write does not cover in full are uptodate.
+ */
+bool folio_dontcache_edge_written(struct folio *folio, loff_t pos, size_t len)
+{
+ struct address_space *mapping = folio->mapping;
+ size_t size = folio_size(folio);
+ size_t from, to, bs;
+
+ VM_WARN_ON_ONCE_FOLIO(!folio_test_locked(folio), folio);
+
+ if (!mapping || !filemap_dontcache_edge_unit(mapping, folio))
+ return false;
+ if (folio_test_uptodate(folio))
+ return true;
+ if (!mapping->a_ops->is_partially_uptodate)
+ return false;
+
+ /* The blocks this write covers in full are [from, to). */
+ bs = i_blocksize(mapping->host);
+ from = offset_in_folio(folio, pos);
+ to = min(from + len, size);
+ from = round_up(from, bs);
+ to = round_down(to, bs);
+ if (from && !mapping->a_ops->is_partially_uptodate(folio, 0, from))
+ return false;
+ if (to < size &&
+ !mapping->a_ops->is_partially_uptodate(folio, to, size - to))
+ return false;
+ return true;
+}
+EXPORT_SYMBOL_GPL(folio_dontcache_edge_written);
+
/**
* folio_end_writeback_no_dropbehind - End writeback against a folio.
* @folio: The folio.
@@ -2067,6 +2113,15 @@ struct folio *__filemap_get_folio_mpol(struct address_space *mapping,
if (fgp_flags & FGP_STABLE)
folio_wait_stable(folio);
+
+ /*
+ * An uptodate edge holds the neighbour's part, so this write
+ * completes it.
+ */
+ if ((fgp_flags & FGP_DONTCACHE) && (fgp_flags & FGP_DONTCACHE_EDGE) &&
+ (fgp_flags & FGP_LOCK) && folio_test_uptodate(folio) &&
+ filemap_dontcache_edge_unit(mapping, folio))
+ folio_mark_dropbehind(folio);
no_page:
if (!folio && (fgp_flags & FGP_CREAT)) {
unsigned int min_order = mapping_min_folio_order(mapping);
@@ -2104,7 +2159,9 @@ struct folio *__filemap_get_folio_mpol(struct address_space *mapping,
/* Init accessed so avoid atomic mark_page_accessed later */
if (fgp_flags & FGP_ACCESSED)
__folio_set_referenced(folio);
- if (fgp_flags & FGP_DONTCACHE)
+ /* A new edge waits for the neighbouring write. */
+ if ((fgp_flags & FGP_DONTCACHE) &&
+ !(fgp_flags & FGP_DONTCACHE_EDGE))
__folio_set_dropbehind(folio);
err = filemap_add_folio(mapping, folio, index, gfp);
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v10 6/7] mm, iomap: let an uncached writer complete its shared pages itself
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
` (4 preceding siblings ...)
2026-11-03 21:17 ` [PATCH v10 5/7] mm, iomap: keep a partly covered page until an uncached write completes it Mike Snitzer
@ 2026-11-03 21:17 ` Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 7/7] NFSD: drop a shared boundary page only once both WRITEs have written it Mike Snitzer
2026-11-04 9:17 ` [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Christoph Hellwig
7 siblings, 0 replies; 12+ messages in thread
From: Mike Snitzer @ 2026-11-03 21:17 UTC (permalink / raw)
To: Matthew Wilcox, linux-mm, linux-fsdevel
Cc: Chuck Lever, Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey, Christoph Hellwig, Jens Axboe,
Jonathan Corbet
With the previous commit, the write that completes a shared folio marks
it while it is still dirty. The folio then counts as DONTCACHE dirty
and wakes the DONTCACHE writeback kick. Some callers sync once, after
writing several segments, as NFSD does for a stable WRITE. Such a
caller wakes the kick once per segment, for pages that its own sync is
about to write.
Add IOCB_DONTCACHE_DEFER (with IOMAP_DONTCACHE_DEFER and
FGP_DONTCACHE_DEFER). The write leaves the folios it covers only partly
unmarked, and the caller completes them itself. Called right before the
write, filemap_dontcache_edge_completes() tells the caller whether the
neighbouring write already wrote its part. Called once the write and
its sync are done, filemap_dontcache_edge_complete() waits for writeback
in flight. Then it marks a dirty folio and kicks writeback, drops a
clean unmapped folio at once, or marks a clean mapped one.
Two writes that arrive together may both find the folio absent. The
folio then stays cached until reclaim.
IOCB_DONTCACHE_DEFER enables NFSD, in a later commit, to avoid waking
the DONTCACHE writeback kick.
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on questions from earlier reviews, with the code that answers each:
- Writeback in flight. filemap_dontcache_edge_complete() calls
folio_wait_writeback() under the folio lock before it tests
PG_dirty.
- Writeback kick. The helper calls filemap_dontcache_kick_writeback()
after it releases the folio lock, and only if it marked a dirty
folio.
- WB_DONTCACHE_DIRTY for a clean folio. A clean folio that is not
mapped is not marked: folio_unmap_invalidate() drops it under the
folio lock. If a store dirties it first, folio_unmap_invalidate()
finds PG_dirty under the i_pages lock and keeps the folio, which
has no mark and so no count to lose. Only a dirty folio, or a
mapped clean one, is marked, with folio_mark_dropbehind() (4/7).
- Blocks smaller than the folio. filemap_dontcache_edge_completes()
gets the folio with FGP_LOCK and calls
folio_dontcache_edge_written() (5/7) with the range of this write.
FGP_DONTCACHE keeps the lookup from clearing a mark.
- Flag values. IOCB_DONTCACHE_DEFER (1 << 24) follows
IOCB_HAS_METADATA (1 << 23). IOMAP_DONTCACHE_DEFER (1 << 11)
follows IOMAP_DONTCACHE (1 << 10). FGP_DONTCACHE_DEFER (0x400) is
below the order bits (FGF_GET_ORDER() uses bits 26 to 31).
fs/iomap/buffered-io.c | 6 +++-
fs/iomap/trace.h | 3 +-
include/linux/fs.h | 5 ++-
include/linux/iomap.h | 1 +
include/linux/pagemap.h | 6 ++++
mm/filemap.c | 74 +++++++++++++++++++++++++++++++++++++++--
6 files changed, 90 insertions(+), 5 deletions(-)
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index 919e4d6c3c972..c47fa683aaa8a 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -812,6 +812,8 @@ struct folio *iomap_get_folio(struct iomap_iter *iter, loff_t pos, size_t len)
/* A partly covered unit is shared with the neighbour. */
if (off || len < unit) {
fgp |= FGP_DONTCACHE_EDGE;
+ if (iter->flags & IOMAP_DONTCACHE_DEFER)
+ fgp |= FGP_DONTCACHE_DEFER;
len = min_t(size_t, len, unit - off);
}
}
@@ -823,7 +825,7 @@ struct folio *iomap_get_folio(struct iomap_iter *iter, loff_t pos, size_t len)
* With blocks smaller than the folio, an edge is not uptodate
* until this write completes it.
*/
- if ((fgp & FGP_DONTCACHE_EDGE) &&
+ if ((fgp & FGP_DONTCACHE_EDGE) && !(fgp & FGP_DONTCACHE_DEFER) &&
!IS_ERR(folio) && !folio_test_uptodate(folio) &&
folio_dontcache_edge_written(folio, pos, len))
folio_mark_dropbehind(folio);
@@ -1333,6 +1335,8 @@ iomap_file_buffered_write(struct kiocb *iocb, struct iov_iter *i,
iter.flags |= IOMAP_NOWAIT;
if (iocb->ki_flags & IOCB_DONTCACHE)
iter.flags |= IOMAP_DONTCACHE;
+ if (iocb->ki_flags & IOCB_DONTCACHE_DEFER)
+ iter.flags |= IOMAP_DONTCACHE_DEFER;
while ((ret = iomap_iter(&iter, ops)) > 0)
iter.status = iomap_write_iter(&iter, i, write_ops);
diff --git a/fs/iomap/trace.h b/fs/iomap/trace.h
index e1ea8392cf47d..4f0343eeed1b4 100644
--- a/fs/iomap/trace.h
+++ b/fs/iomap/trace.h
@@ -104,7 +104,8 @@ DEFINE_RANGE_EVENT(iomap_zero_iter);
{ IOMAP_UNSHARE, "UNSHARE" }, \
{ IOMAP_DAX, "DAX" }, \
{ IOMAP_ATOMIC, "ATOMIC" }, \
- { IOMAP_DONTCACHE, "DONTCACHE" }
+ { IOMAP_DONTCACHE, "DONTCACHE" }, \
+ { IOMAP_DONTCACHE_DEFER, "DONTCACHE_DEFER" }
#define IOMAP_F_FLAGS_STRINGS \
{ IOMAP_F_NEW, "NEW" }, \
diff --git a/include/linux/fs.h b/include/linux/fs.h
index 99caef43ff3c4..19aaba0bf41f9 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -357,6 +357,8 @@ struct readahead_control;
/* kiocb is a read or write operation submitted by fs/aio.c. */
#define IOCB_AIO_RW (1 << 22)
#define IOCB_HAS_METADATA (1 << 23)
+/* With IOCB_DONTCACHE: see filemap_dontcache_edge_complete() */
+#define IOCB_DONTCACHE_DEFER (1 << 24)
/* for use in trace events */
#define TRACE_IOCB_STRINGS \
@@ -374,7 +376,8 @@ struct readahead_control;
{ IOCB_NOIO, "NOIO" }, \
{ IOCB_ALLOC_CACHE, "ALLOC_CACHE" }, \
{ IOCB_AIO_RW, "AIO_RW" }, \
- { IOCB_HAS_METADATA, "AIO_HAS_METADATA" }
+ { IOCB_HAS_METADATA, "AIO_HAS_METADATA" }, \
+ { IOCB_DONTCACHE_DEFER, "DONTCACHE_DEFER" }
struct kiocb {
struct file *ki_filp;
diff --git a/include/linux/iomap.h b/include/linux/iomap.h
index 5254430844b2c..fcef97e64a0bd 100644
--- a/include/linux/iomap.h
+++ b/include/linux/iomap.h
@@ -212,6 +212,7 @@ struct iomap_write_ops {
#endif /* CONFIG_FS_DAX */
#define IOMAP_ATOMIC (1 << 9) /* torn-write protection */
#define IOMAP_DONTCACHE (1 << 10)
+#define IOMAP_DONTCACHE_DEFER (1 << 11) /* see IOCB_DONTCACHE_DEFER */
/*
* Return the existing mapping at pos, or reserve space starting at pos for up
diff --git a/include/linux/pagemap.h b/include/linux/pagemap.h
index 7a78f645de9f6..a276d6e7279a3 100644
--- a/include/linux/pagemap.h
+++ b/include/linux/pagemap.h
@@ -700,6 +700,8 @@ pgoff_t page_cache_prev_miss(struct address_space *mapping,
* * %FGP_DONTCACHE - Uncached buffered IO
* * %FGP_DONTCACHE_EDGE - With %FGP_DONTCACHE: the write covers the folio
* only partly. A new folio is not marked dropbehind, one found uptodate is.
+ * * %FGP_DONTCACHE_DEFER - With %FGP_DONTCACHE_EDGE: a folio found uptodate is
+ * not marked either, the caller completes it.
* * %FGP_WRITEBEGIN - The flags to use in a filesystem write_begin()
* implementation.
*/
@@ -715,6 +717,7 @@ typedef unsigned int __bitwise fgf_t;
#define FGP_STABLE ((__force fgf_t)0x00000080)
#define FGP_DONTCACHE ((__force fgf_t)0x00000100)
#define FGP_DONTCACHE_EDGE ((__force fgf_t)0x00000200)
+#define FGP_DONTCACHE_DEFER ((__force fgf_t)0x00000400)
#define FGF_GET_ORDER(fgf) (((__force unsigned)fgf) >> 26) /* top 6 bits */
#define FGP_WRITEBEGIN (FGP_LOCK | FGP_WRITE | FGP_CREAT | FGP_STABLE)
@@ -1311,6 +1314,9 @@ void folio_end_writeback_no_dropbehind(struct folio *folio);
void folio_end_dropbehind(struct folio *folio);
void folio_mark_dropbehind(struct folio *folio);
bool folio_dontcache_edge_written(struct folio *folio, loff_t pos, size_t len);
+bool filemap_dontcache_edge_completes(struct address_space *mapping,
+ loff_t pos, size_t len);
+void filemap_dontcache_edge_complete(struct address_space *mapping, loff_t pos);
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 ec52e4c960849..d540df29870a9 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1721,6 +1721,76 @@ bool folio_dontcache_edge_written(struct folio *folio, loff_t pos, size_t len)
}
EXPORT_SYMBOL_GPL(folio_dontcache_edge_written);
+/**
+ * filemap_dontcache_edge_completes - will this write complete a shared folio?
+ * @mapping: the file's mapping
+ * @pos: file offset of the first byte the write covers in the folio
+ * @len: bytes the write covers in the folio
+ *
+ * For an IOCB_DONTCACHE_DEFER write: call right before the write.
+ *
+ * Return: true if the caller must call filemap_dontcache_edge_complete() once
+ * the write, and any sync of it, is done.
+ */
+bool filemap_dontcache_edge_completes(struct address_space *mapping,
+ loff_t pos, size_t len)
+{
+ struct folio *folio;
+ bool ret;
+
+ /* FGP_DONTCACHE keeps a mark, FGP_LOCK guards the block state. */
+ folio = __filemap_get_folio(mapping, pos >> PAGE_SHIFT,
+ FGP_LOCK | FGP_DONTCACHE, 0);
+ if (IS_ERR(folio))
+ return false;
+ ret = folio_dontcache_edge_written(folio, pos, len);
+ folio_unlock(folio);
+ folio_put(folio);
+ return ret;
+}
+EXPORT_SYMBOL_GPL(filemap_dontcache_edge_completes);
+
+/**
+ * filemap_dontcache_edge_complete - drop a folio both writes have written
+ * @mapping: the file's mapping
+ * @pos: any byte offset within the folio
+ *
+ * Call once the write, and any sync of it, is done. A clean folio is
+ * dropped now, a dirty or mapped one by the writeback that cleans it.
+ */
+void filemap_dontcache_edge_complete(struct address_space *mapping, loff_t pos)
+{
+ struct folio *folio;
+ bool kick = false;
+
+ folio = __filemap_get_folio(mapping, pos >> PAGE_SHIFT,
+ FGP_LOCK | FGP_DONTCACHE, 0);
+ if (IS_ERR(folio))
+ return;
+ if (!filemap_dontcache_edge_unit(mapping, folio))
+ goto out;
+ /* Writeback in flight was submitted unmarked and would not drop it. */
+ folio_wait_writeback(folio);
+ if (folio_test_dirty(folio)) {
+ folio_mark_dropbehind(folio);
+ kick = true;
+ } else if (folio_mapped(folio)) {
+ folio_mark_dropbehind(folio);
+ } else {
+ /*
+ * Drop it now, unmarked: a mark cleared outside the i_pages
+ * lock could miss a store that counts the folio.
+ */
+ folio_unmap_invalidate(mapping, folio, 0);
+ }
+out:
+ folio_unlock(folio);
+ folio_put(folio);
+ if (kick)
+ filemap_dontcache_kick_writeback(mapping);
+}
+EXPORT_SYMBOL_GPL(filemap_dontcache_edge_complete);
+
/**
* folio_end_writeback_no_dropbehind - End writeback against a folio.
* @folio: The folio.
@@ -2119,8 +2189,8 @@ struct folio *__filemap_get_folio_mpol(struct address_space *mapping,
* completes it.
*/
if ((fgp_flags & FGP_DONTCACHE) && (fgp_flags & FGP_DONTCACHE_EDGE) &&
- (fgp_flags & FGP_LOCK) && folio_test_uptodate(folio) &&
- filemap_dontcache_edge_unit(mapping, folio))
+ !(fgp_flags & FGP_DONTCACHE_DEFER) && (fgp_flags & FGP_LOCK) &&
+ folio_test_uptodate(folio) && filemap_dontcache_edge_unit(mapping, folio))
folio_mark_dropbehind(folio);
no_page:
if (!folio && (fgp_flags & FGP_CREAT)) {
--
2.52.0
^ permalink raw reply related [flat|nested] 12+ messages in thread
* [PATCH v10 7/7] NFSD: drop a shared boundary page only once both WRITEs have written it
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
` (5 preceding siblings ...)
2026-11-03 21:17 ` [PATCH v10 6/7] mm, iomap: let an uncached writer complete its shared pages itself Mike Snitzer
@ 2026-11-03 21:17 ` Mike Snitzer
2026-11-04 9:15 ` Christoph Hellwig
2026-11-04 9:17 ` [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Christoph Hellwig
7 siblings, 1 reply; 12+ messages in thread
From: Mike Snitzer @ 2026-11-03 21:17 UTC (permalink / raw)
To: Matthew Wilcox, linux-mm, linux-fsdevel
Cc: Chuck Lever, Jeff Layton, linux-nfs, NeilBrown, Olga Kornievskaia,
Dai Ngo, Tom Talpey, Christoph Hellwig, Jens Axboe,
Jonathan Corbet, Jonathan Flynn
NFSD splits a misaligned direct-mode WRITE into a direct middle and
buffered DONTCACHE ends. Each end covers its first or last unit, the
smallest folio the mapping allows, only partly, and shares that unit
with the neighbouring WRITE.
Issue the buffered ends with IOCB_DONTCACHE_DEFER. Before writing an
end, ask filemap_dontcache_edge_completes() whether the neighbouring
WRITE has written its part of each shared unit. Once the WRITE, its
fsync included, is done, complete those units with
filemap_dontcache_edge_complete(). A short write completes a unit only
if it wrote its own part of it.
With this commit, 32 interleaved writers of 47008-byte records through
NFSD DIRECT read 0.0001 to 0.0002 blocks per WRITE, instead of reading
0.99 blocks per WRITE. FILE_SYNC WRITEs no longer wake the DONTCACHE
writeback kick, where they woke it 23,000 to 65,000 times a run without
IOCB_DONTCACHE_DEFER. On a four-server pNFS flexfiles rig, the page
cache grew by 2.9 GiB during the write phase instead of about 11 GiB.
Reported-by: Jonathan Flynn <jonathan.flynn@hammerspace.com>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on questions from earlier reviews, with the code that answers each:
- Unit size. nfsd_write_dio_seg_hold() uses
mapping_min_folio_nrbytes(), as iomap_get_folio() does (5/7). The
cut between the buffered ends and the direct middle is on the same
boundary since commit b16607436d31 ("NFSD: cut a misaligned
direct-mode WRITE at folio boundaries") in cel/nfsd-testing.
- Short write. NFSD completes the first edge only if written is at
least first_len, and the last edge only if the write covered the
whole segment.
- Release after an error. This is on purpose. A segment whose write
failed is not held: nheld increases only after a write that returns
>= 0. A held segment wrote its bytes, so its edges hold the data
of both WRITEs. After an fsync error the release still runs: the
folio is clean when its writeback ends, and the helper drops it as
DONTCACHE writeback would. The error stays in the mapping and
NFSD returns it. The release changes only what stays cached,
never data.
- Unset fields. For a segment that holds nothing,
nfsd_write_dio_seg_hold() returns early and does not set
first_len. complete_first and complete_last are then false, so
nfsd_write_dio_seg_release() does not read first_len.
- Array size. nsegs is at most 3, so nheld is at most 3 (holds[3]).
.../filesystems/nfs/nfsd-io-modes.rst | 17 +++
fs/nfsd/vfs.c | 106 ++++++++++++++++--
2 files changed, 111 insertions(+), 12 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index e6fd91cac9c77..57acc8f0407ee 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -197,6 +197,23 @@ Misaligned WRITE:
above. Set it to N for 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, the page cache keeps a page an
+ uncached write covers only partly, unmarked, for the write that
+ completes it. NFSD asks, immediately before writing a start or end
+ segment, whether the neighbouring WRITE has already written its
+ part; if it has, NFSD marks the page DONTCACHE once its own WRITE,
+ including its fsync, is complete, and the page is dropped. A page
+ whose neighbouring WRITE never arrives, or arrives at the same time,
+ 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 for a filesystem that does not use iomap for
+ buffered writes.
+
Tracing:
The nfsd_read_direct trace event marks a READ that NFSD issues as
O_DIRECT. It records the offset and length the client requested.
diff --git a/fs/nfsd/vfs.c b/fs/nfsd/vfs.c
index 269dc4968f197..5a2bd2f57620a 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -1428,6 +1428,7 @@ static int wait_for_concurrent_writes(struct file *file)
struct nfsd_write_dio_seg {
struct iov_iter iter;
int flags;
+ bool claim; /* completes shared pages */
};
static unsigned long
@@ -1447,6 +1448,78 @@ 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;
+}
+
+/* The shared pages a segment completes, and how much of it was written. */
+struct nfsd_write_dio_hold {
+ loff_t first, last;
+ size_t first_len; /* the segment's bytes in its first unit */
+ bool complete_first, complete_last;
+ size_t written;
+};
+
+/*
+ * A page that a buffered DONTCACHE segment covers only partly is shared with
+ * the neighbouring WRITE. The page cache keeps it, unmarked, for the write
+ * that completes it; the segment is issued IOCB_DONTCACHE_DEFER so that this
+ * WRITE, not the page cache, marks it, once the whole WRITE, its fsync
+ * included, is complete. Ask whether the neighbour has already written its
+ * part immediately before writing @seg: asking earlier gives the neighbour
+ * time to write its part and find this WRITE's part missing.
+ */
+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)
+{
+ struct address_space *mapping = file->f_mapping;
+ size_t unit, last_len;
+ loff_t mask, last_unit;
+ bool one_unit, head, tail;
+
+ hold->first = pos;
+ hold->last = pos + len - 1;
+ hold->complete_first = false;
+ hold->complete_last = false;
+ hold->written = 0;
+ if (!seg->claim || !len)
+ return;
+
+ /* The shared unit is the smallest folio the mapping allows. */
+ unit = mapping_min_folio_nrbytes(mapping);
+ mask = ~(loff_t)(unit - 1);
+ one_unit = (hold->first & mask) == (hold->last & mask);
+ head = hold->first & (unit - 1);
+ tail = (hold->last + 1) & (unit - 1);
+ last_unit = hold->last & mask;
+ last_len = (hold->last & (unit - 1)) + 1;
+
+ hold->first_len = min_t(size_t, len,
+ unit - (hold->first & (unit - 1)));
+ if (head || (one_unit && tail))
+ hold->complete_first =
+ filemap_dontcache_edge_completes(mapping, hold->first,
+ hold->first_len);
+ if (!one_unit && tail)
+ hold->complete_last =
+ filemap_dontcache_edge_completes(mapping, last_unit,
+ last_len);
+}
+
+/* Drop 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)
+{
+ struct address_space *mapping = file->f_mapping;
+
+ /* A short write may have stopped inside the first unit. */
+ if (hold->complete_first && hold->written >= hold->first_len)
+ filemap_dontcache_edge_complete(mapping, hold->first);
+ /* A short write did not reach the last unit. */
+ if (hold->complete_last &&
+ hold->written == hold->last - hold->first + 1)
+ filemap_dontcache_edge_complete(mapping, hold->last);
}
static unsigned int
@@ -1469,7 +1542,9 @@ nfsd_write_dio_iters_init(struct svc_rqst *rqstp, struct svc_fh *fhp,
if (nf->nf_file->f_op->fop_flags & FOP_DONTCACHE)
dontcache_flags = IOCB_DONTCACHE;
- buffered_flags = nfsd_direct_misaligned_dontcache ? dontcache_flags : 0;
+ /* NFSD marks their shared pages itself, see nfsd_write_dio_seg_hold() */
+ buffered_flags = nfsd_direct_misaligned_dontcache && dontcache_flags ?
+ dontcache_flags | IOCB_DONTCACHE_DEFER : 0;
/*
* Check if direct I/O is feasible for this write request.
@@ -1523,7 +1598,8 @@ nfsd_write_dio_iters_init(struct svc_rqst *rqstp, struct svc_fh *fhp,
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,
@@ -1556,7 +1632,8 @@ nfsd_write_dio_iters_init(struct svc_rqst *rqstp, struct svc_fh *fhp,
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;
@@ -1565,6 +1642,7 @@ nfsd_write_dio_iters_init(struct svc_rqst *rqstp, struct svc_fh *fhp,
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;
}
@@ -1573,12 +1651,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(rqstp, fhp, nf, rqstp->rq_bvec,
@@ -1608,23 +1687,26 @@ nfsd_direct_write(struct svc_rqst *rqstp, struct svc_fh *fhp,
kiocb->ki_flags);
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;
+ /* Complete the shared units once the WRITE and its fsync are done. */
+ 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] 12+ messages in thread
* Re: [PATCH v10 7/7] NFSD: drop a shared boundary page only once both WRITEs have written it
2026-11-03 21:17 ` [PATCH v10 7/7] NFSD: drop a shared boundary page only once both WRITEs have written it Mike Snitzer
@ 2026-11-04 9:15 ` Christoph Hellwig
2026-11-04 17:01 ` Mike Snitzer
0 siblings, 1 reply; 12+ messages in thread
From: Christoph Hellwig @ 2026-11-04 9:15 UTC (permalink / raw)
To: Mike Snitzer
Cc: Matthew Wilcox, linux-mm, linux-fsdevel, Chuck Lever, Jeff Layton,
linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Jonathan Corbet, Jonathan Flynn
On Tue, Nov 03, 2026 at 04:17:54PM -0500, Mike Snitzer wrote:
> NFSD splits a misaligned direct-mode WRITE into a direct middle and
> buffered DONTCACHE ends. Each end covers its first or last unit, the
> smallest folio the mapping allows, only partly, and shares that unit
> with the neighbouring WRITE.
>
> Issue the buffered ends with IOCB_DONTCACHE_DEFER. Before writing an
> end, ask filemap_dontcache_edge_completes() whether the neighbouring
> WRITE has written its part of each shared unit. Once the WRITE, its
> fsync included, is done, complete those units with
Why does the write include an fsync? Are these STABLE writes, and
if yes, why?
> filemap_dontcache_edge_complete(). A short write completes a unit only
> if it wrote its own part of it.
>
> With this commit, 32 interleaved writers of 47008-byte records through
Can you explain where this very odd number comes from. Is that just a
worst case? Or for some reason actually relevant?
> NFSD DIRECT read 0.0001 to 0.0002 blocks per WRITE, instead of reading
> 0.99 blocks per WRITE. FILE_SYNC WRITEs no longer wake the DONTCACHE
> writeback kick, where they woke it 23,000 to 65,000 times a run without
> IOCB_DONTCACHE_DEFER. On a four-server pNFS flexfiles rig, the page
> cache grew by 2.9 GiB during the write phase instead of about 11 GiB.
How does this compare to just not using DONTCACHE at all for unaligned
tails (and maybe heads, if for some reason they I/O you care about
reordered writes). And how does it compare to that plus manual
sync_file_range calls to micro-manage the I/O?
In general I'm not in favour of so many core hacks for
very nfsd-specific behavior.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
` (6 preceding siblings ...)
2026-11-03 21:17 ` [PATCH v10 7/7] NFSD: drop a shared boundary page only once both WRITEs have written it Mike Snitzer
@ 2026-11-04 9:17 ` Christoph Hellwig
7 siblings, 0 replies; 12+ messages in thread
From: Christoph Hellwig @ 2026-11-04 9:17 UTC (permalink / raw)
To: Mike Snitzer
Cc: Matthew Wilcox, linux-mm, linux-fsdevel, Chuck Lever, Jeff Layton,
linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Jonathan Corbet
On Tue, Nov 03, 2026 at 04:17:47PM -0500, Mike Snitzer wrote:
> Hi,
>
> An uncached (RWF_DONTCACHE) buffered write drops each folio it writes
> once the folio is written back. A folio that the write covers only
> partly is shared with the neighbouring write. If it is dropped before
> the neighbour writes its part, the neighbour reads it back from disk
> first. 32 threads writing interleaved records to one XFS file, 4 KiB
> pages, before and after 5/7:
Very colourful terminology here. Who is "the neighbour". How do we
know a "neighbour" exists and what it wants to do?
In general if the user gave a very clear indication that the data
should not be cached, it is a bit awkward to second uess that.
Especially if the expectations are not very clearly documented.
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v10 1/7] mm: move clearing the uncached flag of a found folio into a helper
2026-11-03 21:17 ` [PATCH v10 1/7] mm: move clearing the uncached flag of a found folio into a helper Mike Snitzer
@ 2026-11-04 9:17 ` Christoph Hellwig
0 siblings, 0 replies; 12+ messages in thread
From: Christoph Hellwig @ 2026-11-04 9:17 UTC (permalink / raw)
To: Mike Snitzer
Cc: Matthew Wilcox, linux-mm, linux-fsdevel, Chuck Lever, Jeff Layton,
linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Christoph Hellwig, Jens Axboe, Jonathan Corbet
Looks good as a general cleanup independent of the rest:
Reviewed-by: Christoph Hellwig <hch@lst.de>
^ permalink raw reply [flat|nested] 12+ messages in thread
* Re: [PATCH v10 7/7] NFSD: drop a shared boundary page only once both WRITEs have written it
2026-11-04 9:15 ` Christoph Hellwig
@ 2026-11-04 17:01 ` Mike Snitzer
0 siblings, 0 replies; 12+ messages in thread
From: Mike Snitzer @ 2026-11-04 17:01 UTC (permalink / raw)
To: Christoph Hellwig
Cc: Matthew Wilcox, linux-mm, linux-fsdevel, Chuck Lever, Jeff Layton,
linux-nfs, NeilBrown, Olga Kornievskaia, Dai Ngo, Tom Talpey,
Jens Axboe, Jonathan Corbet, Jonathan Flynn
On Wed, Nov 04, 2026 at 10:15:43AM +0100, Christoph Hellwig wrote:
> Why does the write include an fsync? Are these STABLE writes, and
> if yes, why?
Only when the client asks for it. A WRITE with stable_how FILE_SYNC or
DATA_SYNC must be on stable storage before NFSD replies. So NFSD runs
one vfs_fsync_range() over the whole WRITE after all of its segments.
An UNSTABLE WRITE has no fsync. Its shared units are completed while
still dirty, and the writeback that cleans them drops them, whether the
flusher or the client's COMMIT starts it.
The Linux client sends an O_DIRECT write that fits in one WRITE as
FILE_SYNC (FLUSH_COND_STABLE in nfs_pgio_rpcsetup()). That is the
common case for the workload below.
> Can you explain where this very odd number comes from. Is that just a
> worst case? Or for some reason actually relevant?
It is the transfer size of IO500's ior-hard: every rank writes
47008-byte records, interleaved with the other ranks, into one shared
file, with O_DIRECT. It is meant as a hard case, and it is the
workload that showed the problem on our servers. The same happens
with any record size that is not a multiple of the page size. The
cover letter also has 6000-byte records.
> How does this compare to just not using DONTCACHE at all for unaligned
> tails (and maybe heads, if for some reason they I/O you care about
> reordered writes).
Jon measured that on four pNFS flexfiles data servers with ior-hard
from three clients. In one run, cached heads and tails removed the
reads, 0.000 per record. But the page cache on the four servers grew
by 622 GiB during a 318 s write phase. In a later run of the same
workload with this series, it grew by 2.9 GiB, at 0.003 reads per
record. Keeping the data out of the server's page cache is the point
of direct mode, so cached ends are not a fix for this case. Using
buffered heads and tails are available (in cel/nfsd-testing): set the
NFSD debugfs knob /sys/kernel/debug/nfsd/direct_misaligned_dontcache
to N.
> And how does it compare to that plus manual
> sync_file_range calls to micro-manage the I/O?
Wasn't measured because sync_file_range() writes the folio back but
leaves it cached. To get it out of the cache, NFSD would have to drop
it after writeback. That is only safe once the neighbouring WRITE has
written its part of the folio. Otherwise the neighbour reads it back
from disk, which is the problem in the first place. NFSD does not know
when that is without tracking the half-written folios of every file
itself. An earlier version of NFSD kept one tail folio per open file
for the next sequential WRITE. With interleaved writers the next
WRITE is rarely the neighbour, and it still read 0.81 blocks per
record. The folio's uptodate state already records whether the
neighbour has written its part, and 5/7 uses that.
> In general I'm not in favour of so many core hacks for
> very nfsd-specific behavior.
Most of the series is not NFSD-specific. 1/7 to 5/7 fix RWF_DONTCACHE
itself, for any application. The table in the cover letter is plain
pwritev2() on a local XFS file, with no NFSD. It reads 0.97 blocks per
write at 831 MiB/s before 5/7, and 0.0005 at 2281 MiB/s after it. 2/7
also fixes a bug in v7.3 on its own.
Only 6/7 is for callers that sync once after writing several segments,
which is what NFSD does for a stable WRITE. The NFS client's LOCALIO
path does the same. Without 6/7, each stable WRITE wakes the
DONTCACHE writeback kick once per segment, 23,000 to 65,000 times a
run, for folios that its own fsync is about to write.
Thanks,
Mike
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2026-11-04 17:01 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-03 21:17 [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 1/7] mm: move clearing the uncached flag of a found folio into a helper Mike Snitzer
2026-11-04 9:17 ` Christoph Hellwig
2026-11-03 21:17 ` [PATCH v10 2/7] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 3/7] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 4/7] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 5/7] mm, iomap: keep a partly covered page until an uncached write completes it Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 6/7] mm, iomap: let an uncached writer complete its shared pages itself Mike Snitzer
2026-11-03 21:17 ` [PATCH v10 7/7] NFSD: drop a shared boundary page only once both WRITEs have written it Mike Snitzer
2026-11-04 9:15 ` Christoph Hellwig
2026-11-04 17:01 ` Mike Snitzer
2026-11-04 9:17 ` [PATCH v10 0/7] mm, iomap, NFSD: stop uncached writes reading back shared pages Christoph Hellwig
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox