mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Breno Leitao <leitao@debian.org>
To: Tejun Heo <tj@kernel.org>, Lai Jiangshan <jiangshanlai@gmail.com>
Cc: linux-kernel@vger.kernel.org, marco.crivellari@suse.com,
	 Breno Leitao <leitao@debian.org>,
	kernel-team@meta.com
Subject: [PATCH wq/for-7.5 v4 2/2] workqueue: Keep the limits of both max_active domains current
Date: Thu, 29 Oct 2026 05:33:46 -0700	[thread overview]
Message-ID: <20261029-wq_final-v4-2-2985ea9f24fd@debian.org> (raw)
In-Reply-To: <20261029-wq_final-v4-0-2985ea9f24fd@debian.org>

Commit 27db9dd7f84f3a ("workqueue: Give percpu workqueues their own
max_active") gave the percpu backend a limit of its own. A workqueue
carries max_active and min_active for the unbound backend and
percpu_max_active for the percpu one. Only the backend it is created on
gets its limits set. The other stays at zero.

The planned PERCPU affinity scope will let a workqueue switch backends
through its attributes, and a switch should land on a sensible limit.
Derive the other domain whenever the limits of the backend in use are
set.

The two numbers cannot simply be kept equal. percpu_max_active caps every
CPU on its own, while max_active is a workqueue wide budget distributed
among the nodes, with min_active as the per-node floor. A per-CPU limit
of 16 on a 64 CPU machine is not a workqueue wide limit of 16.

Convert between them as Tejun suggested [1]:

  U = unbound max_active, the system-wide target
  L = unbound min_active, the floor on each node
  P = percpu_max_active, the limit on each CPU
  N = online CPUs the workqueue is spread over, at least 1

  Setting P:
    U = min(P * N, WQ_MAX_ACTIVE)
    L = P

  Setting U:
    L = min(L, U)
    P = max(DIV_ROUND_UP(U, N), L)

  Setting L:
    L = clamp(L, 0, U)
    P = max(DIV_ROUND_UP(U, N), L)

For an unbound workqueue, N counts the online CPUs in its effective
cpumask, which wq_update_node_max_active() distributes max_active over.
For a percpu workqueue, N counts the online CPUs in wq_unbound_cpumask,
which an unbound scope would spread it over.

wq_rescale_max_active() does the conversion and publishes the limits
through wq_adjust_max_active(). The setters call it, and so does
apply_wqattrs_commit(), which runs when a workqueue is created and when
its attributes or the unbound cpumask change. workqueue_init_topology()
calls it too, as workqueues created before smp_init() counted only the
boot CPU.

N is not resampled on CPU hotplug, nor for a percpu workqueue when
wq_unbound_cpumask changes. Nothing reads the derived domain yet, and a
scope change goes through apply_wqattrs_commit(), which rederives it.

For a percpu workqueue, saved_max_active now holds the derived unbound
limit. start_flush_work() and max_active_show() want the limit the
workqueue was configured with, so they read it through
wq_user_max_active(). Both read it locklessly, so the saved limits are
written with WRITE_ONCE().

Suggested-by: Tejun Heo <tj@kernel.org>
Link: https://lore.kernel.org/all/ec6c22eddc8ce0b1ef2434ed62acd560@kernel.org/ [1]
Signed-off-by: Breno Leitao <leitao@debian.org>
---
 kernel/workqueue.c | 152 ++++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 120 insertions(+), 32 deletions(-)

diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index 826a64c6055d0c..492ff2133c41a2 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -393,6 +393,7 @@ struct workqueue_struct {
 	int			min_active;	/* WO: min active works */
 	int			saved_max_active; /* WQ: saved max_active */
 	int			saved_min_active; /* WQ: saved min_active */
+	int			saved_percpu_max_active; /* WQ: percpu limit */
 
 	struct workqueue_attrs	*attrs;	/* PW: workqueue attributes */
 	struct pool_workqueue __rcu *dfl_pwq;   /* PW: only for unbound wqs */
@@ -421,6 +422,14 @@ struct workqueue_struct {
 	struct wq_node_nr_active *node_nr_active[]; /* I: per-node nr_active */
 };
 
+static int wq_user_max_active(struct workqueue_struct *wq)
+{
+	if (wq->flags & WQ_UNBOUND)
+		return READ_ONCE(wq->saved_max_active);
+
+	return READ_ONCE(wq->saved_percpu_max_active);
+}
+
 /*
  * Each pod type describes how CPUs should be grouped for unbound workqueues.
  * See the comment above workqueue_attrs->affn_scope.
@@ -569,6 +578,7 @@ static int worker_thread(void *__worker);
 static void workqueue_sysfs_unregister(struct workqueue_struct *wq);
 static void show_pwq(struct pool_workqueue *pwq);
 static void show_one_worker_pool(struct worker_pool *pool);
+static void wq_rescale_max_active(struct workqueue_struct *wq);
 
 #define CREATE_TRACE_POINTS
 #include <trace/events/workqueue.h>
@@ -4549,7 +4559,7 @@ static bool start_flush_work(struct work_struct *work, struct wq_barrier *barr,
 	 * workqueues the deadlock happens when the rescuer stalls, blocking
 	 * forward progress.
 	 */
-	if (!from_cancel && (wq->saved_max_active == 1 || wq->rescuer))
+	if (!from_cancel && (wq_user_max_active(wq) == 1 || wq->rescuer))
 		touch_wq_lockdep_map(wq);
 
 	rcu_read_unlock();
@@ -5800,6 +5810,9 @@ static void apply_wqattrs_commit(struct apply_wqattrs_ctx *ctx)
 	if (ctx->wq->flags & WQ_UNBOUND)
 		wq_update_node_max_active(ctx->wq, -1);
 
+	/* derive the other max_active domain for the new effective cpumask */
+	wq_rescale_max_active(ctx->wq);
+
 	mutex_unlock(&ctx->wq->mutex);
 }
 
@@ -6031,6 +6044,42 @@ static int init_rescuer(struct workqueue_struct *wq)
 	return 0;
 }
 
