Linux filesystem development
 help / color / mirror / Atom feed
From: "Kiryl Shutsemau (Meta)" <kas@kernel.org>
To: Andrew Morton <akpm@linux-foundation.org>,
	Linus Torvalds <torvalds@linux-foundation.org>,
	David Hildenbrand <david@kernel.org>,
	Matthew Wilcox <willy@infradead.org>,
	Vlastimil Babka <vbabka@kernel.org>,
	Lorenzo Stoakes <ljs@kernel.org>, Jan Kara <jack@suse.cz>,
	Hugh Dickins <hughd@google.com>,
	Christian Brauner <brauner@kernel.org>,
	Alexander Viro <viro@zeniv.linux.org.uk>,
	Harry Yoo <harry@kernel.org>
Cc: "Kiryl Shutsemau (Meta)" <kas@kernel.org>,
	Miklos Szeredi <miklos@szeredi.hu>,
	Miaohe Lin <linmiaohe@huawei.com>,
	Naoya Horiguchi <nao.horiguchi@gmail.com>,
	Oscar Salvador <osalvador@suse.de>,
	"Liam R. Howlett" <liam@infradead.org>,
	Mike Rapoport <rppt@kernel.org>,
	Suren Baghdasaryan <surenb@google.com>,
	Michal Hocko <mhocko@suse.com>,
	Brendan Jackman <brendan.jackman@linux.dev>,
	Johannes Weiner <hannes@cmpxchg.org>, Zi Yan <ziy@nvidia.com>,
	Baolin Wang <baolin.wang@linux.alibaba.com>,
	Nico Pache <nico.pache@linux.dev>,
	Ryan Roberts <ryan.roberts@arm.com>, Dev Jain <dev.jain@arm.com>,
	Barry Song <baohua@kernel.org>, Lance Yang <lance.yang@linux.dev>,
	Usama Arif <usama.arif@linux.dev>,
	Kairui Song <kasong@tencent.com>, Qi Zheng <qi.zheng@linux.dev>,
	Shakeel Butt <shakeel.butt@linux.dev>,
	Axel Rasmussen <axelrasmussen@google.com>,
	Yuanchu Xie <yuanchu@google.com>, Wei Xu <weixugc@google.com>,
	Baoquan He <baoquan.he@linux.dev>,
	"Paul E. McKenney" <paulmck@kernel.org>,
	Pedro Falcato <pfalcato@suse.de>,
	Luis Chamberlain <mcgrof@kernel.org>,
	Ilya Gladyshev <ilya.gladyshev@linux.dev>,
	Jaegeuk Kim <jaegeuk@kernel.org>,
	linux-mm@kvack.org, linux-kernel@vger.kernel.org,
	linux-fsdevel@vger.kernel.org, fuse-devel@lists.linux.dev,
	kernel-team@meta.com
Subject: [RFC PATCH v2 00/18] mm: read the page cache without folio references
Date: Wed,  4 Nov 2026 13:00:56 +0000	[thread overview]
Message-ID: <20261104130118.2507513-1-kas@kernel.org> (raw)

Small reads of one file from many CPUs all take and drop a reference on
the same folio, and its refcount bounces between the CPUs.  This series
lets filemap_read() and shmem copy from the page cache without taking a
reference.  It is Linus' RFC [1], reworked: a folio that such a reader
may copy from is freed only after an RCU grace period.

Only the folios that reads keep coming back to get the flag,
PG_rcu_free: those that folio_mark_accessed() finds as accessed as the
LRU records.  Streaming reads and data that never gets hot are not
deferred, so the memory held back stays small and most frees are not
touched.

Who gains: programs that read the same few pages with read() from many
threads and keep no cache of their own.  Most programs that care about
read performance cache hot data themselves and never get here; of the
real workloads I tried, a kernel build, pgbench and SQLite do not
change.  A read still pays for the system call, so this does not replace
a userspace cache.  It does keep the page cache from collapsing under
many readers, which may let some programs keep a smaller cache of their
own, or none.

  1      PG_rcu_free and the deferred free (64-bit, not PREEMPT_RT)
  2-3    splice and fuse no longer reuse page cache folios that a
         lockless reader may still copy from
  4-8    NR_FREE_RCU_PAGES; an expedited grace period when a CPU's
         deferred frees pile up and memory runs short; the waiting
         pages are freed before the allocator, alloc_contig_range() or
         memory offline give up, and when compaction empties a block
  9      memory_failure() and soft offline deal with such pages
  10-12  filemap_read() marks a folio accessed in one place and sets
         PG_rcu_free on the folios that reads keep coming back to
  13     filemap_read() copies small folios without a reference
  14     ... and large folios, with one more lookup per folio
  15     no f_ra.prev_pos store for FMODE_RANDOM files after such a read
  16-17  shmem does the same
  18     a debugfs switch to turn the reads off

Numbers: 64-vCPU KVM guest on a Threadripper PRO 7975WX (Zen 4), no
debug options, base v7.3, both kernels in the same session, 3 rounds.
With the default mitigations every read here pays ~400 ns at the
user/kernel boundary (TSA, SRSO), which holds one thread to ~1.8M
reads/s; Linus' numbers with mitigations off are in [1].

  16-byte pread() at offset 0, each thread with its own fd, M reads/s:
                                 base           this series
    ext4, 64 threads             17.1-18.1      68.7-71.0
    tmpfs, 64 threads            1.5-1.9        68.3-70.5
    block dev, 64 threads        14.7-14.9      28.3-28.9
    1 thread, any of them        1.6-1.8        1.7-1.9

  Base tmpfs takes the folio lock on every read: 64 threads read as
  fast as one.  The block device takes i_rwsem shared on every read
  (inode_lock_shared() in blkdev_read_iter()), and its count bounces in
  place of the refcount.

  RocksDB readrandom on a hot range (db_bench, 100 keys, 4K blocks, no
  block cache; the threads share one fd per table file, opened with
  POSIX_FADV_RANDOM), 64 threads, M ops/s:
                                 base           this series
    ext4                         10.6-10.9      14.2-14.7
    XFS                          8.9            12.2-12.4

  At 16 threads ext4 is within 5% of base, XFS at 0.89 of it by median,
  with the series spread over 5.7-6.5 against 6.2-6.4.  16-byte reads
  through one shared fd at 64 threads: 1.20x base at one offset, 0.97x
  on random pages, where the struct file's cache line takes over from
  the folio refcount.

  With a block cache RocksDB reads the page cache no more: its
  HyperClockCache does 26.5M ops/s at 64 threads on either kernel, 1.8x
  the series without a cache.  Its LRUCache does 3.5M on this hot range,
  below base without a cache.

  Under memory pressure no load got an OOM kill.  pgbench in an 8G
  guest runs at 0.92-0.97 of base.  A stress load that keeps rereading
  and dropping 1G of hot data next to a memory hog runs at 0.88; it is
  there to push flagged folios through the deferred free.

Open:

- Patch 17 drops the folio lock from tmpfs reads.  shmem_get_folio()
  held it only for the lookup, so a read was never atomic against a
  write or a hole punch; now a read also does not wait for one that
  holds the lock, as filemap_read() does not.  Fallocated folios stay
  hidden: they are not uptodate, and only uptodate folios get the flag.
  A race test of reads against fallocate, its undo, hole punch, swap,
  collapse and migration found no bad data.  I would still like Hugh to
  double check.
- Patch 16 makes tmpfs reads store f_ra.prev_pos, as filemap_read()
  does.  tmpfs ignores POSIX_FADV_RANDOM, so threads that read through
  one tmpfs fd cannot skip that store.  Not measured.
- A deferred folio is uncharged from its memcg before it waits, as every
  page cache folio is.  For a grace period it counts towards no cgroup.
- defrag_mode: a stress test that leaves free pages in a fixed pattern
  makes compaction fail and the allocator stall in direct reclaim, on
  base too.  That is a compaction bug; [3] fixes it.  On that layout the
  series stalls badly more often, 5 runs in 10 against 1, and I do not
  know why yet; the series with [3] is not measured.  Patch 8 cuts
  compaction's scanning there, but not the stalls.
- The flag is a page flag, 64-bit only.  On large configs it costs MGLRU
  one refs bit.
- On writably mapped files, architectures that implement
  flush_dcache_page() take the slow path.
- filemap_mark_hot() still tests uptodate.  Willy pointed out that a
  filesystem clearing it under a reader is a filesystem bug; the test is
  cheap.

Changes since v1 [2]:

- Dropped v1 patch 12, which cleared the flag on the folios of an inode
  being evicted (suggested by Linus).  Sashiko found an ABA: a reader
  holding a stale pointer can see the folio move to another inode, get
  the flag there, lose it when that inode is evicted, and be reused in
  the slot it looked at.  Deleting or evicting a hot file defers its
  frees again.  The f2fs fix it depended on is no longer needed.
- Replaced v1 patch 13, which stored f_ra.prev_pos only when it
  changed: as Linus pointed out, it never stays the same in a real load,
  and the compare cost RocksDB 4-5%.  Patch 15 instead skips the store
  for FMODE_RANDOM files after a read done without folio references.
- New patch 8: compaction frees the migrated folios that wait for a
  grace period before it checks whether it is done; async compaction
  does not.
- PAGE_RCU_FREE is off on PREEMPT_RT, where expedited grace periods are
  normal ones (patch 1).
- Patch 6: the drain before OOM frees to the buddy lists, counts what
  reclaim can still free, checks the watermark at order 0, and waits
  for expedites in flight (Sashiko).
- Patch 9: memory_failure() frees the waiting pages on every retry in
  get_any_page(), and waits for readers on the already-poisoned path
  (Sashiko).
- Patches 13-14: races with a split (Sashiko).  An order-0 folio is
  copied only at its own index; a large folio's order is clamped and
  PG_has_hwpoisoned read without the PG_head assertion.
- Patch 13: the folio batch is declared in the slow-path loop, so stack
  auto-initialisation does not zero it on reads done without references.
- Patch 16: shmem stores f_ra.prev_pos as filemap_read() does.
- v1 carried Assisted-by trailers; they are gone, so every patch shows
  "!" in the range-diff below.

The series is also at:
  https://git.kernel.org/pub/scm/linux/kernel/git/kas/linux.git rcu-free/rfc-v2

[1] https://lore.kernel.org/all/CAHk-=wjLjgfwgSZ1RDk4YdNFcV0ntosMO5AmBoHD76Zb3HFY7Q@mail.gmail.com/
[2] https://lore.kernel.org/all/20261019154913.1765974-1-kas@kernel.org/
[3] https://lore.kernel.org/all/20261104101747.2485913-1-kas@kernel.org/

Kiryl Shutsemau (Meta) (11):
  fuse: don't move a folio with PG_rcu_free into a fuse file
  mm: count the pages waiting for a grace period
  mm: free the pages waiting for a grace period before OOM
  mm: free the pages waiting for a grace period before isolation fails
  mm/compaction: free the migrated folios that wait for a grace period
  mm/memory-failure: handle folios with PG_rcu_free
  mm/filemap: read large folios without folio references
  mm/filemap: don't store f_ra.prev_pos for FMODE_RANDOM files
  mm/shmem: count accesses to a folio the way filemap_read() does
  mm/shmem: read without folio references
  mm/filemap: add a debugfs switch for lockless page cache reads

Linus Torvalds (7):
  mm: free PG_rcu_free folios only after an RCU grace period
  splice: don't steal page cache folios
  mm: expedite the grace period when a CPU has many deferred frees
  mm: have folio_mark_accessed() return whether the folio is hot
  mm/filemap: mark a folio accessed in one place in filemap_read()
  mm/filemap: set PG_rcu_free on the folios that reads keep coming back
    to
  mm/filemap: read from the page cache without folio references

 fs/fuse/dev.c                  |   4 +
 fs/splice.c                    |  51 -----
 include/linux/mmzone.h         |   1 +
 include/linux/page-flags.h     |  21 ++
 include/linux/pagemap.h        |  12 ++
 include/linux/swap.h           |   2 +-
 include/linux/vm_event_item.h  |   2 +
 include/trace/events/mmflags.h |   7 +
 mm/Kconfig                     |  18 ++
 mm/compaction.c                |   8 +
 mm/filemap.c                   | 346 +++++++++++++++++++++++++++++++--
 mm/folio.c                     |  46 +++--
 mm/huge_memory.c               |   1 +
 mm/internal.h                  |   3 +
 mm/memory-failure.c            |  33 ++++
 mm/memory_hotplug.c            |   6 +
 mm/page_alloc.c                | 297 +++++++++++++++++++++++++++-
 mm/page_alloc.h                |   9 +
 mm/shmem.c                     |  20 +-
 mm/show_mem.c                  |   9 +-
 mm/vmstat.c                    |   6 +
 21 files changed, 808 insertions(+), 94 deletions(-)

