Deferring every exit would churn small processes for no benefit and let
unreaped memory grow without bound. Split the defer decision into a
pure predicate and a budget reservation:

  - async_mm_teardown_eligible() (side-effect free): defer only when
    RSS >= async_mm_teardown_thresh_pages and both MMF_OOM_SKIP and
    MMF_OOM_TARGETED are clear:
      * MMF_OOM_SKIP -- the mm is hidden from the OOM killer and
        reaper. Usually that means it has already been reaped or run
        through exit_mmap() and has no RSS left worth deferring; in the
        mm-pinned-by-init case (__oom_kill_process()) the flag is set at
        kill time with RSS intact, and keeping such an mm synchronous is
        equally what we want;
      * MMF_OOM_TARGETED -- the mm is the target of an in-flight OOM
        kill (added in the preceding patch). Such a victim is torn
        down synchronously so its memory is released promptly rather
        than queued behind the low-priority, housekeeping-confined
        mm_reaper, which would work against the oom_reaper during the
        exact pressure that triggered the kill. The flag is set while
        some task still holds a live reference to the mm, and this
        check is reached only after mm_users has dropped to 0 -- which
        requires that task's own exit_mm() (or exec_mmap(), the other
        path that sheds ->mm) to have already cleared ->mm under that
        same task_lock() -- so marking and checking can never overlap in
        time. Whichever thread performs the final decrement is therefore
        guaranteed to observe the flag already set, per the preceding
        patch.

  - async_mm_teardown_reserve(): charge RSS against a pending-pages
    budget with atomic_long_add_return(); if the result exceeds
    async_mm_teardown_max_pending_pages it subtracts back and returns
    false, so the caller falls back to synchronous __mmput().

mmput_exit() reads RSS once and checks eligibility before reservation,
so the budget is only touched for processes that pass the cheap
checks. On success it stores the charged amount in mm->async_reap_node's
companion field async_reap_rss before enqueueing, and the reaper
releases exactly that stored charge after __mmput(). The charge cannot
be re-derived at reap time: RSS keeps shrinking after mm_users reaches
zero -- rmap-based reclaim can still swap out anon pages and truncation
can still unmap file pages, neither of which requires mm_users -- and
get_mm_rss() is an approximate per-CPU counter read besides. Two
independent reads at enqueue and reap would systematically
under-release, drifting the pending counter upward until the cap
permanently forces synchronous fallback.

An eligible mm would otherwise have its entire __mmput() -- including
exit_aio() -- deferred onto the single mm_reaper kthread.
exit_aio() can block indefinitely waiting for in-flight AIO to drain,
so mmput_exit() calls it eagerly, on the exiting task's own context,
before reservation: a stuck AIO context then only stalls this task,
same as a synchronous mmput() would, instead of blocking mm_reaper and
stranding every other mm already queued behind it. exit_aio() is
idempotent -- it is a no-op once mm->ioctx_table has been torn down --
so __mmput() calling it again unconditionally is harmless, whether that
second call lands on the reaper or -- when the reservation fails --
back on the exiting task itself.

Because reservation is add-then-check rather than read-then-add,
committed pending never stays above the cap: an exit that would exceed
it tears down synchronously instead. Under concurrent exits a failing
reservation can transiently inflate the counter between its add and
roll-back, which may cause another exiter to fall back to sync
conservatively -- it can over-reject but never over-admit.

The cap is a coarse safety bound on unreaped memory, not a reclaim
mechanism: pages queued here are invisible to reclaim until the reaper
frees them. Integrating the queue with reclaim (unmap-first draining,
a shrinker) is future work; see the cover letter.

The RSS threshold defaults to a fixed 64MB at init; the pending cap
defaults to totalram_pages() / 4 as a placeholder pending tuning. Both
are static module variables here and not yet user-tunable. The static
key that gates the whole path, and sysctl registration, follow in the
next patch.

This patch is dormant: nothing calls mmput_exit() yet (exit_mm() is
routed to it later in the series), so no teardown is deferred.

Signed-off-by: Aditya Sharma <[email protected]>
---
 include/linux/mm_types.h |  1 +
 kernel/fork.c            | 79 +++++++++++++++++++++++++++++++++++++++-
 2 files changed, 79 insertions(+), 1 deletion(-)

diff --git a/include/linux/mm_types.h b/include/linux/mm_types.h
index 3240c029f..ee6c0ac84 100644
--- a/include/linux/mm_types.h
+++ b/include/linux/mm_types.h
@@ -1370,6 +1370,7 @@ struct mm_struct {
                struct work_struct async_put_work;
 #ifdef CONFIG_ASYNC_MM_TEARDOWN
                struct llist_node async_reap_node;
+               unsigned long async_reap_rss;
 #endif /* CONFIG_ASYNC_MM_TEARDOWN */
 
 #ifdef CONFIG_IOMMU_MM_DATA
diff --git a/kernel/fork.c b/kernel/fork.c
index 884e4e767..d45418e66 100644
--- a/kernel/fork.c
+++ b/kernel/fork.c
@@ -113,6 +113,7 @@
 #include <linux/pgalloc.h>
 #include <linux/uaccess.h>
 #include <linux/sched/isolation.h>
+#include <linux/sizes.h>
 
 #include <asm/mmu_context.h>
 #include <asm/cacheflush.h>
@@ -3412,6 +3413,48 @@ subsys_initcall(init_fork_sysctl);
 #ifdef CONFIG_ASYNC_MM_TEARDOWN
 static LLIST_HEAD(mm_reaper_list);
 static DECLARE_WAIT_QUEUE_HEAD(mm_reaper_wait);
+static atomic_long_t mm_reaper_pending_pages;
+static unsigned long sysctl_async_mm_teardown_thresh_pages;
+static unsigned long sysctl_async_mm_teardown_max_pending_pages;
+
+static bool async_mm_teardown_eligible(struct mm_struct *mm, unsigned long rss)
+{
+       if (rss < READ_ONCE(sysctl_async_mm_teardown_thresh_pages))
+               return false;
+       if (mm_flags_test(MMF_OOM_SKIP, mm))    /* reaped, or hidden from the 
reaper */
+               return false;
+       /*
+        * MMF_OOM_TARGETED is set by the OOM killer while some task still
+        * holds a live reference to this mm (see mark_oom_victim() and
+        * __oom_kill_process()), and this eligibility check is reached only
+        * from mmput_exit(), after mm_users has dropped to 0 -- so marking
+        * and this check can never overlap in time. Whichever thread
+        * performs the final decrement is therefore guaranteed to observe
+        * the flag already set, regardless of whether that thread was
+        * itself ever passed to mark_oom_victim() (a CLONE_VM-sharing
+        * process in another thread group never is, but still observes
+        * this flag on the shared mm).
+        */
+       if (mm_flags_test(MMF_OOM_TARGETED, mm))
+               return false;
+       return true;
+}
+
+/*
+ * Charge rss against the pending-teardown budget.  Returns true if it fits
+ * under the cap, in which case the caller enqueues and the reaper releases
+ * the same rss after __mmput().  On overshoot nothing stays charged and the
+ * caller must tear down synchronously.
+ */
+static bool async_mm_teardown_reserve(unsigned long rss)
+{
+       if (atomic_long_add_return(rss, &mm_reaper_pending_pages) >
+           READ_ONCE(sysctl_async_mm_teardown_max_pending_pages)) {
+               atomic_long_sub(rss, &mm_reaper_pending_pages);
+               return false;
+       }
+       return true;
+}
 
 static void async_mm_teardown_queue(struct mm_struct *mm)
 {
@@ -3429,7 +3472,10 @@ static int mm_reaper(void *unused)
                wait_event_freezable(mm_reaper_wait, 
!llist_empty(&mm_reaper_list));
                batch = llist_del_all(&mm_reaper_list);
                llist_for_each_entry_safe(mm, n, batch, async_reap_node) {
+                       unsigned long pages = mm->async_reap_rss;
+
                        __mmput(mm); /* may free mm via mmdrop */
+                       atomic_long_sub(pages, &mm_reaper_pending_pages);
                        cond_resched();
                        try_to_freeze();
                }
@@ -3439,10 +3485,38 @@ static int mm_reaper(void *unused)
 
 void mmput_exit(struct mm_struct *mm)
 {
+       unsigned long rss;
+
        might_sleep();
+       /*
+        * Fully ordered RMW: combined with exit_mm() (or exec_mmap(), the
+        * other path that sheds ->mm) clearing ->mm under task_lock() before
+        * this call, it guarantees MMF_OOM_TARGETED set at any mark site is
+        * visible here -- see mark_oom_victim(). Any new caller of
+        * mmput_exit() outside exit_mm() must re-verify that argument.
+        */
        if (!atomic_dec_and_test(&mm->mm_users))
                return;
-       async_mm_teardown_queue(mm);
+
+       rss = get_mm_rss(mm);
+
+       if (async_mm_teardown_eligible(mm, rss)) {
+               /*
+                * exit_aio() can block indefinitely. Run it here so a stuck
+                * AIO only hangs this task (same as how it would happen in
+                * case of synchronous mmput()) instead of stranding every
+                * teardown queued behind this mm
+                */
+               exit_aio(mm);
+
+               if (async_mm_teardown_reserve(rss)) {
+                       mm->async_reap_rss = rss;
+                       async_mm_teardown_queue(mm);
+                       return;
+               }
+       }
+
+       __mmput(mm);
 }
 
 static int __init mm_reaper_init(void)
@@ -3456,7 +3530,10 @@ static int __init mm_reaper_init(void)
        }
        set_user_nice(th, 19); /* minimize competition with other fair class 
tasks */
        kthread_affine_preferred(th, housekeeping_cpumask(HK_TYPE_KTHREAD));
+       sysctl_async_mm_teardown_thresh_pages      = SZ_64M >> PAGE_SHIFT;
+       sysctl_async_mm_teardown_max_pending_pages = totalram_pages() / 4;  /* 
TODO: placeholder */
        wake_up_process(th);
+
        return 0;
 }
 subsys_initcall(mm_reaper_init);
-- 
2.34.1


Reply via email to