[PATCH 5/6] cgroup/cpuset: Don't wait for memory migration when cpuset.mems is written with O_NONBLOCK

From: Matthew Ruffell

Date: Tue Nov 03 2026 - 03:07:31 EST


From: Caron Wills <caron.wills@xxxxxxxxxx>

Writing cpuset.mems migrates the memory of every task in the cpuset on
the ordered cpuset_migrate_mm_wq, and since commit 3514309e0322
("cpuset: Defer flushing of the cpuset_migrate_mm_wq to task_work") the
writer flushes that workqueue from task_work before returning to
userspace, so that the write appears synchronous (the design of commit
e93ad19d0564 ("cpuset: make mm migration asynchronous")). The writer
therefore sleeps in TASK_UNINTERRUPTIBLE for as long as all queued
migrations take. On a system with isolated CPUs running SCHED_FIFO tasks
that was measured at 2 to 10 minutes (LP: #2165410), and it is unbounded
in principle: a migration blocked behind a starved kworker or a slow
expedited grace period holds up every migration queued after it.

The writer is usually systemd. It applies AllowedMemoryNodes= from PID 1
itself, and since v259 it opens every cgroup v2 attribute file with
O_NONBLOCK precisely to opt into asynchronous semantics wherever the
kernel offers them (src/basic/cgroup-util.c, cg_set_attribute(), citing
the memory.max/memory.high behaviour of commit c8e6002bd611 ("memcg:
introduce non-blocking limit setting option")). While PID 1 is stuck in
the flush, logind cannot create sessions and SSH logins fail.

Honour O_NONBLOCK the same way memcg does: skip the task_work flush when
the file was opened non-blocking and let the migration complete in the
background. Blocking writers keep the synchronous behaviour and the
cgroup.procs attach path is unchanged.

This does not make the migration any faster; it only stops the writer,
in practice init, from being held hostage by it.

Link: https://bugs.launchpad.net/bugs/2165410
Signed-off-by: Caron Wills <caron.wills@xxxxxxxxxx>
Signed-off-by: Matthew Ruffell <matthew.ruffell@xxxxxxxxxxxxx>
---
kernel/cgroup/cpuset.c | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/kernel/cgroup/cpuset.c b/kernel/cgroup/cpuset.c
index 3219f927c35e..439b55a310fe 100644
--- a/kernel/cgroup/cpuset.c
+++ b/kernel/cgroup/cpuset.c
@@ -3482,7 +3482,16 @@ ssize_t cpuset_write_resmask(struct kernfs_open_file *of,
free_cpuset(trialcs);
out_unlock:
cpuset_update_sd_hk_unlock();
- if (of_cft(of)->private == FILE_MEMLIST)
+ /*
+ * Memory migration runs on cpuset_migrate_mm_wq and the writer
+ * normally waits for it before returning to userspace, so that the
+ * write appears synchronous. A writer that opened the file with
+ * O_NONBLOCK asks not to wait: let the migration finish in the
+ * background, like memory.max and memory.high skip synchronous
+ * reclaim for non-blocking writers.
+ */
+ if (of_cft(of)->private == FILE_MEMLIST &&
+ !(of->file->f_flags & O_NONBLOCK))
schedule_flush_migrate_mm();
return retval ?: nbytes;
}
--
2.55.0