Range-diff against v1:
 1:  80d1ea6c758a !  1:  4ef64163f1a6 mm: free PG_rcu_free folios only after an RCU grace period
    @@ Commit message
     
         The flag is 64-bit only (CONFIG_PAGE_RCU_FREE): page flags are scarce
         on 32-bit, and the contention this avoids is a problem of large
    -    machines.
    +    machines.  It is off on PREEMPT_RT, where expedited grace periods are
    +    normal ones, so the waiting pages cannot be freed quickly when memory
    +    runs short.
     
         pgfree_rcu in /proc/vmstat counts the deferred pages.  Nothing sets the
         flag yet.
     
    -    Assisted-by: LLM
         Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
    -    [kas: rework for flagging at read time, 64-bit only, shorter comments
    -     and changelog]
    +    [kas: rework for flagging at read time, 64-bit only and not PREEMPT_RT,
    +     shorter comments and changelog]
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## include/linux/page-flags.h ##
    @@ mm/Kconfig: config PCP_BATCH_SCALE_MAX
      	  the maximum latency.
      
     +config PAGE_RCU_FREE
    -+	def_bool 64BIT
    ++	def_bool y
    ++	depends on 64BIT
    ++	depends on !PREEMPT_RT
     +	help
     +	  Reads from the page cache can copy from folios that are read
     +	  again and again without taking a reference on them, so small
    @@ mm/Kconfig: config PCP_BATCH_SCALE_MAX
     +	  folios becomes reusable only after an RCU grace period.
     +
     +	  64-bit only: it takes a page flag, and those are scarce on
    -+	  32-bit.
    ++	  32-bit.  Not on PREEMPT_RT, where expedited grace periods are
    ++	  normal ones, so freeing the waiting pages on demand would stall.
     +
      config PHYS_ADDR_T_64BIT
      	def_bool 64BIT
 2:  54ae7580bf73 !  2:  1cfc33d7be6e splice: don't steal page cache folios
    @@ Commit message
         stealing fails.  SPLICE_F_MOVE from the page cache into a fuse file or
         a virtio console now copies.
     
    -    Assisted-by: LLM
         Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
         [kas: rewrote the changelog]
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
 3:  7a4a1df99f88 !  3:  6f1f2faa2ee2 fuse: don't move a folio with PG_rcu_free into a fuse file
    @@ Commit message
     
         Copy such a folio instead, as fuse does whenever it cannot take one.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## fs/fuse/dev.c ##
 4:  226465c86d51 !  4:  b4b2afbb5dbf mm: count the pages waiting for a grace period
    @@ Commit message
         free_rcu in show_free_areas().  Preparation for freeing the waiting
         pages sooner when they hold back memory that is needed.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## include/linux/mmzone.h ##
 5:  601a788a3c9d !  5:  1114d57c872f mm: expedite the grace period when a CPU has many deferred frees
    @@ Commit message
         zone still has memory.  When the readers that refill the range then run
         short, nothing is freed that would check again.
     
    -    With rcu_normal, as on PREEMPT_RT, the expedited grace period is a
    -    normal one.
    +    With rcu_normal, the expedited grace period is a normal one.
     
         pgfree_rcu_expedite in /proc/vmstat counts the expedite runs that freed
         something.
     
    -    Assisted-by: LLM
         Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
         [kas: take both lists, expedite only when the memory matters, rewrote
          the changelog]
 6:  d13fd5787f06 !  6:  f9063909ef1e mm: free the pages waiting for a grace period before OOM
    @@ Commit message
         grace period.
     
         Before should_reclaim_retry() gives up, free the waiting pages if they
    -    would bring an eligible zone above its min watermark, and retry.  That
    -    costs an expedited grace period, which is cheap next to the OOM killer.
    -    The retries are bounded per no-progress streak, so a drain that does not
    -    help still ends in an OOM kill.
    +    and what reclaim can still free would bring an eligible zone above its
    +    min watermark, and retry.  The drain frees them straight to the buddy
    +    lists, where the retry's watermark check sees them, and waits for the
    +    expedites already in flight.  That costs an expedited grace period,
    +    which is cheap next to the OOM killer.  The retries are bounded per
    +    no-progress streak, so a drain that does not help still ends in an OOM
    +    kill.
     
         pgfree_rcu_drain in /proc/vmstat counts the drains that freed something.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## include/linux/vm_event_item.h ##
    @@ include/linux/vm_event_item.h: enum vm_event_item { PGPGIN, PGPGOUT, PSWPIN, PSW
      		PGREUSE,
     
      ## mm/page_alloc.c ##
    +@@ mm/page_alloc.c: static void rcu_free_take(struct rcu_free_pcpu *r, struct list_head *list)
    + 	r->nr_pending = 0;
    + }
    + 
    +-static unsigned long rcu_free_pages(struct list_head *list)
    ++static unsigned long rcu_free_pages(struct list_head *list, bool direct)
    + {
    + 	struct folio_batch fbatch;
    + 	struct folio *folio, *next;
    +@@ mm/page_alloc.c: static unsigned long rcu_free_pages(struct list_head *list)
    + 		nr += folio_nr_pages(folio);
    + 		mod_zone_page_state(folio_zone(folio), NR_FREE_RCU_PAGES,
    + 				    -folio_nr_pages(folio));
    +-		if (!folio_batch_add(&fbatch, folio)) {
    ++		if (direct) {
    ++			__free_pages_ok(&folio->page, folio_order(folio),
    ++					FPI_NONE);
    ++			cond_resched();
    ++		} else if (!folio_batch_add(&fbatch, folio)) {
    + 			free_unref_folios(&fbatch);
    + 			cond_resched_tasks_rcu_qs();
    + 		}
    +@@ mm/page_alloc.c: static unsigned long rcu_free_pages(struct list_head *list)
    + 	return nr;
    + }
    + 
    +-/* Free what rcu_free_take() took: the grace period starts after that */
    +-static unsigned long rcu_free_flush(struct list_head *list)
    ++/*
    ++ * Free what rcu_free_take() took: the grace period starts after that.
    ++ * @direct frees to the buddy lists instead of this CPU's pcp lists.
    ++ */
    ++static unsigned long rcu_free_flush(struct list_head *list, bool direct)
    + {
    + 	if (list_empty(list))
    + 		return 0;
    + 
    + 	synchronize_rcu_expedited();
    +-	return rcu_free_pages(list);
    ++	return rcu_free_pages(list, direct);
    + }
    + 
    + static void rcu_free_workfn(struct work_struct *work)
    +@@ mm/page_alloc.c: static void rcu_free_workfn(struct work_struct *work)
    + 	rcu_free_start(r);
    + 	spin_unlock_irq(&r->lock);
    + 
    +-	rcu_free_pages(&done);
    ++	rcu_free_pages(&done, false);
    + }
    + 
    + static void rcu_free_expedite_workfn(struct work_struct *work)
    +@@ mm/page_alloc.c: static void rcu_free_expedite_workfn(struct work_struct *work)
    + 	rcu_free_take(r, &list);
    + 	spin_unlock_irq(&r->lock);
    + 
    +-	if (rcu_free_flush(&list))
    ++	if (rcu_free_flush(&list, false))
    + 		count_vm_event(PGFREE_RCU_EXPEDITE);
    + }
    + 
     @@ mm/page_alloc.c: static void rcu_free_defer(struct folio *folio)
      		queue_work(rcu_free_wq, &r->expedite);
      }
      
     +/*
    -+ * Free what every CPU's lists hold.  A concurrent drain holds the lock
    -+ * until it has freed what it took.
    ++ * Free what every CPU's lists hold, straight to the buddy lists: the
    ++ * callers want to allocate the pages next.  A concurrent drain holds the
    ++ * lock until it has freed what it took.
     + */
     +static void rcu_free_drain(void)
     +{
    @@ mm/page_alloc.c: static void rcu_free_defer(struct folio *folio)
     +		rcu_free_take(r, &list);
     +		spin_unlock_irq(&r->lock);
     +	}
    -+	if (rcu_free_flush(&list))
    ++	if (rcu_free_flush(&list, true))
     +		count_vm_event(PGFREE_RCU_DRAIN);
    ++	/* An expedite in flight holds pages it took off the lists */
    ++	for_each_possible_cpu(cpu)
    ++		flush_work(&per_cpu_ptr(&rcu_free_pcpu, cpu)->expedite);
     +	mutex_unlock(&lock);
     +}
     +
    @@ mm/page_alloc.c: bool gfp_pfmemalloc_allowed(gfp_t gfp_mask)
      }
      
     +/*
    -+ * Reclaim is about to give up: if the pages waiting for a grace period
    -+ * would bring an eligible zone above its min watermark, free them and
    -+ * retry.  Not for a dying task, which frees memory by exiting.
    ++ * Reclaim is about to give up: if the pages waiting for a grace period,
    ++ * with what reclaim can still free, would bring an eligible zone above its
    ++ * min watermark, free them and retry.  Not for a dying task, which frees
    ++ * memory by exiting.
     + */
     +static bool rcu_free_wait(gfp_t gfp_mask, unsigned int order,
     +			  struct alloc_context *ac, int alloc_flags,
    @@ mm/page_alloc.c: bool gfp_pfmemalloc_allowed(gfp_t gfp_mask)
     +		if (!waiting)
     +			continue;
     +
    -+		available = waiting +
    ++		available = waiting + zone_reclaimable_pages(zone) +
     +			    zone_page_state_snapshot(zone, NR_FREE_PAGES);
    -+		if (__zone_watermark_ok(zone, order, min_wmark_pages(zone),
    ++		/*
    ++		 * Count pages only: for order > 0 __zone_watermark_ok() also
    ++		 * wants a free block of that order, and the waiting pages are
    ++		 * not free yet.
    ++		 */
    ++		if (__zone_watermark_ok(zone, 0, min_wmark_pages(zone),
     +					ac->highest_zoneidx, alloc_flags,
     +					available)) {
     +			(*rcu_free_waits)++;
 7:  61b797a643a3 !  7:  1058885aa5a9 mm: free the pages waiting for a grace period before isolation fails
    @@ Commit message
         offline stuck on a busy page would otherwise start an expedited grace
         period on every retry.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## mm/memory_hotplug.c ##
    @@ mm/memory_hotplug.c: int offline_pages(unsigned long start_pfn, unsigned long nr
      		ret = test_pages_isolated(start_pfn, end_pfn,
      					  PB_ISOLATE_MODE_MEM_OFFLINE);
     +		/* What this pass migrated can wait for a grace period */
    -+		if (ret && migrated &&
    -+		    zone_page_state_snapshot(zone, NR_FREE_RCU_PAGES))
    ++		if (ret && migrated && zone_rcu_free_waiting(zone))
     +			rcu_free_sync();
      
      	} while (ret);
    @@ mm/page_alloc.c: static void rcu_free_drain(void)
     +		return;
     +
     +	rcu_free_drain();
    -+	for_each_possible_cpu(cpu) {
    -+		struct rcu_free_pcpu *r = per_cpu_ptr(&rcu_free_pcpu, cpu);
    -+
    -+		flush_work(&r->rwork.work);
    -+		flush_work(&r->expedite);
    -+	}
    ++	for_each_possible_cpu(cpu)
    ++		flush_work(&per_cpu_ptr(&rcu_free_pcpu, cpu)->rwork.work);
     +}
     +
      static void __init rcu_free_init(void)
    @@ mm/page_alloc.c: int alloc_contig_frozen_range_noprof(unsigned long start, unsig
      	/* Make sure the range is really isolated. */
     -	if (test_pages_isolated(outer_start, end, mode)) {
     +	ret = test_pages_isolated(outer_start, end, mode);
    -+	if (ret && zone_page_state_snapshot(cc.zone, NR_FREE_RCU_PAGES)) {
    ++	if (ret && zone_rcu_free_waiting(cc.zone)) {
     +		rcu_free_sync();
     +
     +		/* A freed folio can merge with a free buddy below start */
    @@ mm/page_alloc.h: struct page *__alloc_frozen_pages_noprof(gfp_t gfp, unsigned in
      void free_frozen_pages(struct page *page, unsigned int order);
      void free_unref_folios(struct folio_batch *fbatch);
     +void rcu_free_sync(void);
    ++
    ++/* Whether @zone has pages waiting for a grace period */
    ++static inline bool zone_rcu_free_waiting(struct zone *zone)
    ++{
    ++	return IS_ENABLED(CONFIG_PAGE_RCU_FREE) &&
    ++	       zone_page_state_snapshot(zone, NR_FREE_RCU_PAGES);
    ++}
      
      #ifdef CONFIG_NUMA
      struct page *alloc_frozen_pages_noprof(gfp_t, unsigned int order);
 -:  ------------ >  8:  c1fe5ee6d2a9 mm/compaction: free the migrated folios that wait for a grace period
 8:  cbcc28707563 !  9:  58f670555c22 mm/memory-failure: handle folios with PG_rcu_free
    @@ Commit message
         later freed with the poisoned page in it, and that page can be handed
         out again.
     
    -    Free the waiting pages first when the page looks like one of them.
    -    memory_failure() has to do it before it sets PG_hwpoison.  The free of a
    -    poisoned order-0 page drops the page instead of returning it to the
    -    buddy allocator, so a later drain would still leave the error ignored.
    +    Free the waiting pages first when the page looks like one of them, and
    +    again on each retry in get_any_page(): the page can be freed while it
    +    retries.  memory_failure() has to do it before it sets PG_hwpoison.  The
    +    free of a poisoned order-0 page drops the page instead of returning it
    +    to the buddy allocator, so a later drain would still leave the error
    +    ignored.
     
         A folio with PG_rcu_free that is still in use may be copied by a reader
         that holds no reference.  When memory_failure() succeeds, its caller can
    @@ Commit message
     
         Both callers hold mf_mutex and can wait for the grace period.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## mm/memory-failure.c ##
    -@@ mm/memory-failure.c: static int get_hwpoison_page(struct page *p, unsigned long flags)
    - 	return ret;
    +@@ mm/memory-failure.c: static int __get_hwpoison_page(struct page *page, unsigned long flags)
    + 	return 0;
      }
      
     +/* A page waiting for a grace period is not in the buddy yet: free it */
     +static void mf_free_rcu_pages(struct page *p)
     +{
    -+	if (!IS_ENABLED(CONFIG_PAGE_RCU_FREE))
    ++	if (!zone_rcu_free_waiting(page_zone(p)))
     +		return;
     +	if (page_count(p))
     +		return;
    -+	if (!zone_page_state_snapshot(page_zone(p), NR_FREE_RCU_PAGES))
    -+		return;
     +	if (is_free_buddy_page(p))
     +		return;
     +
     +	rcu_free_sync();
     +}
     +
    - /*
    -  * The caller must guarantee the folio isn't large folio, except hugetlb.
    -  * try_to_unmap() can't handle it.
    + #define GET_PAGE_MAX_RETRY_NUM 3
    + 
    + static int get_any_page(struct page *p, unsigned long flags)
    +@@ mm/memory-failure.c: static int get_any_page(struct page *p, unsigned long flags)
    + 	}
    + 
    + try_again:
    ++	mf_free_rcu_pages(p);
    + 	if (!count_increased) {
    + 		ret = __get_hwpoison_page(p, flags);
    + 		if (!ret) {
     @@ mm/memory-failure.c: int memory_failure(unsigned long pfn, int flags)
      	if (res != -ENOENT)
      		goto unlock_mutex;
    @@ mm/memory-failure.c: int memory_failure(unsigned long pfn, int flags)
      	if (TestSetPageHWPoison(p)) {
      		res = -EHWPOISON;
      		if (flags & MF_ACTION_REQUIRED)
    +@@ mm/memory-failure.c: int memory_failure(unsigned long pfn, int flags)
    + 		if (flags & MF_COUNT_INCREASED)
    + 			put_page(p);
    + 		action_result(pfn, MF_MSG_ALREADY_POISONED, MF_FAILED);
    ++		/*
    ++		 * The memory_failure() that set PG_hwpoison may have failed
    ++		 * without waiting for lockless readers, and the caller may
    ++		 * unmap the page once this returns 0.
    ++		 */
    ++		if (!res && folio_test_rcu_free(page_folio(p)))
    ++			synchronize_rcu();
    + 		goto unlock_mutex;
    + 	}
    + 
     @@ mm/memory-failure.c: int memory_failure(unsigned long pfn, int flags)
      
      identify_page_state:
    @@ mm/memory-failure.c: int memory_failure(unsigned long pfn, int flags)
      	mutex_unlock(&mf_mutex);
      	return res;
      unlock_page:
    -@@ mm/memory-failure.c: int soft_offline_page(unsigned long pfn, int flags)
    - 
    - retry:
    - 	get_online_mems();
    -+	mf_free_rcu_pages(page);
    - 	ret = get_hwpoison_page(page, flags | MF_SOFT_OFFLINE);
    - 	put_online_mems();
    - 
 9:  e1e5b7cd3c3e ! 10:  46d88376b778 mm: have folio_mark_accessed() return whether the folio is hot
    @@ Commit message
         No caller uses the value yet.  Preparation for reading the folios that
         it calls hot without a folio reference.
     
    -    Assisted-by: LLM
         Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
         [kas: shorter changelog]
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
10:  fcd474ea7b1c ! 11:  3afb00ad5b68 mm/filemap: mark a folio accessed in one place in filemap_read()
    @@ Commit message
         No change in behaviour intended.  Preparation for deciding in the
         helper which folios are worth reading without a reference.
     
    -    Assisted-by: LLM
         Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
         [kas: shorter changelog]
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
11:  585dd033a029 ! 12:  abe088444b40 mm/filemap: set PG_rcu_free on the folios that reads keep coming back to
    @@ Commit message
     
         pgfree_rcu_mark in /proc/vmstat counts the pages that get the flag.
     
    -    Assisted-by: LLM
         Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
         [kas: count the marks, AS_RCU_READ, 64-bit only, shorter comment,
          rewrote the changelog]
12:  34d07f67849e <  -:  ------------ mm/filemap: don't defer the free of folios of an inode being evicted
13:  5bd0abaeaa08 <  -:  ------------ mm/filemap: store the end of a read in f_ra only when it changes
14:  e9ece1540b38 ! 13:  970b0c9bb8d4 mm/filemap: read from the page cache without folio references
    @@ Commit message
         Leave to the slow path a folio that is not uptodate, needs readahead or
         dropbehind, is not hot, or has a poisoned page.  Leave large folios
         there too: looking one up for every page is slower than one reference
    -    for all of its pages.  Copy at most 512 KiB per rcu_read_lock()
    -    section, and skip files that never had a folio flagged or that are
    -    writably mapped where flush_dcache_folio() is needed.
    +    for all of its pages.  A split clears PG_head before the new folios
    +    replace the old one in their slots, so copy an order-0 folio only at its
    +    own index.  Copy at most 512 KiB per rcu_read_lock() section, and skip
    +    files that never had a folio flagged or that are writably mapped where
    +    flush_dcache_folio() is needed.
    +
    +    Declare the folio batch in the loop: with stack auto-initialisation,
    +    the compiler zeroes it where it is declared, and the copy without
    +    references does not use it.
     
    -    Assisted-by: LLM
         Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
         [kas: i_size before the second look at the slot, poisoned folio check,
    -     large folios left to the slow path, 512 KiB sections, shorter comments
    -     and changelog]
    +     large folios left to the slow path, 512 KiB sections, folio batch in
    +     the loop, shorter comments and changelog]
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## mm/filemap.c ##
    @@ mm/filemap.c: static void filemap_end_dropbehind_read(struct folio *folio)
     +		return false;
     +	if (folio_test_readahead(folio) || folio_test_dropbehind(folio))
     +		return false;
    -+	/* memory_failure() waits only for readers that got past this */
    -+	if (folio_contain_hwpoisoned_page(folio))
    -+		return false;
     +	/* One reference covers all of a large folio's pages */
     +	if (folio_test_large(folio))
     +		return false;
    ++	/* A split clears PG_head before it puts the new folios in their slots */
    ++	if (folio->index != index)
    ++		return false;
    ++	/* memory_failure() waits only for readers that got past this */
    ++	if (folio_test_hwpoison(folio))
    ++		return false;
     +
     +	/*
     +	 * The slow path does not call folio_mark_accessed() on the folio that
    @@ mm/filemap.c: static void filemap_end_dropbehind_read(struct folio *folio)
       * filemap_read - Read data from the page cache.
       * @iocb: The iocb to read.
     @@ mm/filemap.c: ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
    + 	struct file_ra_state *ra = &filp->f_ra;
    + 	struct address_space *mapping = filp->f_mapping;
    + 	struct inode *inode = mapping->host;
    +-	struct folio_batch fbatch;
    + 	int i, error = 0;
      	bool writably_mapped;
      	loff_t isize, end_offset;
      	loff_t last_pos = ra->prev_pos;
    @@ mm/filemap.c: ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
      	if (unlikely(iocb->ki_pos < 0))
      		return -EINVAL;
     @@ mm/filemap.c: ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
    - 	iov_iter_truncate(iter, inode->i_sb->s_maxbytes - iocb->ki_pos);
    - 	folio_batch_init(&fbatch);
    + 		return 0;
      
    + 	iov_iter_truncate(iter, inode->i_sb->s_maxbytes - iocb->ki_pos);
    +-	folio_batch_init(&fbatch);
    ++
     +	fast_read = filemap_read_rcu(iocb, iter, last_pos);
     +	if (fast_read) {
     +		already_read += fast_read;
    @@ mm/filemap.c: ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
     +		if (!iov_iter_count(iter))
     +			goto out;
     +	}
    -+
    + 
      	do {
    ++		struct folio_batch fbatch;
    ++
    ++		folio_batch_init(&fbatch);
      		cond_resched();
      
    + 		/*
     @@ mm/filemap.c: ssize_t filemap_read(struct kiocb *iocb, struct iov_iter *iter,
    - 		folio_batch_init(&fbatch);
    + 			filemap_end_dropbehind_read(folio);
    + 			folio_put(folio);
    + 		}
    +-		folio_batch_init(&fbatch);
      	} while (iov_iter_count(iter) && iocb->ki_pos < isize && !error);
      
     +out:
      	file_accessed(filp);
    - 	/* Readers sharing the file would bounce the line for nothing */
    - 	if (ra->prev_pos != last_pos)
    + 	ra->prev_pos = last_pos;
    + 	return already_read ? already_read : error;
15:  c90a38e2e97a ! 14:  7eaf88828496 mm/filemap: read large folios without folio references
    @@ Commit message
         slot of the last page to copy holds it too, the pages in between are
         this folio's: copy them after one lookup.  The folio's size is read
         without a reference and only picks the page to check; a split after the
    -    check copies PG_rcu_free to the new folios.
    +    check copies PG_rcu_free to the new folios.  A split can also change the
    +    order and PG_head under the reader: clamp the order, and read
    +    PG_has_hwpoisoned without folio_test_has_hwpoisoned(), which asserts
    +    PG_head.
     
         Copy page by page with _copy_to_iter(): the hardened usercopy check of
         copy_to_iter() reads the folio's order, which a split can change.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## mm/filemap.c ##
     @@ mm/filemap.c: static bool filemap_read_rcu_can_copy(struct folio *folio, pgoff_t index,
    - 	/* memory_failure() waits only for readers that got past this */
    - 	if (folio_contain_hwpoisoned_page(folio))
    + 		return false;
    + 	if (folio_test_readahead(folio) || folio_test_dropbehind(folio))
      		return false;
     -	/* One reference covers all of a large folio's pages */
     -	if (folio_test_large(folio))
     -		return false;
    + 	/* A split clears PG_head before it puts the new folios in their slots */
    +-	if (folio->index != index)
    ++	if (!folio_test_large(folio) && folio->index != index)
    + 		return false;
    + 	/* memory_failure() waits only for readers that got past this */
    + 	if (folio_test_hwpoison(folio))
    + 		return false;
    ++	/* Not folio_test_has_hwpoisoned(): it asserts PG_head, a split clears it */
    ++	if (folio_test_large(folio) &&
    ++	    test_bit(PG_has_hwpoisoned, &folio->_flags_1))
    ++		return false;
      
      	/*
      	 * The slow path does not call folio_mark_accessed() on the folio that
    @@ mm/filemap.c: static struct folio *filemap_read_rcu_lookup(struct xa_state *xas,
     +		pgoff_t index, size_t offset, size_t bytes)
     +{
     +	pgoff_t end = index + DIV_ROUND_UP(offset + bytes, PAGE_SIZE);
    ++	unsigned int order;
     +	pgoff_t next;
     +
     +	if (end - index > 1 && folio_test_large(folio)) {
    -+		/* Read without a reference: it only picks the page to check */
    -+		next = folio_next_index(folio);
    ++		/* A split can change it under us: it only picks the page to check */
    ++		order = min(folio_large_order(folio), MAX_PAGECACHE_ORDER);
    ++		next = folio->index + (1UL << order);
     +		if (next > index && next < end)
     +			end = next;
     +		xas_set(xas, end - 1);
 -:  ------------ > 15:  1455ef101c5c mm/filemap: don't store f_ra.prev_pos for FMODE_RANDOM files
16:  c878dc794ca4 ! 16:  77e7c1ce6060 mm/shmem: count accesses to a folio the way filemap_read() does
    @@ Commit message
         marks a tmpfs folio accessed too, so a folio written in several pieces
         can get the flag on its first read.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## mm/filemap.c ##
    @@ mm/shmem.c: static ssize_t shmem_file_read_iter(struct kiocb *iocb, struct iov_i
      	}
      
      	file_accessed(file);
    -+	/* Readers sharing the file would bounce the line for nothing */
    -+	if (file->f_ra.prev_pos != last_pos)
    -+		file->f_ra.prev_pos = last_pos;
    ++	file->f_ra.prev_pos = last_pos;
      	return retval ? retval : error;
      }
      
17:  e472d4328d29 ! 17:  4fc2a5f445d0 mm/shmem: read without folio references
    @@ Commit message
         PG_reclaim, which drop_caches can leave set on dirty tmpfs folios.  Do
         not let it end the copy there.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## mm/filemap.c ##
    @@ mm/filemap.c: static void filemap_end_dropbehind_read(struct folio *folio)
     +	/* shmem does no readahead: there the bit is PG_reclaim */
     +	if (folio_test_readahead(folio) && !folio_test_swapbacked(folio))
      		return false;
    - 	/* memory_failure() waits only for readers that got past this */
    - 	if (folio_contain_hwpoisoned_page(folio))
    + 	/* A split clears PG_head before it puts the new folios in their slots */
    + 	if (!folio_test_large(folio) && folio->index != index)
     @@ mm/filemap.c: static bool filemap_read_rcu_batch(struct kiocb *iocb, struct iov_iter *iter,
       * Copy from the page cache without folio references, in batches, and
       * return how much was copied.  The slow path reads the rest.
    @@ mm/shmem.c: static ssize_t shmem_file_read_iter(struct kiocb *iocb, struct iov_i
     -
     +out:
      	file_accessed(file);
    - 	/* Readers sharing the file would bounce the line for nothing */
    - 	if (file->f_ra.prev_pos != last_pos)
    + 	file->f_ra.prev_pos = last_pos;
    + 	return retval ? retval : error;
18:  b05c7c67fdf7 ! 18:  9a845ea67175 mm/filemap: add a debugfs switch for lockless page cache reads
    @@ Commit message
         Folios that have the flag keep it, and their free stays deferred: a read
         that tested the key before the switch may still be copying from them.
     
    -    Assisted-by: LLM
         Signed-off-by: Kiryl Shutsemau (Meta) <kas@kernel.org>
     
      ## mm/Kconfig ##
     @@ mm/Kconfig: config PAGE_RCU_FREE
    - 	  64-bit only: it takes a page flag, and those are scarce on
    - 	  32-bit.
    + 	  32-bit.  Not on PREEMPT_RT, where expedited grace periods are
    + 	  normal ones, so freeing the waiting pages on demand would stall.
      
     +	  Writing 0 to lockless_page_cache_read in debugfs turns these
     +	  reads off.

base-commit: c03c1a14507dfaf8b856c2608f9ac216021fc222
-- 
2.54.0


             reply	other threads:[~2026-11-04 13:01 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-04 13:00 Kiryl Shutsemau (Meta) [this message]
2026-11-04 13:00 ` [RFC PATCH v2 01/18] mm: free PG_rcu_free folios only after an RCU grace period Kiryl Shutsemau (Meta)
2026-11-04 13:00 ` [RFC PATCH v2 02/18] splice: don't steal page cache folios Kiryl Shutsemau (Meta)
2026-11-04 16:05   ` David Hildenbrand (Arm)
2026-11-04 13:00 ` [RFC PATCH v2 03/18] fuse: don't move a folio with PG_rcu_free into a fuse file Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 04/18] mm: count the pages waiting for a grace period Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 05/18] mm: expedite the grace period when a CPU has many deferred frees Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 06/18] mm: free the pages waiting for a grace period before OOM Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 07/18] mm: free the pages waiting for a grace period before isolation fails Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 08/18] mm/compaction: free the migrated folios that wait for a grace period Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 09/18] mm/memory-failure: handle folios with PG_rcu_free Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 10/18] mm: have folio_mark_accessed() return whether the folio is hot Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 11/18] mm/filemap: mark a folio accessed in one place in filemap_read() Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 12/18] mm/filemap: set PG_rcu_free on the folios that reads keep coming back to Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 13/18] mm/filemap: read from the page cache without folio references Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 14/18] mm/filemap: read large folios " Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 15/18] mm/filemap: don't store f_ra.prev_pos for FMODE_RANDOM files Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 16/18] mm/shmem: count accesses to a folio the way filemap_read() does Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 17/18] mm/shmem: read without folio references Kiryl Shutsemau (Meta)
2026-11-04 13:01 ` [RFC PATCH v2 18/18] mm/filemap: add a debugfs switch for lockless page cache reads Kiryl Shutsemau (Meta)

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261104130118.2507513-1-kas@kernel.org \
    --to=kas@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=axelrasmussen@google.com \
    --cc=baohua@kernel.org \
    --cc=baolin.wang@linux.alibaba.com \
    --cc=baoquan.he@linux.dev \
    --cc=brauner@kernel.org \
    --cc=brendan.jackman@linux.dev \
    --cc=david@kernel.org \
    --cc=dev.jain@arm.com \
    --cc=fuse-devel@lists.linux.dev \
    --cc=hannes@cmpxchg.org \
    --cc=harry@kernel.org \
    --cc=hughd@google.com \
    --cc=ilya.gladyshev@linux.dev \
    --cc=jack@suse.cz \
    --cc=jaegeuk@kernel.org \
    --cc=kasong@tencent.com \
    --cc=kernel-team@meta.com \
    --cc=lance.yang@linux.dev \
    --cc=liam@infradead.org \
    --cc=linmiaohe@huawei.com \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=ljs@kernel.org \
    --cc=mcgrof@kernel.org \
    --cc=mhocko@suse.com \
    --cc=miklos@szeredi.hu \
    --cc=nao.horiguchi@gmail.com \
    --cc=nico.pache@linux.dev \
    --cc=osalvador@suse.de \
    --cc=paulmck@kernel.org \
    --cc=pfalcato@suse.de \
    --cc=qi.zheng@linux.dev \
    --cc=rppt@kernel.org \
    --cc=ryan.roberts@arm.com \
    --cc=shakeel.butt@linux.dev \
    --cc=surenb@google.com \
    --cc=torvalds@linux-foundation.org \
    --cc=usama.arif@linux.dev \
    --cc=vbabka@kernel.org \
    --cc=viro@zeniv.linux.org.uk \
    --cc=weixugc@google.com \
    --cc=willy@infradead.org \
    --cc=yuanchu@google.com \
    --cc=ziy@nvidia.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox