* [PATCH v9 0/6] mm, iomap, NFSD: stop uncached writes reading back shared pages
@ 2026-10-24 22:11 Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 1/6] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there Mike Snitzer
` (5 more replies)
0 siblings, 6 replies; 7+ messages in thread
From: Mike Snitzer @ 2026-10-24 22:11 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,
Please consider this v9 for the v7.5 development cycle, so that it can
go upstream in the v7.5 merge window.
An uncached (RWF_DONTCACHE) buffered write drops each page it writes
once that page is written back. A page the write covers only partly is
shared with the neighbouring write. Once the first of the two is
written back, the second reads the page back from disk before it writes
its part. 32 threads wrote interleaved records to one file on
XFS (emulated 4Kn NVMe, 4 KiB pages). Before and after 4/6:
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
What is left after is metadata. 49152-byte records cover only whole
pages, and they read 0.0005 per write either way. The test program and
script that produced this table, and how to run them, are in:
https://lore.kernel.org/linux-nfs/atqJBS8MlYfeUU-t@kernel.org/
4/6 has the page cache keep such a page for the write that completes
it. 5/6 lets a writer that syncs its own write complete those pages
itself once the sync is done. NFSD does that for a stable WRITE. 6/6
is the NFSD change that uses it. 2/6 reports the DONTCACHE dirty count
in the bdi debugfs stats. 3/6 adds folio_mark_dropbehind(), which 4/6
and 5/6 use.
1/6 fixes 88d6f128d06d ("mm: track DONTCACHE dirty pages per
bdi_writeback"), in v7.3. A cached lookup that clears the dropbehind
flag could uncount a folio that was never counted. It could also leave
a folio counted with nothing to uncount it. The patch applies to v7.3
as it is, if the mm side would rather take it alone.
v8 1/7 is not in this series. As Chuck asked, I post it on its own. It
is a revised version of 80728157d1bd ("NFSD: cut a misaligned
direct-mode WRITE at page boundaries") in cel/nfsd-testing. This series
applies on cel/nfsd-testing at 6d13b7f74eec ("NFSD: Map every
NFS_ACLv2 reply status into aclstat2"), with or without that revision.
Each patch builds with W=1 with no warnings.
1/6 to 5/6 are mm, with small iomap changes in 4/6 and 5/6. They are
Cc'd to linux-mm and linux-fsdevel. If the mm and iomap maintainers
are agreeable, their acks would let Chuck take the series through the
nfsd tree. But whatever approach the maintainers prefer for this kind
of cross-subsystem merge is obviously fine.
Changes since v8, from two independent sashiko reviews of v8: the one
Jeff Layton relayed, and the one that sashiko-bot posted to linux-mm.
Some findings were in both.
- 1/6 and 3/6 (both reviews): a folio is counted in WB_DONTCACHE_DIRTY
only when both PG_dirty and its dirty tag are set. v8 tested the tag
alone. __folio_cancel_dirty() clears PG_dirty but leaves the tag, and
so does writeback that stops at EOF after folio_clear_dirty_for_io().
A folio that stays cached then keeps a stale tag. 1/6 uncounted such
a folio twice, and 3/6 counted it and never uncounted it.
- 4/6, 5/6 and 6/6 (second review): with blocks smaller than the folio,
iomap tracks uptodate per block. The edge that a write completes is
then not uptodate when the write finds it, so it was never marked and
stayed cached until reclaim. This happens with 4 KiB blocks on 16 or
64 KiB pages. The new folio_dontcache_edge_written() also accepts a
folio whose blocks outside this write are uptodate.
filemap_dontcache_edge_completes() takes the range that the write
covers, and 6/6 passes it.
- 5/6 (second review): filemap_dontcache_edge_complete() now waits for
writeback in flight, which could not drop the folio. It also kicks
DONTCACHE writeback when it marks a dirty folio. The earlier kick
came before the mark. It drops a clean folio that is not mapped
without a mark. A mark that filemap_end_dropbehind() then cleared
outside the i_pages lock could leave WB_DONTCACHE_DIRTY counted.
- 6/6 (both reviews): NFSD finds the edges that a buffered segment
shares by the mapping's smallest folio, as iomap does, not by page.
On 16 KiB blocks, a WRITE written as one buffered segment can have
page-aligned ends inside a block. Nothing completed those edges.
NFSD also completes a segment's first edge only when the write
reached the end of the segment's bytes in that unit (first review).
- 4/6 (first review): a paragraph on a limit. 4/6 marks the folio
before iomap validates the iomap. A stale iomap stopped by a fatal
signal then leaves a mark on a folio that this write did not complete.
This costs one read-back and no data.
- 2/6: unchanged.
Patches 1, 3, 4, 5 and 6 carry notes below the --- line. The notes
give each question from the v8 reviews, and the code, lock or check
that answers it. This includes the limits that the series keeps on
purpose.
Before posting, I ran a third review of v9 locally, with Sashiko's
review stages and the review prompts it uses. It found no new
problems.
Measured for 6/6 on 16 KiB-block XFS over the loop device. Its
dma_alignment is 511, so every TCP payload is memory-misaligned and
NFSD writes each WRITE as one buffered segment. 32 writers of 12288-byte
records, 1 GiB: v8 left all 32,736 pages of a 128 MiB file resident.
v9 leaves 948 of 262,080 pages (0.4%) and reads 0.0003 blocks per
record. The v8 results hold. In every run BdiDontcacheDirty was 0
after the final sync. That includes runs that mix uncached writes,
cached writes and mmap stores on one file. Every record verified.
I measured all of v9 again on 4 KiB pages, with XFS blocks of 1 KiB
and 2 KiB over the loop device. These blocks are smaller than the page.
32 writers wrote 47008-byte records, 1 GiB. Uncached application writes
left 0 of 261,850 pages resident. NFSD left 497 (1 KiB blocks) and 349
(2 KiB blocks). LOCALIO left 854 and 949. Every path read 0.0001
blocks per record. A kernel without these fixes, on a 128 MiB run with
1 KiB blocks, left about one page for every two records. Uncached
writes through NFSD, cached writes and mmap stores ran together on one
file, with 4 KiB and 1 KiB blocks. BdiDontcacheDirty was 0 after every
round, and every record verified. I did not test kernels with 16 KiB
or 64 KiB pages.
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 (6):
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 | 36 ++-
fs/iomap/trace.h | 3 +-
fs/nfsd/vfs.c | 116 +++++++-
include/linux/fs.h | 8 +-
include/linux/iomap.h | 1 +
include/linux/pagemap.h | 14 +
mm/backing-dev.c | 6 +
mm/filemap.c | 265 +++++++++++++++++-
9 files changed, 435 insertions(+), 31 deletions(-)
base-commit: 6d13b7f74eec16e08d5e7f772ee119432a14ef2c
--
2.52.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v9 1/6] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there
2026-10-24 22:11 [PATCH v9 0/6] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
@ 2026-10-24 22:11 ` Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 2/6] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
` (4 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Mike Snitzer @ 2026-10-24 22:11 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 in __filemap_get_folio_mpol() that finds a folio marked
dropbehind clears the flag, and subtracts the folio from
WB_DONTCACHE_DIRTY if PG_dirty is set. But filemap_dirty_folio() sets
PG_dirty before folio_account_dirtied() counts the folio, under the
i_pages lock and without the folio lock. A lookup between the two
subtracts a folio that was never added, and folio_account_dirtied(),
finding the flag clear, does not add it either, so the counter the
DONTCACHE writeback kick reads falls short and can go negative. A
lookup without FGP_LOCK also races with the paths that uncount a dirty
folio, which hold the folio lock: folio_clear_dirty_for_io() clears
PG_dirty and then tests the flag, so a lookup between the two leaves
the folio counted with no one to uncount it.
Every path that uncounts a dirty folio holds the folio lock, and
folio_account_dirtied() sets the folio's dirty tag in the same i_pages
section that counts it. So clear the flag with the folio locked. A
lookup without FGP_LOCK takes the lock with a trylock. Under the
i_pages lock, test both PG_dirty and the dirty tag: the folio is
uncounted only when both are set. Neither alone says that the folio
is counted. A dirtier past folio_test_set_dirty() but not yet counting
has set PG_dirty, not the tag. __folio_cancel_dirty() uncounts a folio
and clears PG_dirty, but leaves the tag until writeback or truncation
clears it. Under that lock the wb is
inode_to_wb(), as in folio_account_dirtied() and inode_do_switch_wbs(),
which moves the counter by the same tag. A lookup that finds the
folio locked by another task leaves the flag set, as it would have
been without the lookup.
Fixes: 88d6f128d06d ("mm: track DONTCACHE dirty pages per bdi_writeback")
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on the v8 review questions, with the code that answers each:
- Stale dirty tag. __folio_cancel_dirty() clears PG_dirty and
uncounts the folio in folio_account_cleaned(), but leaves
PAGECACHE_TAG_DIRTY set. filemap_clear_dropbehind() uncounts only
when folio_test_dirty() and the tag are both true, so a stale tag
does not uncount twice.
- 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 in folio_account_dirtied() and
sets the tag under the i_pages lock. The uncount paths
(__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.
mm/filemap.c | 57 ++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 44 insertions(+), 13 deletions(-)
diff --git a/mm/filemap.c b/mm/filemap.c
index 00fd89cf6f550..5176c4125a4db 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1927,6 +1927,48 @@ void *filemap_get_entry(struct address_space *mapping, pgoff_t index)
return folio;
}
+/*
+ * Clear the uncached flag of a folio a cached lookup found, and take the
+ * folio off WB_DONTCACHE_DIRTY if it is counted there.
+ *
+ * folio_account_dirtied() counts a dropbehind folio under the i_pages lock,
+ * in the section that sets its dirty tag, after PG_dirty has been set
+ * without the folio lock. Every path that uncounts a dirty folio holds the
+ * folio lock. So with the folio locked and the i_pages lock held, the folio
+ * is counted when both PG_dirty and the dirty tag are set. PG_dirty alone
+ * is not enough: a dirtier past folio_test_set_dirty() but not yet counting
+ * finds the flag clear and does not count the folio. The tag alone is not
+ * enough either: __folio_cancel_dirty() uncounts a folio and clears
+ * PG_dirty, but leaves the tag until writeback or truncation clears it. A
+ * folio another task has locked keeps the flag, as it would have had this
+ * lookup not run.
+ */
+static void filemap_clear_dropbehind(struct address_space *mapping,
+ struct folio *folio, bool locked)
+{
+ unsigned long flags;
+
+ 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);
+}
+
/**
* __filemap_get_folio_mpol - Find and get a reference to a folio.
* @mapping: The address_space to search.
@@ -2058,19 +2100,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) && 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] 7+ messages in thread
* [PATCH v9 2/6] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats
2026-10-24 22:11 [PATCH v9 0/6] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 1/6] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there Mike Snitzer
@ 2026-10-24 22:11 ` Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 3/6] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
` (3 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Mike Snitzer @ 2026-10-24 22:11 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 18e999053bae0..111b452629796 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] 7+ messages in thread
* [PATCH v9 3/6] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it
2026-10-24 22:11 [PATCH v9 0/6] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 1/6] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 2/6] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
@ 2026-10-24 22:11 ` Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 4/6] mm, iomap: keep a partly covered page until an uncached write completes it Mike Snitzer
` (2 subsequent siblings)
5 siblings, 0 replies; 7+ messages in thread
From: Mike Snitzer @ 2026-10-24 22:11 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 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
has been counted as dirty, add it to WB_DONTCACHE_DIRTY. Neither
PG_dirty nor the dirty tag alone says that the folio is counted.
filemap_dirty_folio() sets PG_dirty before folio_account_dirtied()
counts the folio, under the i_pages lock and without the folio lock. A
folio marked between the two would be counted twice.
__folio_cancel_dirty() uncounts a folio and clears PG_dirty, but leaves
the tag until writeback or truncation clears it. A folio marked with
that stale tag would be counted and never uncounted.
folio_account_dirtied() sets the tag in the same i_pages section that
counts the folio, and every path that uncounts a dirty folio holds the
folio lock. So set the flag and test both under the i_pages lock, and
count the folio only when both are set. A dirtier not yet counting the
folio finds the flag set and counts it itself. Under that lock the wb
is inode_to_wb(),
as in folio_account_dirtied(). 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.
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on the v8 review questions, with the code that answers each:
- Counting rule. The same rule as filemap_clear_dropbehind() in
1/6: folio_mark_dropbehind() counts the folio only when
folio_test_dirty() and PAGECACHE_TAG_DIRTY are both true. The
test is under the folio lock (VM_WARN_ON_ONCE_FOLIO checks it) and
the i_pages lock. A stale tag on a clean folio does not add a
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()).
include/linux/pagemap.h | 1 +
mm/filemap.c | 42 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 43 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 5176c4125a4db..d0e4d1788fd40 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1644,6 +1644,48 @@ 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
+ * already counted as 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.
+ *
+ * folio_account_dirtied() counts a folio under the i_pages lock, in the
+ * section that sets its dirty tag, after PG_dirty has been set without the
+ * folio lock. Every path that uncounts a dirty folio holds the folio lock.
+ * So with the folio locked and the i_pages lock held, the folio is counted
+ * when both PG_dirty and the dirty tag are set. PG_dirty alone is not
+ * enough: a dirtier past folio_test_set_dirty() but not yet counting finds
+ * the flag set and adds the folio itself. The tag alone is not enough
+ * either: __folio_cancel_dirty() uncounts a folio and clears PG_dirty, but
+ * leaves the tag until writeback or truncation clears it.
+ */
+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] 7+ messages in thread
* [PATCH v9 4/6] mm, iomap: keep a partly covered page until an uncached write completes it
2026-10-24 22:11 [PATCH v9 0/6] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
` (2 preceding siblings ...)
2026-10-24 22:11 ` [PATCH v9 3/6] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
@ 2026-10-24 22:11 ` Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 5/6] mm, iomap: let an uncached writer complete its shared pages itself Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 6/6] NFSD: drop a shared boundary page only once both WRITEs have written it Mike Snitzer
5 siblings, 0 replies; 7+ messages in thread
From: Mike Snitzer @ 2026-10-24 22:11 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 uncached (RWF_DONTCACHE) buffered write marks drop-behind every folio
it creates, so writeback drops each one as soon as this write's data is
on disk. A page the write covers only partly is usually shared with the
neighbouring write, the one that ends or starts in it. Once the first
of the two has been written back, the page is gone, and the second has
to read the block back from disk before it can write its part: one read
per misaligned write.
Measured locally on XFS (emulated 4Kn NVMe, 4 KiB pages), 32 threads
writing interleaved records to one file with pwritev2(), 1 GiB per run,
before and after this patch:
record 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
Before this patch every write waited for its read, and because XFS holds
its IOLOCK exclusively across a buffered write, the other 31 writers
waited too: both record sizes ran at about 20 thousand writes a second,
one device read per write; after it only metadata is read. With
RWF_DSYNC each write still waits for its own sync, so the gain is
smaller: the uncached write now runs as fast as the same write without
RWF_DONTCACHE, 1237 and 209 MiB/s against 1227 and 206 in the table.
Records of 49152 bytes, which cover only whole pages, read only that
metadata, 0.0005 per write, before and after.
NFSD DIRECT issues the misaligned ends of a WRITE as buffered uncached
segments and gains the same way. Without any handling of the shared
page, 32 interleaved writers of 47008-byte records read 45144 blocks
for 45664 WRITEs; with this patch, and no NFSD code for the shared page,
0.0001 to 0.0008 blocks per WRITE whether each record is sent as one
FILE_SYNC WRITE, as two one after the other, as two together, or as two
UNSTABLE. The page cache then marks the completed page while it is
still dirty, so each segment of a stable WRITE wakes the DONTCACHE
writeback kick (23 to 65 thousand rounds a run); the next two patches
let NFSD mark it after its sync instead.
Let the write path say which pages a write covers only partly
(FGP_DONTCACHE_EDGE) and get such a page as the smallest folio the
mapping allows: a single page, or a single block where the mapping's
minimum folio order makes the block larger than a page, which is then
the unit two writes share. iomap sets it when the write starts inside
that unit or ends before its end, testing against
mapping_min_folio_nrbytes(). A folio created for it is not marked, so
writing back this write's part does not drop it. A folio found
uptodate is the neighbour's, already written: this write completes it,
so mark it, under the folio lock and before its own data goes in, and
the writeback that cleans both writes' data drops it. The folio lock
orders the two writes, so the second always finds the first's data in
place. A folio found there that is larger than that unit, left by
readahead or another buffered write, holds more than the two writes
share and cannot be dropped in part: it is not marked, and stays cached
as it would without this change.
Some file systems track uptodate per block, as iomap does for blocks
smaller than the folio. There a folio is uptodate only once all its
blocks are. So the edge that a write completes is not yet uptodate when
the write looks it up. folio_dontcache_edge_written() also accepts such
a folio. The neighbour wrote its part if every block that this write
does not cover in full is uptodate (->is_partially_uptodate).
iomap_get_folio() marks an edge that passes this test. This matters for
4 KiB blocks on 16 or 64 KiB pages, and for blocks smaller than 4 KiB
pages.
A page whose neighbour never writes, such as the last page of a file,
stays cached until reclaim. A partly covered page another task had
cached and that is uptodate is now dropped after the write's writeback,
where before an uncached write never dropped a page it did not create.
The mark is set when the lookup finds the folio, before iomap validates
the iomap. If the iomap is stale and a fatal signal stops the retry, the
folio keeps the mark although this write did not complete it. Writeback
cleans the folio before it drops it, and the application's retry reads
the folio back once. The lookup cannot undo the mark: a folio an earlier
write marked returns early, so the lookup cannot tell whether it set the
mark.
Only iomap file systems say which pages a write covers partly so far;
write_begin_get_folio() users such as ext4 and the NFS client do not.
With blocks smaller than a page, a write that crosses an extent boundary
inside a page asks for it as two partly covered halves, so the page,
though one write covers it whole, stays cached until reclaim.
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on the v8 review questions, with the code that answers each:
- Mark before iomap validation. The commit message states this
limit. The cost is one read of the folio on the application's
retry. Writeback cleans the folio before the page cache drops it,
so data is safe.
- 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 helper reads the
per-block state 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.
- Page cache 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 (3/6), so a clean folio with a stale tag is not counted.
- File systems that do not use iomap_get_folio() never set
FGP_DONTCACHE_EDGE, so their behaviour does not change.
fs/iomap/buffered-io.c | 32 +++++++++++++++--
include/linux/pagemap.h | 6 ++++
mm/filemap.c | 78 ++++++++++++++++++++++++++++++++++++++++-
3 files changed, 112 insertions(+), 4 deletions(-)
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index 0a5ebfda90f12..eb1f7f019a852 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -798,16 +798,42 @@ 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;
+ /*
+ * The smallest folio the mapping allows, a page or a block
+ * where the block is larger, that this write covers only
+ * partly is shared with a neighbouring write: get it as just
+ * that, which is kept until the write that completes it.
+ */
+ 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));
+ /*
+ * The page cache marks an edge it finds uptodate. With blocks
+ * smaller than the folio, the edge this write completes is not
+ * uptodate yet: mark it if its other blocks are.
+ */
+ 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 3012bd95812a1..0b151c1201082 100644
--- a/include/linux/pagemap.h
+++ b/include/linux/pagemap.h
@@ -701,6 +701,10 @@ 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 this page
+ * only partly, and a neighbouring write may complete it. A folio created
+ * for it is not marked drop-behind; a folio found uptodate is, as the
+ * write that completes it.
* * %FGP_WRITEBEGIN - The flags to use in a filesystem write_begin()
* implementation.
*/
@@ -715,6 +719,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)
@@ -1311,6 +1316,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 d0e4d1788fd40..d7bbea442a019 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1686,6 +1686,62 @@ void folio_mark_dropbehind(struct folio *folio)
}
EXPORT_SYMBOL_GPL(folio_mark_dropbehind);
+/*
+ * Is @folio the unit two writes share at an edge: the smallest folio the
+ * mapping allows, a single page or, where the block is larger, a single
+ * block? A larger folio, left by readahead or another buffered write, holds
+ * more than that unit and is not marked for an edge.
+ */
+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: a locked folio, the edge two uncached writes share
+ * @pos: file offset of the first byte this write covers in @folio
+ * @len: bytes this write covers in @folio
+ *
+ * An uptodate folio has. Where the file system tracks uptodate per block, a
+ * folio is uptodate only once all its blocks are, so the folio a write is
+ * about to complete is not yet uptodate. Its neighbour has then written its
+ * part if every block that this write does not cover in full is uptodate.
+ *
+ * Return: true if @folio is an edge unit and its neighbour has written its
+ * part of it.
+ */
+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.
@@ -2069,6 +2125,19 @@ struct folio *__filemap_get_folio_mpol(struct address_space *mapping,
if (fgp_flags & FGP_STABLE)
folio_wait_stable(folio);
+
+ /*
+ * An uncached write that covers this folio only partly, finding it
+ * uptodate, is the write that completes it: the neighbouring write
+ * that left it here has written its part. Mark it now, under the
+ * folio lock, so the writeback that cleans both writes' data drops it.
+ * Not a folio larger than the edge the two writes share, which holds
+ * more than they wrote and cannot be dropped in part.
+ */
+ 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);
@@ -2106,7 +2175,14 @@ 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 folio the write covers only partly is left
+ * unmarked, so that writing back this write's part
+ * does not drop it before a neighbouring write has
+ * written the rest.
+ */
+ 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] 7+ messages in thread
* [PATCH v9 5/6] mm, iomap: let an uncached writer complete its shared pages itself
2026-10-24 22:11 [PATCH v9 0/6] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
` (3 preceding siblings ...)
2026-10-24 22:11 ` [PATCH v9 4/6] mm, iomap: keep a partly covered page until an uncached write completes it Mike Snitzer
@ 2026-10-24 22:11 ` Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 6/6] NFSD: drop a shared boundary page only once both WRITEs have written it Mike Snitzer
5 siblings, 0 replies; 7+ messages in thread
From: Mike Snitzer @ 2026-10-24 22:11 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 write that completes a page an uncached write covers only partly
marks it drop-behind as it writes, while the page is dirty, so the page
counts as DONTCACHE dirty and the DONTCACHE writeback kick writes it
back. A writer that syncs its write itself, once, after several
segments -- NFSD answering a stable WRITE, the NFS client's LOCALIO path
-- issues each segment without IOCB_DSYNC, so every segment's kick
writes back pages its own sync is about to write. Measured with NFSD
DIRECT and 32 writers of 47008-byte records: with the previous patch
alone, 23,000 to 65,000 kick rounds a run, where NFSD marking the pages
itself after its sync had taken 51 to 274; with this patch and NFSD
using it, a FILE_SYNC WRITE wakes the kick not at all.
Let such a writer complete those pages itself. IOCB_DONTCACHE_DEFER
(IOMAP_DONTCACHE_DEFER, FGP_DONTCACHE_DEFER) leaves a partly covered page
unmarked even when the write finds it uptodate. Right before the writer
writes such a page, it calls filemap_dontcache_edge_completes() with the
range that this write covers in the page. The helper locks the folio and
asks folio_dontcache_edge_written() whether the neighbouring write wrote
its part. If it did, this write completes the page. When the write and
its sync finish, filemap_dontcache_edge_complete() completes the page
under the folio lock:
- It first waits for writeback that is in flight. That bio has no mark,
so its completion would not drop the folio.
- It marks a dirty page, which then counts as DONTCACHE dirty, and wakes
the DONTCACHE writeback kick. The kick from generic_write_sync() came
before the mark.
- It drops a clean page that is not mapped at once, without a mark.
filemap_end_dropbehind() would clear such a mark outside the i_pages
lock. A store through a mapping in between could count the folio as
DONTCACHE dirty, and nothing would uncount it.
- It marks a clean page that is mapped, for the writeback that cleans a
later store.
The helper drops the page before it releases the folio lock. A drop
after the unlock, with folio_end_dropbehind()'s trylock, would leave a
page that another task locks in between for reclaim. As in the default
path, both helpers take only a folio of the smallest size the mapping
allows, and leave a larger one cached.
Two writes that arrive together may both find the page absent before
either has written; that page is then left cached until reclaim, which
the folio-lock ordering of the default path avoids.
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on the v8 review questions, 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. The
folio has no mark, so it has no WB_DONTCACHE_DIRTY count to lose.
The helper marks only a dirty folio, or a mapped clean folio, with
folio_mark_dropbehind() (3/6).
- Blocks smaller than the folio. filemap_dontcache_edge_completes()
gets the folio with FGP_LOCK and calls
folio_dontcache_edge_written() (4/6) with the range of this write.
FGP_DONTCACHE prevents the lookup from clearing a mark.
- Two writes that arrive together. The commit message states this
limit: the page stays cached until reclaim. Data is not affected.
- 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 | 8 +++-
include/linux/iomap.h | 1 +
include/linux/pagemap.h | 7 ++++
mm/filemap.c | 92 ++++++++++++++++++++++++++++++++++++++++-
6 files changed, 112 insertions(+), 5 deletions(-)
diff --git a/fs/iomap/buffered-io.c b/fs/iomap/buffered-io.c
index eb1f7f019a852..e60a6ec5a1aa3 100644
--- a/fs/iomap/buffered-io.c
+++ b/fs/iomap/buffered-io.c
@@ -817,6 +817,8 @@ struct folio *iomap_get_folio(struct iomap_iter *iter, loff_t pos, size_t len)
*/
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);
}
}
@@ -829,7 +831,7 @@ struct folio *iomap_get_folio(struct iomap_iter *iter, loff_t pos, size_t len)
* smaller than the folio, the edge this write completes is not
* uptodate yet: mark it if its other blocks are.
*/
- 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);
@@ -1334,6 +1336,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 f9d1e05e8ae68..ad181b6ce6b4f 100644
--- a/include/linux/fs.h
+++ b/include/linux/fs.h
@@ -357,6 +357,11 @@ 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: the caller completes the pages the write covers only
+ * partly itself, see filemap_dontcache_edge_complete().
+ */
+#define IOCB_DONTCACHE_DEFER (1 << 24)
/* for use in trace events */
#define TRACE_IOCB_STRINGS \
@@ -374,7 +379,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 bc7ae6327dbf8..f3509d495a92a 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 0b151c1201082..73c906e782b7e 100644
--- a/include/linux/pagemap.h
+++ b/include/linux/pagemap.h
@@ -705,6 +705,9 @@ pgoff_t page_cache_prev_miss(struct address_space *mapping,
* only partly, and a neighbouring write may complete it. A folio created
* for it is not marked drop-behind; a folio found uptodate is, as the
* write that completes it.
+ * * %FGP_DONTCACHE_DEFER - With %FGP_DONTCACHE_EDGE: the caller completes the
+ * page itself (filemap_dontcache_edge_complete()), so a folio found uptodate
+ * is not marked either.
* * %FGP_WRITEBEGIN - The flags to use in a filesystem write_begin()
* implementation.
*/
@@ -720,6 +723,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)
@@ -1317,6 +1321,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 d7bbea442a019..be9997aa5ac48 100644
--- a/mm/filemap.c
+++ b/mm/filemap.c
@@ -1742,6 +1742,94 @@ 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 page?
+ * @mapping: the file's mapping
+ * @pos: file offset of the first byte the write covers in the page
+ * @len: bytes the write covers in the page
+ *
+ * For an uncached writer that completes the pages it covers only partly
+ * itself (IOCB_DONTCACHE_DEFER). Call right before writing such a page: it
+ * is in the page cache, with the neighbouring write's part written, when
+ * that write came first, and this write then completes it (see
+ * folio_dontcache_edge_written()). Two writes that arrive together may both
+ * find it absent. The page is then left cached, unmarked, until reclaim.
+ *
+ * Return: true if the caller is to call filemap_dontcache_edge_complete()
+ * once its write, including 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: a plain lookup would clear a drop-behind mark.
+ * FGP_LOCK: the per-block state of the folio is read under its lock.
+ */
+ 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 shared page both writes have written
+ * @mapping: the file's mapping
+ * @pos: any byte offset within the page
+ *
+ * A page already written back is dropped now. A dirty page is marked
+ * drop-behind, counts as DONTCACHE dirty, and the DONTCACHE writeback kick
+ * is woken, so the writeback that cleans it drops it. A caller that syncs
+ * its write calls this after the sync. A page that is clean but mapped is
+ * marked for the writeback that cleans a later store through the mapping.
+ * A folio larger than the smallest the mapping allows is left as it is: it
+ * holds more than the two writes' page, and it cannot be dropped in part.
+ */
+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;
+ /*
+ * A writeback already in flight was submitted without the mark, and
+ * its completion would not drop the folio. Wait for 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, with the lock still held, and without a mark.
+ * A mark set here and cleared again outside the i_pages lock
+ * could miss a store through a mapping that counts the folio
+ * as DONTCACHE dirty in between.
+ */
+ 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.
@@ -2135,8 +2223,8 @@ struct folio *__filemap_get_folio_mpol(struct address_space *mapping,
* more than they wrote and cannot be dropped in part.
*/
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] 7+ messages in thread
* [PATCH v9 6/6] NFSD: drop a shared boundary page only once both WRITEs have written it
2026-10-24 22:11 [PATCH v9 0/6] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
` (4 preceding siblings ...)
2026-10-24 22:11 ` [PATCH v9 5/6] mm, iomap: let an uncached writer complete its shared pages itself Mike Snitzer
@ 2026-10-24 22:11 ` Mike Snitzer
5 siblings, 0 replies; 7+ messages in thread
From: Mike Snitzer @ 2026-10-24 22:11 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
A buffered DONTCACHE segment of a direct-mode WRITE -- the misaligned
start or end of a split WRITE, or the whole of one too small to split --
covers its first and last unit only partly when it does not start or end
on a unit boundary. The unit is the smallest folio the mapping allows: a
page, or a block where the block is larger than a page. Such a unit is
shared with the neighbouring WRITE, which may arrive before, after or at
the same time, from any client.
The page cache keeps a page an uncached write covers only partly for the
write that completes it ("mm, iomap: keep a partly covered page until an
uncached write completes it"), so the second WRITE does not have to read
it back from disk. Left at that, the page cache marks the page drop-
behind while it is still dirty, which counts it as DONTCACHE dirty and
wakes the DONTCACHE writeback kick for every segment of a stable WRITE,
only to write back what NFSD's single fsync is about to write.
Issue the buffered segments IOCB_DONTCACHE_DEFER and mark the pages here
instead ("mm, iomap: let an uncached writer complete its shared pages
itself"). Immediately before writing a segment, NFSD asks
filemap_dontcache_edge_completes() whether the neighbouring WRITE wrote
its part of each unit that the segment covers only partly. It passes the
range that the segment covers in that unit. Once the whole WRITE, its
fsync included, is complete, it calls filemap_dontcache_edge_complete()
for those units, which drops a clean one at once. A short write that
stopped inside the segment's first unit has not written its part of that
edge, so NFSD completes the first edge only when the write reached the
end of the segment's bytes in that unit. An UNSTABLE WRITE marks the
units dirty, and the writeback that cleans them, by the flusher or for
the client's COMMIT, drops them.
A page whose neighbouring WRITE never arrives, such as the last page of
the file, or arrives at the same time, so that neither finds the other's
part, stays cached until it is reclaimed. Nothing changes when the
buffered segments are normal buffered I/O (direct_misaligned_dontcache
is N, or the file system lacks FOP_DONTCACHE); the page cache keeps
partly covered pages only for file systems that use iomap for buffered
writes.
Measured with 32 interleaved writers of 47008-byte records over an
emulated 4Kn NVMe with 4 KiB pages, NFSD DIRECT with
direct_misaligned_dontcache=Y, each run from a freshly made file system:
device reads per WRITE are 0.0001-0.0002 whether each record is one
FILE_SYNC WRITE, two sent one after the other, two sent together, or two
sent UNSTABLE, against 45144 reads for 45664 WRITEs with no shared-page
handling, and the DONTCACHE writeback kick does not run at all for the
FILE_SYNC WRITEs.
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, keeping the shared page took device reads from 0.014-0.015 per
record to 0.003, page-cache growth over the write phase from about
11 GiB to 2.9 GiB, and write throughput up by 2.4% to 4.9%, 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>
Signed-off-by: Mike Snitzer <snitzer@kernel.org>
---
Notes:
Notes on the v8 review questions, with the code that answers each:
- Unit size. nfsd_write_dio_seg_hold() uses
mapping_min_folio_nrbytes(), as iomap_get_folio() does (4/6). A
block larger than the page is one unit in both places.
- Short write. NFSD completes the first edge only if written is at
least first_len. It completes the last edge only if the write
covered the whole segment.
- Release after an error. NFSD does this 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
contain 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 first_len is not set.
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]).
- Two WRITEs that arrive together. The commit message states this
limit: the page stays cached until reclaim.
.../filesystems/nfs/nfsd-io-modes.rst | 17 +++
fs/nfsd/vfs.c | 116 ++++++++++++++++--
2 files changed, 121 insertions(+), 12 deletions(-)
diff --git a/Documentation/filesystems/nfs/nfsd-io-modes.rst b/Documentation/filesystems/nfs/nfsd-io-modes.rst
index 0e69b5fdd6756..9c663ce97c48f 100644
--- a/Documentation/filesystems/nfs/nfsd-io-modes.rst
+++ b/Documentation/filesystems/nfs/nfsd-io-modes.rst
@@ -167,6 +167,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 9219f8eb386d0..0b62c4659e547 100644
--- a/fs/nfsd/vfs.c
+++ b/fs/nfsd/vfs.c
@@ -1387,6 +1387,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
@@ -1406,6 +1407,82 @@ 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 unit two writes share is the smallest folio the mapping allows:
+ * a page, or a block where the block is larger. Only a unit the
+ * segment covers partly is shared.
+ */
+ 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
@@ -1428,7 +1505,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.
@@ -1483,7 +1562,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,
@@ -1516,7 +1596,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;
@@ -1525,6 +1606,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;
}
@@ -1533,12 +1615,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,
@@ -1567,23 +1650,32 @@ 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;
+ /*
+ * 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] 7+ messages in thread
end of thread, other threads:[~2026-10-24 22:12 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-24 22:11 [PATCH v9 0/6] mm, iomap, NFSD: stop uncached writes reading back shared pages Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 1/6] mm: take a folio off WB_DONTCACHE_DIRTY only if it was counted there Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 2/6] mm: expose WB_DONTCACHE_DIRTY in bdi debugfs stats Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 3/6] mm: add folio_mark_dropbehind() to mark a folio dropbehind and account it Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 4/6] mm, iomap: keep a partly covered page until an uncached write completes it Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 5/6] mm, iomap: let an uncached writer complete its shared pages itself Mike Snitzer
2026-10-24 22:11 ` [PATCH v9 6/6] NFSD: drop a shared boundary page only once both WRITEs have written it Mike Snitzer
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox