* [PATCH wq/for-7.5 v4 0/2] workqueue: Keep max_active and percpu_max_active in sync
@ 2026-10-29 12:33 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 ` [PATCH wq/for-7.5 v4 2/2] workqueue: Keep the limits of both max_active domains current Breno Leitao
0 siblings, 2 replies; 4+ messages in thread
From: Breno Leitao @ 2026-10-29 12:33 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan
Cc: linux-kernel, marco.crivellari, Breno Leitao, kernel-team
Commit 27db9dd7f84f3a ("workqueue: Give percpu workqueues their own
max_active") gave the percpu backend a limit of its own, so a workqueue
now carries both percpu_max_active and max_active.
__alloc_workqueue() only ever sets the pair matching the backend the
workqueue is created on, so the other stays at zero for its lifetime.
Nothing reads "the other scope" max_active today, so this is groundwork
rather than a fix.
Two patches. The first moves the initial limit setup out of
__alloc_workqueue() into wq_init_max_active().
The second sets both domains rather than one, according to Tejun's
recommendation:
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 in the effective unbound mask, 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)
It rederives the other domain when the cpumask changes and once CPU
topology is known, as workqueues created before smp_init() count only
the boot CPU.
This shouldn't change the behaviour of workqueue for now, but keeps the
groundwork correct for the PERCPU affinity scope work it's meant to
enable.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
Changes in v4:
- Folded the topology rescale (v3 3/3) into patch 2 (Tejun)
- Added wq_rescale_max_active(): it derives the other domain from the
backend's own saved limits and publishes both through
wq_adjust_max_active(). The setters, apply_wqattrs_commit() and
workqueue_init_topology() call it, so the topology pass no longer
passes a derived limit back in, resets min_active or writes the live
limits directly (Tejun)
- Rederive in apply_wqattrs_commit() so N follows cpumask changes, and
set only the backend's own limits in wq_init_max_active() (Tejun)
- Test WQ_UNBOUND in wq_nr_effective_cpus() instead of installed_pwq()
(Tejun)
- Pair the READ_ONCE() in wq_user_max_active() with WRITE_ONCE() (Tejun)
- Reworded the wq_adjust_max_active() comment as a contract (Tejun)
- Patch 2 changelog: spelled out the conversions, explained the
wq_user_max_active() readers and what is not resampled, dropped the
CM/RCU wording (Tejun)
- Dropped the PS from patch 1's changelog (Tejun)
- Rebased on wq/for-7.5
- Link to v3: https://patch.msgid.link/20261009-wq_final-v3-0-eb0b095b9de3@debian.org
Changes in v3:
- Dropped the silly percpu_concurrency_managed attribute patch.
- Fixed N to come from the workqueue's own unbound_effective_cpumask()
- Added a patch rederiving both domains once workqueue_init_topology()
knows the real CPU count.
- Handled BH in one place: wq_init_max_active() sets the limits of a BH
workqueue through wq_init_bh_max_active() and returns, instead of
giving BH its own branch further down (Tejun)
- Set min_active to percpu_max_active when deriving from a percpu limit
instead of min(percpu_max_active, max_active), as max_active can never
be lower (Tejun)
- Changelog of patch 2 now refers to the new PERCPU affinity scope
rather than WQ_AFFN_CPU (Tejun)
- Link to v2: https://patch.msgid.link/20260925-wq_final-v2-0-860ee052169e@debian.org
Changes in v2:
- Cut the series down to the three patches that change nothing
observable. The runtime scaling, the affinity scopes, the backend
rework, the concurrency management attribute and the sysfs files are
all held back.
- Set both max_active domains instead of holding them equal, deriving
one from the other rather than duplicating the value (Tejun)
- Link to v1:
https://patch.msgid.link/20260918-wq_final-v1-0-5c43c08a26bc@debian.org
---
Breno Leitao (2):
workqueue: Move the initial max_active setup into a helper
workqueue: Keep the limits of both max_active domains current
kernel/workqueue.c | 186 ++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 141 insertions(+), 45 deletions(-)
---
base-commit: e92304919f8104ef1452a6389fc54311e857b953
base-tree: git://git.kernel.org/pub/scm/linux/kernel/git/tj/wq.git for-7.5
change-id: 20260914-wq_final-bcfbcd3b0f79
Best regards,
--
Breno Leitao <leitao@debian.org>
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH wq/for-7.5 v4 1/2] workqueue: Move the initial max_active setup into a helper
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 ` Breno Leitao
2026-10-29 12:33 ` [PATCH wq/for-7.5 v4 2/2] workqueue: Keep the limits of both max_active domains current Breno Leitao
1 sibling, 0 replies; 4+ messages in thread
From: Breno Leitao @ 2026-10-29 12:33 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan
Cc: linux-kernel, marco.crivellari, Breno Leitao, kernel-team
Extract the max/min active assignment into a helper, simplifying
__alloc_workqueue() and keeping the next patch easier to review and reason
about.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
kernel/workqueue.c | 46 +++++++++++++++++++++++++++-------------------
1 file changed, 27 insertions(+), 19 deletions(-)
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index f2e8071a6f4eb0..826a64c6055d0c 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -6031,6 +6031,32 @@ static int init_rescuer(struct workqueue_struct *wq)
return 0;
}
+static void wq_init_max_active(struct workqueue_struct *wq, int max_active)
+{
+ int effective_max_active;
+
+ if (wq->flags & WQ_BH) {
+ /*
+ * 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);
+ }
+
+ 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;
+ } else {
+ wq->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
@@ -6156,27 +6182,9 @@ static struct workqueue_struct *__alloc_workqueue(const char *fmt,
flags &= ~WQ_PERCPU;
}
- if (flags & WQ_BH) {
- /*
- * BH workqueues always share a single execution context per CPU
- * and don't impose any max_active limit.
- */
- max_active = INT_MAX;
- } else {
- max_active = max_active ?: WQ_DFL_ACTIVE;
- max_active = wq_clamp_max_active(max_active, flags, wq->name);
- }
-
/* init wq */
wq->flags = flags;
- if (flags & WQ_UNBOUND) {
- wq->max_active = max_active;
- wq->min_active = min(max_active, WQ_DFL_MIN_ACTIVE);
- wq->saved_min_active = wq->min_active;
- } else {
- wq->percpu_max_active = max_active;
- }
- wq->saved_max_active = max_active;
+ wq_init_max_active(wq, max_active);
mutex_init(&wq->mutex);
atomic_set(&wq->nr_pwqs_to_flush, 0);
INIT_LIST_HEAD(&wq->pwqs);
--
2.53.0-Meta
^ permalink raw reply [flat|nested] 4+ messages in thread
* [PATCH wq/for-7.5 v4 2/2] workqueue: Keep the limits of both max_active domains current
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
2026-10-29 19:07 ` Tejun Heo
1 sibling, 1 reply; 4+ messages in thread
From: Breno Leitao @ 2026-10-29 12:33 UTC (permalink / raw)
To: Tejun Heo, Lai Jiangshan
Cc: linux-kernel, marco.crivellari, Breno Leitao, kernel-team
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
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH wq/for-7.5 v4 2/2] workqueue: Keep the limits of both max_active domains current
2026-10-29 12:33 ` [PATCH wq/for-7.5 v4 2/2] workqueue: Keep the limits of both max_active domains current Breno Leitao
@ 2026-10-29 19:07 ` Tejun Heo
0 siblings, 0 replies; 4+ messages in thread
From: Tejun Heo @ 2026-10-29 19:07 UTC (permalink / raw)
To: Breno Leitao; +Cc: Lai Jiangshan, Marco Crivellari, linux-kernel, kernel-team
Hello, Breno.
On Thu, Oct 29, 2026 at 05:33:46AM -0700, Breno Leitao wrote:
> 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.
I think the two domains should be separated completely at the interface,
each with a fixed meaning regardless of the backend in use, as discussed in
the RFC thread. Otherwise, it gets too confusing. On the sysfs side, the
existing max_active would become compat only, with max_active_percpu and
max_active_total as the main interfaces. The kernel API would match:
workqueue_set_max_active_percpu() and workqueue_set_max_active_total(),
with the conversion to the other domain done in those setters.
This patch only makes sense as a way to get there, and it's difficult to
tell whether it's in line with that without the actual conversion.
> +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);
> +}
Maybe write it as "if () return X; else return Y;"? Neither branch is the
special case. The same goes for wq_nr_effective_cpus() and wherever the
choice between the two domains ends up.
> - if (!from_cancel && (wq->saved_max_active == 1 || wq->rescuer))
> + if (!from_cancel && (wq_user_max_active(wq) == 1 || wq->rescuer))
I wonder whether this condition is right. It should trigger when the
effective max_active is 1, whether the workqueue is in percpu or unbound
mode, but this only looks at the domain the workqueue was created in. Once a
workqueue can switch, the limit in force may be the derived one, and that
can reach 1 even for a workqueue created with a higher max_active, e.g.
after min_active is lowered.
Maybe this means there should be further restrictions on how low max_active
can be pushed. For example, a workqueue created with max_active > 1
shouldn't be driven down to 1 in either domain, however it gets adjusted, so
that whether a workqueue can end up at 1 is decided by the max_active it was
created with. One way could be a floor of 2 in the setters and the
derivation for workqueues created with max_active > 1, while one created
with max_active 1 can still go back to 1, e.g. after being raised. That's
just one option though.
> +static int wq_nr_online_cpus(const struct cpumask *mask)
> +{
> + return max(cpumask_weight_and(mask, cpu_online_mask), 1);
> +}
> ...
> +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);
> +}
I'm not sure single-use helpers without any comment are an improvement. They
seem to obscure more than they help. Maybe open-code them with comments
explaining why? That would include wq_nr_online_cpus(),
wq_nr_effective_cpus(), wq_scale_to_unbound(), wq_scale_to_percpu() and
wq_init_bh_max_active().
> +/*
> + * 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)
Would it make sense to make the rescale a part of wq_adjust_max_active() and
use that everywhere, on init, any config change, freeze and so on? With the
separate setters, the workqueue could remember which domain was set last,
and the derivation would always go from that one with the current CPU count,
so calling it again couldn't ratchet the limits. BH would stay out, and
hotplug could keep using the saved values.
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-10-29 19:07 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
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 ` [PATCH wq/for-7.5 v4 2/2] workqueue: Keep the limits of both max_active domains current Breno Leitao
2026-10-29 19:07 ` Tejun Heo
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®