+static int wq_nr_online_cpus(const struct cpumask *mask)
+{
+	return max(cpumask_weight_and(mask, cpu_online_mask), 1);
+}
+
+/* online CPUs an unbound backend of @wq would spread max_active over */
+static int wq_nr_effective_cpus(struct workqueue_struct *wq)
+{
+	lockdep_assert_held(&wq->mutex);
+
+	if (!(wq->flags & WQ_UNBOUND))
+		return wq_nr_online_cpus(wq_unbound_cpumask);
+
+	return wq_nr_online_cpus(unbound_effective_cpumask(wq));
+}
+
+static int wq_scale_to_unbound(int percpu_max, int nr_cpus)
+{
+	return min_t(s64, (s64)percpu_max * nr_cpus, WQ_MAX_ACTIVE);
+}
+
+static int wq_scale_to_percpu(int max_active, int min_active, int nr_cpus)
+{
+	return max(DIV_ROUND_UP(max_active, nr_cpus), min_active);
+}
+
+static void wq_init_bh_max_active(struct workqueue_struct *wq)
+{
+	wq->max_active = INT_MAX;
+	wq->min_active = INT_MAX;
+	wq->percpu_max_active = INT_MAX;
+	wq->saved_max_active = INT_MAX;
+	wq->saved_min_active = INT_MAX;
+	wq->saved_percpu_max_active = INT_MAX;
+}
+
 static void wq_init_max_active(struct workqueue_struct *wq, int max_active)
 {
 	int effective_max_active;
@@ -6040,65 +6089,69 @@ static void wq_init_max_active(struct workqueue_struct *wq, int max_active)
 		 * BH workqueues always share a single execution context per CPU
 		 * and don't impose any max_active limit.
 		 */
-		effective_max_active = INT_MAX;
-	} else {
-		effective_max_active = max_active ?: WQ_DFL_ACTIVE;
-		effective_max_active = wq_clamp_max_active(effective_max_active,
-							   wq->flags, wq->name);
+		wq_init_bh_max_active(wq);
+		return;
 	}
 
+	effective_max_active = max_active ?: WQ_DFL_ACTIVE;
+	effective_max_active = wq_clamp_max_active(effective_max_active,
+						   wq->flags, wq->name);
+
+	/* apply_wqattrs_commit() derives the other domain */
 	if (wq->flags & WQ_UNBOUND) {
 		wq->max_active = effective_max_active;
 		wq->min_active = min(effective_max_active, WQ_DFL_MIN_ACTIVE);
 		wq->saved_min_active = wq->min_active;
+		wq->saved_max_active = effective_max_active;
 	} else {
 		wq->percpu_max_active = effective_max_active;
+		wq->saved_percpu_max_active = effective_max_active;
 	}
-	wq->saved_max_active = effective_max_active;
 }
 
 /**
  * wq_adjust_max_active - update a wq's max_active to the current setting
  * @wq: target workqueue
  *
- * If @wq isn't freezing, set the limit that applies to @wq's backing to the
- * saved_max_active and activate inactive work items accordingly. If @wq is
- * freezing, clear it to zero.
+ * If @wq isn't freezing, publish the saved limits of both domains and activate
+ * inactive work items accordingly. If @wq is freezing, clear them to zero.
  */
 static void wq_adjust_max_active(struct workqueue_struct *wq)
 {
+	int new_max, new_min, new_percpu_max;
 	bool activated;
-	int new_max, new_min;
 
 	lockdep_assert_held(&wq->mutex);
 
 	if ((wq->flags & WQ_FREEZABLE) && workqueue_freezing) {
 		new_max = 0;
 		new_min = 0;
+		new_percpu_max = 0;
 	} else {
 		new_max = wq->saved_max_active;
 		new_min = wq->saved_min_active;
+		new_percpu_max = wq->saved_percpu_max_active;
 	}
 
 	/*
-	 * Update the limit and then kick inactive work items if more active
+	 * Update both domains and then kick inactive work items if more active
 	 * work items are allowed. This doesn't break work item ordering
 	 * because new work items are always queued behind existing inactive
 	 * work items if there are any.
+	 *
+	 * Both domains are kept current so that a pwq on either kind of pool
+	 * has a valid limit.
 	 */
-	if (wq->flags & WQ_UNBOUND) {
-		if (wq->max_active == new_max && wq->min_active == new_min)
-			return;
+	if (wq->max_active == new_max && wq->min_active == new_min &&
+	    wq->percpu_max_active == new_percpu_max)
+		return;
 
-		WRITE_ONCE(wq->max_active, new_max);
-		WRITE_ONCE(wq->min_active, new_min);
-		wq_update_node_max_active(wq, -1);
-	} else {
-		if (wq->percpu_max_active == new_max)
-			return;
+	WRITE_ONCE(wq->max_active, new_max);
+	WRITE_ONCE(wq->min_active, new_min);
+	WRITE_ONCE(wq->percpu_max_active, new_percpu_max);
 
-		WRITE_ONCE(wq->percpu_max_active, new_max);
-	}
+	if (wq->flags & WQ_UNBOUND)
+		wq_update_node_max_active(wq, -1);
 
 	if (new_max == 0)
 		return;
@@ -6125,6 +6178,37 @@ static void wq_adjust_max_active(struct workqueue_struct *wq)
 	} while (activated);
 }
 
+/*
+ * Derive the saved limits of the max_active domain @wq's backend doesn't use
+ * from those of the one it does, with the current number of usable CPUs, and
+ * publish both.
+ */
+static void wq_rescale_max_active(struct workqueue_struct *wq)
+{
+	int nr_cpus;
+
+	lockdep_assert_held(&wq->mutex);
+
+	/* BH workqueues have no max_active limit in either domain */
+	if (wq->flags & WQ_BH)
+		return;
+
+	nr_cpus = wq_nr_effective_cpus(wq);
+
+	if (wq->flags & WQ_UNBOUND) {
+		WRITE_ONCE(wq->saved_percpu_max_active,
+			   wq_scale_to_percpu(wq->saved_max_active,
+					      wq->saved_min_active, nr_cpus));
+	} else {
+		WRITE_ONCE(wq->saved_max_active,
+			   wq_scale_to_unbound(wq->saved_percpu_max_active,
+					       nr_cpus));
+		wq->saved_min_active = wq->saved_percpu_max_active;
+	}
+
+	wq_adjust_max_active(wq);
+}
+
 __printf(1, 0)
 static struct workqueue_struct *__alloc_workqueue(const char *fmt,
 						  unsigned int flags,
@@ -6459,11 +6543,14 @@ void workqueue_set_max_active(struct workqueue_struct *wq, int max_active)
 
 	mutex_lock(&wq->mutex);
 
-	wq->saved_max_active = max_active;
-	if (wq->flags & WQ_UNBOUND)
+	if (wq->flags & WQ_UNBOUND) {
+		WRITE_ONCE(wq->saved_max_active, max_active);
 		wq->saved_min_active = min(wq->saved_min_active, max_active);
+	} else {
+		WRITE_ONCE(wq->saved_percpu_max_active, max_active);
+	}
 
-	wq_adjust_max_active(wq);
+	wq_rescale_max_active(wq);
 
 	mutex_unlock(&wq->mutex);
 }
@@ -6492,7 +6579,7 @@ void workqueue_set_min_active(struct workqueue_struct *wq, int min_active)
 
 	mutex_lock(&wq->mutex);
 	wq->saved_min_active = clamp(min_active, 0, wq->saved_max_active);
-	wq_adjust_max_active(wq);
+	wq_rescale_max_active(wq);
 	mutex_unlock(&wq->mutex);
 }
 
@@ -7587,7 +7674,7 @@ static ssize_t max_active_show(struct device *dev,
 {
 	struct workqueue_struct *wq = dev_to_wq(dev);
 
-	return scnprintf(buf, PAGE_SIZE, "%d\n", wq->saved_max_active);
+	return scnprintf(buf, PAGE_SIZE, "%d\n", wq_user_max_active(wq));
 }
 
 static ssize_t max_active_store(struct device *dev,
@@ -8822,11 +8909,12 @@ void __init workqueue_init_topology(void)
 	list_for_each_entry(wq, &workqueues, list) {
 		for_each_online_cpu(cpu)
 			unbound_wq_update_pwq(wq, cpu);
-		if (wq->flags & WQ_UNBOUND) {
-			mutex_lock(&wq->mutex);
+		mutex_lock(&wq->mutex);
+		if (wq->flags & WQ_UNBOUND)
 			wq_update_node_max_active(wq, -1);
-			mutex_unlock(&wq->mutex);
-		}
+		/* early workqueues counted only the boot CPU */
+		wq_rescale_max_active(wq);
+		mutex_unlock(&wq->mutex);
 	}
 
 	mutex_unlock(&wq_pool_mutex);

-- 
2.53.0-Meta


  parent reply	other threads:[~2026-10-29 12:36 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-29 12:33 [PATCH wq/for-7.5 v4 0/2] workqueue: Keep max_active and percpu_max_active in sync Breno Leitao
2026-10-29 12:33 ` [PATCH wq/for-7.5 v4 1/2] workqueue: Move the initial max_active setup into a helper Breno Leitao
2026-10-29 12:33 ` Breno Leitao [this message]
2026-10-29 19:07   ` [PATCH wq/for-7.5 v4 2/2] workqueue: Keep the limits of both max_active domains current 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=20261029-wq_final-v4-2-2985ea9f24fd@debian.org \
    --to=leitao@debian.org \
    --cc=jiangshanlai@gmail.com \
    --cc=kernel-team@meta.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=marco.crivellari@suse.com \
    --cc=tj@kernel.org \
    /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®