From: Matthew Ruffell <matthew.ruffell@canonical.com>
To: Waiman Long <longman@redhat.com>,
Frederic Weisbecker <frederic@kernel.org>,
Tejun Heo <tj@kernel.org>, Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@redhat.com>,
Andrew Morton <akpm@linux-foundation.org>,
David Hildenbrand <david@kernel.org>
Cc: "Caron Wills" <caron.wills@pulsar.com>,
"Ridong Chen" <ridong.chen@linux.dev>,
"Johannes Weiner" <hannes@cmpxchg.org>,
"Michal Koutný" <mkoutny@suse.com>,
"Juri Lelli" <juri.lelli@redhat.com>,
"Vincent Guittot" <vincent.guittot@linaro.org>,
"Dietmar Eggemann" <dietmar.eggemann@arm.com>,
"Steven Rostedt" <rostedt@goodmis.org>,
"Ben Segall" <bsegall@google.com>, "Mel Gorman" <mgorman@suse.de>,
"Valentin Schneider" <vschneid@redhat.com>,
"K Prateek Nayak" <kprateek.nayak@amd.com>,
"Lorenzo Stoakes" <ljs@kernel.org>,
"Liam R . Howlett" <liam@infradead.org>,
"Vlastimil Babka" <vbabka@kernel.org>,
"Mike Rapoport" <rppt@kernel.org>,
"Suren Baghdasaryan" <surenb@google.com>,
"Michal Hocko" <mhocko@suse.com>, "Jann Horn" <jannh@google.com>,
"Pedro Falcato" <pfalcato@suse.de>,
linux-kernel@vger.kernel.org, cgroups@vger.kernel.org,
linux-mm@kvack.org, "Jay Vosburgh" <jay.vosburgh@canonical.com>
Subject: [PATCH 3/6] mm/folio, mm/mlock: Don't batch folios in per-CPU LRU caches on isolated CPUs
Date: Tue, 3 Nov 2026 20:59:37 +1300 [thread overview]
Message-ID: <20261103075940.110308-4-matthew.ruffell@canonical.com> (raw)
In-Reply-To: <20261103075940.110308-1-matthew.ruffell@canonical.com>
From: Caron Wills <caron.wills@pulsar.com>
Folios are collected in per-CPU folio batches (cpu_fbatches and
mlock_fbatch) before being added to or moved on the LRU lists. Another
CPU can only empty such a batch by queueing lru_add_drain_per_cpu() on
the owning CPU and waiting for it in __lru_add_drain_all(), which is
what every lru_add_drain_all() caller (compaction, khugepaged, memory
hotplug, memfd, drop_caches, ...) and every lru_cache_disable() caller
(page migration through cpuset.mems, mbind() and move_pages(),
alloc_contig_range(), memory offlining) does.
On an isolated CPU that per-CPU kworker may not run for a very long
time, or ever: low latency deployments pin a SCHED_FIFO busy-polling
thread to each isolated CPU, and since RT bandwidth throttling is gone
nothing forces such a thread to yield to a SCHED_NORMAL kworker. Any
folio left in that CPU's batch then blocks the waiting caller.
LP: #2165410 hits exactly this. Writing cpuset.mems for a cgroup on a
192-CPU AWS instance with nohz_full CPUs 18-95,108-178 running FIFO
spinners makes cpuset_migrate_mm_workfn() -> do_migrate_pages() ->
lru_cache_disable() -> __lru_add_drain_all() -> flush_work() wait for
the kworkers on those CPUs, and PID 1 (systemd), which flushes the
migration workqueue from task_work before returning to userspace, stays
in D state for 2 to 10 minutes. logind cannot create sessions meanwhile
and SSH logins fail. The reproducer, a FIFO/80 thread per isolated CPU
that touches and MADV_DONTNEEDs a 24 MB buffer in a loop, keeps 1 to 14
folios in the lru_add batch of every isolated CPU at all times.
Stop batching on isolated CPUs: when the current CPU is isolated, move
the folio to the LRU immediately through an on-stack batch, without
touching the per-CPU batch at all. The latter matters: __lru_add_drain_all()
samples the per-CPU counters without the owning CPU's local_lock, so a
folio that sits in the batch even for the few instructions between
folio_batch_add() and an immediate drain is enough to get work queued on
that CPU. A first version of this patch reused the lru_cache_disabled()
add-then-drain path and was caught doing exactly that in testing.
cpu_needs_drain() now stays false for isolated CPUs and
__lru_add_drain_all() never queues work on them. The per-CPU buffer_head
LRU has been skipping isolated CPUs the same way since commit 8a237adf213d
("fs/buffer.c: disable per-CPU buffer_head cache for isolated CPUs"),
and mm/memcontrol.c schedule_drain_work() does not queue memcg stock drains
on them either.
The cost is one lruvec lock acquisition per folio instead of one per
batch of 15 on isolated CPUs, which are not expected to do bulk memory
work. Nothing changes on housekeeping CPUs or on systems without CPU
isolation, where cpu_is_isolated() is a static-key guarded test.
Link: https://bugs.launchpad.net/bugs/2165410
Signed-off-by: Caron Wills <caron.wills@pulsar.com>
Signed-off-by: Matthew Ruffell <matthew.ruffell@canonical.com>
---
mm/folio.c | 21 ++++++++++++++++++++-
mm/internal.h | 28 ++++++++++++++++++++++++++++
mm/mlock.c | 28 ++++++++++++++++++++++------
3 files changed, 70 insertions(+), 7 deletions(-)
diff --git a/mm/folio.c b/mm/folio.c
index 35e242b48870..7f85747910d7 100644
--- a/mm/folio.c
+++ b/mm/folio.c
@@ -208,6 +208,16 @@ static void folio_batch_move_lru(struct folio_batch *fbatch, move_fn_t move_fn)
folios_put(fbatch);
}
+/* Move a single folio to the LRU without touching the per-CPU batches. */
+static void folio_move_lru_now(struct folio *folio, move_fn_t move_fn)
+{
+ struct folio_batch fbatch;
+
+ folio_batch_init(&fbatch);
+ folio_batch_add(&fbatch, folio);
+ folio_batch_move_lru(&fbatch, move_fn);
+}
+
static void __folio_batch_add_and_move(struct folio_batch __percpu *fbatch,
struct folio *folio, move_fn_t move_fn, bool disable_irq)
{
@@ -220,7 +230,9 @@ static void __folio_batch_add_and_move(struct folio_batch __percpu *fbatch,
else
local_lock(&cpu_fbatches.lock);
- if (!folio_batch_add(this_cpu_ptr(fbatch), folio) ||
+ if (lru_batching_bypassed())
+ folio_move_lru_now(folio, move_fn);
+ else if (!folio_batch_add(this_cpu_ptr(fbatch), folio) ||
!folio_may_be_lru_cached(folio) || lru_cache_disabled())
folio_batch_move_lru(this_cpu_ptr(fbatch), move_fn);
@@ -856,6 +868,13 @@ static inline void __lru_add_drain_all(bool force_all_cpus)
WRITE_ONCE(lru_drain_gen, lru_drain_gen + 1);
smp_mb();
+ /*
+ * Isolated CPUs don't batch folios (see lru_batching_bypassed()), so
+ * their counters stay at zero and no work gets queued to them. This
+ * matters because the per-CPU kworker of an isolated CPU may not get
+ * to run for a very long time, if ever, and flush_work() below would
+ * wait for it.
+ */
cpumask_clear(&has_work);
for_each_online_cpu(cpu) {
struct work_struct *work = &per_cpu(lru_add_drain_work, cpu);
diff --git a/mm/internal.h b/mm/internal.h
index 566cbd2dce48..5888f7b5ea5c 100644
--- a/mm/internal.h
+++ b/mm/internal.h
@@ -17,6 +17,7 @@
#include <linux/pagewalk.h>
#include <linux/rmap.h>
#include <linux/swap.h>
+#include <linux/sched/isolation.h>
#include <linux/leafops.h>
#include <linux/tracepoint-defs.h>
@@ -1006,6 +1007,33 @@ bool need_mlock_drain(int cpu);
void mlock_drain_local(void);
void mlock_drain_remote(int cpu);
+/*
+ * Folios are normally parked in per-CPU batches (cpu_fbatches and
+ * mlock_fbatch) before they are moved to the LRU lists. A remote CPU can
+ * only empty such a batch by queueing lru_add_drain_per_cpu() on the owning
+ * CPU and waiting for it, see __lru_add_drain_all(). On an isolated CPU that
+ * kworker may not get to run for a long time, e.g. because a SCHED_FIFO task
+ * busy-loops there, and every lru_add_drain_all() or lru_cache_disable()
+ * caller in the system then blocks behind it.
+ *
+ * Isolated CPUs therefore bypass the per-CPU batches entirely and move
+ * folios to the LRU lists right away, through an on-stack batch. The folio
+ * must not even transiently sit in the per-CPU batch: __lru_add_drain_all()
+ * samples the batch counters without the owning CPU's lock, so the few
+ * instructions between folio_batch_add() and an immediate drain are enough
+ * to get work queued on the isolated CPU. The per-CPU buffer_head LRU
+ * (bh_lru_install()) and the memcg stock drain (schedule_drain_work())
+ * already stay away from isolated CPUs the same way.
+ *
+ * Callers hold the batch's local_lock, so smp_processor_id() is stable. The
+ * housekeeping cpumask consulted by cpu_is_isolated() is RCU protected.
+ */
+static inline bool lru_batching_bypassed(void)
+{
+ guard(rcu)();
+ return cpu_is_isolated(smp_processor_id());
+}
+
extern pmd_t maybe_pmd_mkwrite(pmd_t pmd, struct vm_area_struct *vma);
static inline unsigned long __vma_address(const struct vm_area_struct *vma,
diff --git a/mm/mlock.c b/mm/mlock.c
index 67ae9b18b923..3bb8195e5c5f 100644
--- a/mm/mlock.c
+++ b/mm/mlock.c
@@ -209,6 +209,16 @@ static void mlock_folio_batch(struct folio_batch *fbatch)
folios_put(fbatch);
}
+/* Handle a single folio right away, without touching the per-CPU batch. */
+static void mlock_folio_now(struct folio *folio)
+{
+ struct folio_batch fbatch;
+
+ folio_batch_init(&fbatch);
+ folio_batch_add(&fbatch, folio);
+ mlock_folio_batch(&fbatch);
+}
+
void mlock_drain_local(void)
{
struct folio_batch *fbatch;
@@ -254,8 +264,10 @@ void mlock_folio(struct folio *folio)
}
folio_get(folio);
- if (!folio_batch_add(fbatch, mlock_lru(folio)) ||
- !folio_may_be_lru_cached(folio) || lru_cache_disabled())
+ if (lru_batching_bypassed())
+ mlock_folio_now(mlock_lru(folio));
+ else if (!folio_batch_add(fbatch, mlock_lru(folio)) ||
+ !folio_may_be_lru_cached(folio) || lru_cache_disabled())
mlock_folio_batch(fbatch);
local_unlock(&mlock_fbatch.lock);
}
@@ -277,8 +289,10 @@ void mlock_new_folio(struct folio *folio)
__count_vm_events(UNEVICTABLE_PGMLOCKED, nr_pages);
folio_get(folio);
- if (!folio_batch_add(fbatch, mlock_new(folio)) ||
- !folio_may_be_lru_cached(folio) || lru_cache_disabled())
+ if (lru_batching_bypassed())
+ mlock_folio_now(mlock_new(folio));
+ else if (!folio_batch_add(fbatch, mlock_new(folio)) ||
+ !folio_may_be_lru_cached(folio) || lru_cache_disabled())
mlock_folio_batch(fbatch);
local_unlock(&mlock_fbatch.lock);
}
@@ -298,8 +312,10 @@ void munlock_folio(struct folio *folio)
* which will check whether the folio is multiply mlocked.
*/
folio_get(folio);
- if (!folio_batch_add(fbatch, folio) ||
- !folio_may_be_lru_cached(folio) || lru_cache_disabled())
+ if (lru_batching_bypassed())
+ mlock_folio_now(folio);
+ else if (!folio_batch_add(fbatch, folio) ||
+ !folio_may_be_lru_cached(folio) || lru_cache_disabled())
mlock_folio_batch(fbatch);
local_unlock(&mlock_fbatch.lock);
}
--
2.55.0
next prev parent reply other threads:[~2026-11-03 8:00 UTC|newest]
Thread overview: 17+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-11-03 7:59 [PATCH 0/6] mm, sched, cgroup: LRU Cache never drains when isolcpus are used with spinning SCHED_FIFO tasks causing page migration stalls Matthew Ruffell
2026-11-03 7:59 ` [PATCH 1/6] sched/isolation: Consider nohz_full CPUs isolated in cpu_is_isolated() again Matthew Ruffell
2026-11-03 7:59 ` [PATCH 2/6] kthread: Keep unbound kthreads off nohz_full CPUs Matthew Ruffell
2026-11-03 14:01 ` Frederic Weisbecker
2026-11-03 15:13 ` Waiman Long
2026-11-03 7:59 ` Matthew Ruffell [this message]
2026-11-03 12:53 ` [PATCH 3/6] mm/folio, mm/mlock: Don't batch folios in per-CPU LRU caches on isolated CPUs kernel test robot
2026-11-03 13:30 ` kernel test robot
2026-11-03 7:59 ` [PATCH 4/6] sched/isolation: Drain per-CPU LRU batches when CPUs become isolated Matthew Ruffell
2026-11-03 14:10 ` Frederic Weisbecker
2026-11-03 7:59 ` [PATCH 5/6] cgroup/cpuset: Don't wait for memory migration when cpuset.mems is written with O_NONBLOCK Matthew Ruffell
2026-11-03 14:59 ` Tejun Heo
2026-11-03 15:22 ` Waiman Long
2026-11-03 7:59 ` [PATCH 6/6] cgroup/cpuset: Don't make a forking task wait for memory migration Matthew Ruffell
2026-11-03 14:58 ` Tejun Heo
2026-11-03 9:32 ` [PATCH 0/6] mm, sched, cgroup: LRU Cache never drains when isolcpus are used with spinning SCHED_FIFO tasks causing page migration stalls Michal Hocko
2026-11-03 15:01 ` Tejun Heo
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=20261103075940.110308-4-matthew.ruffell@canonical.com \
--to=matthew.ruffell@canonical.com \
--cc=akpm@linux-foundation.org \
--cc=bsegall@google.com \
--cc=caron.wills@pulsar.com \
--cc=cgroups@vger.kernel.org \
--cc=david@kernel.org \
--cc=dietmar.eggemann@arm.com \
--cc=frederic@kernel.org \
--cc=hannes@cmpxchg.org \
--cc=jannh@google.com \
--cc=jay.vosburgh@canonical.com \
--cc=juri.lelli@redhat.com \
--cc=kprateek.nayak@amd.com \
--cc=liam@infradead.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=ljs@kernel.org \
--cc=longman@redhat.com \
--cc=mgorman@suse.de \
--cc=mhocko@suse.com \
--cc=mingo@redhat.com \
--cc=mkoutny@suse.com \
--cc=peterz@infradead.org \
--cc=pfalcato@suse.de \
--cc=ridong.chen@linux.dev \
--cc=rostedt@goodmis.org \
--cc=rppt@kernel.org \
--cc=surenb@google.com \
--cc=tj@kernel.org \
--cc=vbabka@kernel.org \
--cc=vincent.guittot@linaro.org \
--cc=vschneid@redhat.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
all inboxes | Powered by JetHome®