From: Liviu Dudau <liviu.dudau@arm.com>
To: Tvrtko Ursulin <tvrtko.ursulin@igalia.com>
Cc: linux-kernel@vger.kernel.org, dri-devel@lists.freedesktop.org,
kernel-dev@igalia.com,
Boris Brezillon <boris.brezillon@collabora.com>,
Bradley Morgan <include@grrlz.net>, Chia-I Wu <olv@google.com>,
Matthew Brost <matthew.brost@intel.com>,
Steven Price <steven.price@arm.com>, Tejun Heo <tj@kernel.org>
Subject: Re: [PATCH v7 3/4] workqueue: Add support for real-time workers
Date: Fri, 23 Oct 2026 14:23:13 +0100 [thread overview]
Message-ID: <attfwedt31kPayyo@e142607> (raw)
In-Reply-To: <20261022093848.5727-4-tvrtko.ursulin@igalia.com>
On Thu, Oct 22, 2026 at 10:38:47AM +0100, Tvrtko Ursulin wrote:
> For use cases such as the DRM scheduler submitting work to the GPU on
> behalf of low latency userspace applications, where latter have sufficient
> privileges to have had successfully obtained realtime Vulkan global
> priority, competing with random background CPU load can create large
> latency spikes which gets in the way of a smooth user experience.
>
> For these situations the existing WQ_HIGHPRI does not bring a noticeable
> improvement and a stronger hint is needed.
>
> Lets add WQ_RT which creates workers with a SCHED_FIFO scheduling class to
> improve this.
>
> We use a minimum priority level since we only care about winning the
> contest against normal background CPU load.
>
> Signed-off-by: Tvrtko Ursulin <tvrtko.ursulin@igalia.com>
> Cc: Boris Brezillon <boris.brezillon@collabora.com>
> Cc: Bradley Morgan <include@grrlz.net>
> Cc: Chia-I Wu <olv@google.com>
> Cc: Liviu Dudau <liviu.dudau@arm.com>
> Cc: Matthew Brost <matthew.brost@intel.com>
> Cc: Steven Price <steven.price@arm.com>
> Cc: Tejun Heo <tj@kernel.org>
Reviewed-by: Liviu Dudau <liviu.dudau@arm.com>
Best regards,
Liviu
> ---
> v2:
> * Limit WQ_RTPRI to unbound workqueues and make it have strict CPU
> affinitity. (Tejun)
> * Fixed commit message typos. (AI)
> * Fixed sysfs handling, max_active setting and user modified nice
> application. (AI)
>
> v3:
> * Fix worker->pool null pointer dereference race by moving the
> global decrement to detach_dying_workers().
> * Rebase for upstream changes.
>
> v4:
> * Fixed onion unwind.
> * Moved affinity setting to default attributes.
>
> v5:
> * Dropped global and local limits.
> * Documented in workqueue.rst.
> * Added NR_WQ_ATTRIBUTES.
> * Reverted BH handling changes.
>
> v6:
> * Dropped separate attr->prio in favour of RTPRI_NICE_LEVEL checks. (Tejun)
> * Reworked on top of tj/for-7.4.
>
> v7:
> * Convert to attrs->prio encoded analoguous to task_struct->prio.
> * Rename flag to WQ_PRIO and do not re-order enums.
> * Forbid WQ_RT affinity modifications via sysfs.
> * Added wq_dump.py support.
>
> v8:
> * Fix max rt prio confusion.
> * Initialize priority also in alloc_workqueue_attrs_noprof.
> * Restrict rt cpumask modifications.
>
> v9:
> * Reject WQ_RT | __WQ_ORDERED.
> * Fixed spelling in a comment.
> * Align RT_PRIORITY with sched_set_fifo_low.
> * Simplify wq_sysfs_unbound_group_visible and use it to deny modifications.
> * Print -rt from pr_cont_pool_info.
> * Keep wq_dump.py more backward compatible.
> * Disallow RT prio change via apply_workqueue_attrs().
> ---
> Documentation/core-api/workqueue.rst | 8 +++
> include/linux/workqueue.h | 5 +-
> kernel/workqueue.c | 92 +++++++++++++++++++---------
> tools/workqueue/wq_dump.py | 9 ++-
> 4 files changed, 83 insertions(+), 31 deletions(-)
>
> diff --git a/Documentation/core-api/workqueue.rst b/Documentation/core-api/workqueue.rst
> index bb770f556568..d699c3832b19 100644
> --- a/Documentation/core-api/workqueue.rst
> +++ b/Documentation/core-api/workqueue.rst
> @@ -225,6 +225,14 @@ resources, scheduled and executed.
> each other. Each maintains its separate pool of workers and
> implements concurrency management among its workers.
>
> +``WQ_RT``
> + Real-time priority workqueues must be created as unbound and will be
> + configured with the strict CPU affinity set. Their worker threads use the FIFO
> + scheduling policy with the lowest applicable priority.
> +
> + To be used sparingly for use cases such as the real-time GPU rendering
> + contexts accessible to privileged clients.
> +
> ``WQ_CPU_INTENSIVE``
> Work items of a CPU intensive wq do not contribute to the
> concurrency level. In other words, runnable CPU intensive
> diff --git a/include/linux/workqueue.h b/include/linux/workqueue.h
> index a283766a192a..da9843251018 100644
> --- a/include/linux/workqueue.h
> +++ b/include/linux/workqueue.h
> @@ -147,9 +147,9 @@ enum wq_affn_scope {
> */
> struct workqueue_attrs {
> /**
> - * @nice: nice level
> + * @prio: priority encoded analogous to task_struct->prio.
> */
> - int nice;
> + int prio;
>
> /**
> * @cpumask: allowed CPUs
> @@ -404,6 +404,7 @@ enum wq_flags {
> */
> WQ_POWER_EFFICIENT = 1 << 7,
> WQ_PERCPU = 1 << 8, /* bound to a specific cpu */
> + WQ_RT = 1 << 9, /* real-time priority, valid only with WQ_UNBOUND */
>
> __WQ_DESTROYING = 1 << 15, /* internal: workqueue is destroying */
> __WQ_DRAINING = 1 << 16, /* internal: workqueue is draining */
> diff --git a/kernel/workqueue.c b/kernel/workqueue.c
> index c634b20f1874..b34881830b8b 100644
> --- a/kernel/workqueue.c
> +++ b/kernel/workqueue.c
> @@ -47,6 +47,7 @@
> #include <linux/jhash.h>
> #include <linux/hashtable.h>
> #include <linux/rculist.h>
> +#include <linux/sched/rt.h>
> #include <linux/nodemask.h>
> #include <linux/moduleparam.h>
> #include <linux/uaccess.h>
> @@ -126,7 +127,8 @@ enum wq_internal_consts {
> * all cpus. Give MIN_NICE.
> */
> RESCUER_NICE_LEVEL = MIN_NICE,
> - HIGHPRI_NICE_LEVEL = MIN_NICE,
> + HIGHPRI_PRIORITY = NICE_TO_PRIO(MIN_NICE),
> + RT_PRIORITY = MAX_RT_PRIO - 2, /* sched_set_fifo_low() */
>
> WQ_NAME_LEN = 32,
> WORKER_ID_LEN = 10 + WQ_NAME_LEN, /* "kworker/R-" + WQ_NAME_LEN */
> @@ -1275,7 +1277,7 @@ static bool assign_work(struct work_struct *work, struct worker *worker,
>
> static struct irq_work *bh_pool_irq_work(struct worker_pool *pool)
> {
> - int high = pool->attrs->nice == HIGHPRI_NICE_LEVEL ? 1 : 0;
> + int high = pool->attrs->prio == HIGHPRI_PRIORITY ? 1 : 0;
>
> return &per_cpu(bh_pool_irq_works, pool->cpu)[high];
> }
> @@ -1290,7 +1292,7 @@ static void kick_bh_pool(struct worker_pool *pool)
> return;
> }
> #endif
> - if (pool->attrs->nice == HIGHPRI_NICE_LEVEL)
> + if (pool->attrs->prio == HIGHPRI_PRIORITY)
> raise_softirq_irqoff(HI_SOFTIRQ);
> else
> raise_softirq_irqoff(TASKLET_SOFTIRQ);
> @@ -2959,7 +2961,8 @@ static int format_worker_id(char *buf, size_t size, struct worker *worker,
> if (pool->cpu >= 0)
> return scnprintf(buf, size, "kworker/%d:%d%s",
> pool->cpu, worker->id,
> - pool->attrs->nice < 0 ? "H" : "");
> + pool->attrs->prio < NICE_TO_PRIO(0) ?
> + "H" : "");
> else
> return scnprintf(buf, size, "kworker/u%d:%d",
> pool->id, worker->id);
> @@ -3018,7 +3021,12 @@ static struct worker *create_worker(struct worker_pool *pool)
> goto fail;
> }
>
> - set_user_nice(worker->task, pool->attrs->nice);
> + if (rt_prio(pool->attrs->prio))
> + sched_set_fifo_low(worker->task);
> + else
> + set_user_nice(worker->task,
> + PRIO_TO_NICE(pool->attrs->prio));
> +
> kthread_bind_mask(worker->task, pool_allowed_cpus(pool));
> }
>
> @@ -3910,7 +3918,7 @@ static void bh_worker(struct worker *worker)
>
> if (budget_exhausted)
> trace_workqueue_bh_budget_yield(pool, restarts, timeout,
> - pool->attrs->nice == HIGHPRI_NICE_LEVEL);
> + pool->attrs->prio == HIGHPRI_PRIORITY);
> }
>
> /*
> @@ -3969,7 +3977,7 @@ static void drain_dead_softirq_workfn(struct work_struct *work)
> * don't hog this CPU's BH.
> */
> if (repeat) {
> - if (pool->attrs->nice == HIGHPRI_NICE_LEVEL)
> + if (pool->attrs->prio == HIGHPRI_PRIORITY)
> queue_work(system_bh_highpri_wq, work);
> else
> queue_work(system_bh_wq, work);
> @@ -4001,7 +4009,7 @@ void workqueue_softirq_dead(unsigned int cpu)
> dead_work.pool = pool;
> init_completion(&dead_work.done);
>
> - if (pool->attrs->nice == HIGHPRI_NICE_LEVEL)
> + if (pool->attrs->prio == HIGHPRI_PRIORITY)
> queue_work(system_bh_highpri_wq, &dead_work.work);
> else
> queue_work(system_bh_wq, &dead_work.work);
> @@ -5005,6 +5013,7 @@ struct workqueue_attrs *alloc_workqueue_attrs_noprof(void)
> goto fail;
>
> cpumask_copy(attrs->cpumask, cpu_possible_mask);
> + attrs->prio = DEFAULT_PRIO;
> attrs->affn_scope = WQ_AFFN_DFL;
> return attrs;
> fail:
> @@ -5015,7 +5024,7 @@ struct workqueue_attrs *alloc_workqueue_attrs_noprof(void)
> static void copy_workqueue_attrs(struct workqueue_attrs *to,
> const struct workqueue_attrs *from)
> {
> - to->nice = from->nice;
> + to->prio = from->prio;
> cpumask_copy(to->cpumask, from->cpumask);
> cpumask_copy(to->__pod_cpumask, from->__pod_cpumask);
> to->affn_strict = from->affn_strict;
> @@ -5046,7 +5055,7 @@ static u32 wqattrs_hash(const struct workqueue_attrs *attrs)
> {
> u32 hash = 0;
>
> - hash = jhash_1word(attrs->nice, hash);
> + hash = jhash_1word(attrs->prio, hash);
> hash = jhash_1word(attrs->affn_strict, hash);
> hash = jhash(cpumask_bits(attrs->__pod_cpumask),
> BITS_TO_LONGS(nr_cpumask_bits) * sizeof(long), hash);
> @@ -5060,7 +5069,7 @@ static u32 wqattrs_hash(const struct workqueue_attrs *attrs)
> static bool wqattrs_equal(const struct workqueue_attrs *a,
> const struct workqueue_attrs *b)
> {
> - if (a->nice != b->nice)
> + if (a->prio != b->prio)
> return false;
> if (a->affn_strict != b->affn_strict)
> return false;
> @@ -5711,6 +5720,13 @@ static void
> sanitize_immutable_workqueue_attrs(struct workqueue_struct *wq,
> struct workqueue_attrs *attrs)
> {
> + if (wq->flags & WQ_RT) {
> + attrs->prio = RT_PRIORITY;
> + /* Strict CPU affinity for low latency execution. */
> + attrs->affn_scope = WQ_AFFN_CPU;
> + attrs->affn_strict = true;
> + }
> +
> if (wq->flags & __WQ_ORDERED)
> attrs->ordered = true;
> }
> @@ -5721,7 +5737,7 @@ sanitize_mutable_workqueue_attrs(struct workqueue_struct *wq,
> struct workqueue_attrs *attrs)
> {
> if (wq->flags & WQ_HIGHPRI)
> - attrs->nice = HIGHPRI_NICE_LEVEL;
> + attrs->prio = HIGHPRI_PRIORITY;
> }
>
> static void
> @@ -6126,6 +6142,11 @@ static struct workqueue_struct *__alloc_workqueue(const char *fmt,
> return NULL;
> }
>
> + if (WARN_ON_ONCE((flags & WQ_RT) &&
> + ((flags & (WQ_HIGHPRI | WQ_UNBOUND | __WQ_ORDERED)) !=
> + WQ_UNBOUND)))
> + return NULL;
> +
> /* see the comment above the definition of WQ_POWER_EFFICIENT */
> if ((flags & WQ_POWER_EFFICIENT) && wq_power_efficient)
> flags = (flags & ~WQ_PERCPU) | WQ_UNBOUND;
> @@ -6682,9 +6703,11 @@ static void pr_cont_pool_info(struct worker_pool *pool)
> pr_cont(" flags=0x%x", pool->flags);
> if (pool->flags & POOL_BH)
> pr_cont(" bh%s",
> - pool->attrs->nice == HIGHPRI_NICE_LEVEL ? "-hi" : "");
> + pool->attrs->prio == HIGHPRI_PRIORITY ? "-hi" : "");
> + else if (rt_prio(pool->attrs->prio))
> + pr_cont(" rt");
> else
> - pr_cont(" nice=%d", pool->attrs->nice);
> + pr_cont(" nice=%d", PRIO_TO_NICE(pool->attrs->prio));
> }
>
> static void pr_cont_worker_id(struct worker *worker)
> @@ -6693,7 +6716,7 @@ static void pr_cont_worker_id(struct worker *worker)
>
> if (pool->flags & POOL_BH)
> pr_cont("bh%s",
> - pool->attrs->nice == HIGHPRI_NICE_LEVEL ? "-hi" : "");
> + pool->attrs->prio == HIGHPRI_PRIORITY ? "-hi" : "");
> else
> pr_cont("%d%s", task_pid_nr(worker->task),
> worker->rescue_wq ? "(RESCUER)" : "");
> @@ -7617,7 +7640,11 @@ static ssize_t nice_show(struct device *dev, struct device_attribute *attr,
> int written;
>
> mutex_lock(&wq->mutex);
> - written = scnprintf(buf, PAGE_SIZE, "%d\n", wq->attrs->nice);
> + if (wq->attrs->prio == RT_PRIORITY)
> + written = scnprintf(buf, PAGE_SIZE, "rt\n");
> + else
> + written = scnprintf(buf, PAGE_SIZE, "%d\n",
> + PRIO_TO_NICE(wq->attrs->prio));
> mutex_unlock(&wq->mutex);
>
> return written;
> @@ -7643,19 +7670,21 @@ static ssize_t nice_store(struct device *dev, struct device_attribute *attr,
> {
> struct workqueue_struct *wq = dev_to_wq(dev);
> struct workqueue_attrs *attrs;
> - int ret = -ENOMEM;
> + int ret, nice = 0;
> +
> + if (sscanf(buf, "%d", &nice) != 1 || nice < MIN_NICE || nice > MAX_NICE)
> + return -EINVAL;
>
> mutex_lock(&wq_pool_mutex);
>
> attrs = wq_sysfs_prep_attrs(wq);
> - if (!attrs)
> + if (!attrs) {
> + ret = -ENOMEM;
> goto out_unlock;
> + }
>
> - if (sscanf(buf, "%d", &attrs->nice) == 1 &&
> - attrs->nice >= MIN_NICE && attrs->nice <= MAX_NICE)
> - ret = apply_workqueue_attrs_locked(wq, attrs);
> - else
> - ret = -EINVAL;
> + attrs->prio = NICE_TO_PRIO(nice);
> + ret = apply_workqueue_attrs_locked(wq, attrs);
>
> out_unlock:
> mutex_unlock(&wq_pool_mutex);
> @@ -7797,6 +7826,13 @@ static umode_t wq_sysfs_unbound_group_visible(struct kobject *kobj,
> if (!(wq->flags & WQ_UNBOUND))
> return 0;
>
> + /* Do not allow priority and affinity changes for RT workers. */
> + if ((wq->flags & WQ_RT) &&
> + (attr == &dev_attr_nice.attr ||
> + attr == &dev_attr_affinity_scope.attr ||
> + attr == &dev_attr_affinity_strict.attr))
> + return 0444;
> +
> return attr->mode;
> }
>
> @@ -8321,13 +8357,13 @@ static void __init restrict_unbound_cpumask(const char *name, const struct cpuma
> cpumask_and(wq_unbound_cpumask, wq_unbound_cpumask, mask);
> }
>
> -static void __init init_cpu_worker_pool(struct worker_pool *pool, int cpu, int nice)
> +static void __init init_cpu_worker_pool(struct worker_pool *pool, int cpu, int prio)
> {
> BUG_ON(init_worker_pool(pool));
> pool->cpu = cpu;
> cpumask_copy(pool->attrs->cpumask, cpumask_of(cpu));
> cpumask_copy(pool->attrs->__pod_cpumask, cpumask_of(cpu));
> - pool->attrs->nice = nice;
> + pool->attrs->prio = prio;
> pool->attrs->affn_strict = true;
> pool->node = cpu_to_node(cpu);
>
> @@ -8350,7 +8386,7 @@ static void __init init_cpu_worker_pool(struct worker_pool *pool, int cpu, int n
> void __init workqueue_init_early(void)
> {
> struct wq_pod_type *pt = &wq_pod_types[WQ_AFFN_SYSTEM];
> - int std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL };
> + int std_prio[NR_STD_WORKER_POOLS] = { DEFAULT_PRIO, HIGHPRI_PRIORITY };
> void (*irq_work_fns[NR_STD_WORKER_POOLS])(struct irq_work *) =
> { bh_pool_kick_normal, bh_pool_kick_highpri };
> int i, cpu;
> @@ -8402,7 +8438,7 @@ void __init workqueue_init_early(void)
>
> i = 0;
> for_each_bh_worker_pool(pool, cpu) {
> - init_cpu_worker_pool(pool, cpu, std_nice[i]);
> + init_cpu_worker_pool(pool, cpu, std_prio[i]);
> pool->flags |= POOL_BH;
> init_irq_work(bh_pool_irq_work(pool), irq_work_fns[i]);
> i++;
> @@ -8410,7 +8446,7 @@ void __init workqueue_init_early(void)
>
> i = 0;
> for_each_cpu_worker_pool(pool, cpu)
> - init_cpu_worker_pool(pool, cpu, std_nice[i++]);
> + init_cpu_worker_pool(pool, cpu, std_prio[i++]);
> }
>
> system_wq = alloc_workqueue("events", WQ_PERCPU | __WQ_DEPRECATED, 0);
> diff --git a/tools/workqueue/wq_dump.py b/tools/workqueue/wq_dump.py
> index 9313ebe0c525..650d4cddf25d 100644
> --- a/tools/workqueue/wq_dump.py
> +++ b/tools/workqueue/wq_dump.py
> @@ -122,6 +122,8 @@ POOL_BH = prog['POOL_BH']
> WQ_NAME_LEN = prog['WQ_NAME_LEN'].value_()
> cpumask_str_len = len(cpumask_str(wq_unbound_cpumask))
>
> +rt_prio = prog.constant('RT_PRIORITY', filename='kernel/workqueue.c')
> +
> print('Affinity Scopes')
> print('===============')
>
> @@ -163,7 +165,12 @@ for pi, pool in idr_for_each(worker_pool_idr):
>
> for pi, pool in idr_for_each(worker_pool_idr):
> pool = drgn.Object(prog, 'struct worker_pool', address=pool)
> - print(f'pool[{pi:0{max_pool_id_len}}] flags=0x{pool.flags.value_():02x} ref={pool.refcnt.value_():{max_ref_len}} nice={pool.attrs.nice.value_():3} ', end='')
> + nice = pool.attrs.prio.value_()
> + if nice == rt_prio:
> + nice = 'rt'
> + else:
> + nice = nice - 120 # PRIO_TO_NICE
> + print(f'pool[{pi:0{max_pool_id_len}}] flags=0x{pool.flags.value_():02x} ref={pool.refcnt.value_():{max_ref_len}} nice={nice:3} ', end='')
> print(f'idle/workers={pool.nr_idle.value_():3}/{pool.nr_workers.value_():3} ', end='')
> if pool.cpu >= 0:
> print(f'cpu={pool.cpu.value_():3}', end='')
> --
> 2.56.0
>
--
====================
| I would like to |
| fix the world, |
| but they're not |
| giving me the |
\ source code! /
---------------
¯\_(ツ)_/¯
next prev parent reply other threads:[~2026-10-23 13:23 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-22 9:38 [PATCH v7 0/4] Realtime workqueues and panthor realtime submission Tvrtko Ursulin
2026-10-22 9:38 ` [PATCH v7 1/4] workqueue: Simplify unbound sysfs attribute registration Tvrtko Ursulin
2026-10-23 13:14 ` Liviu Dudau
2026-10-23 21:04 ` Tejun Heo
2026-10-22 9:38 ` [PATCH v7 2/4] workqueue: Separate sysfs mutable vs immutable wq attribute sanitizing Tvrtko Ursulin
2026-10-23 13:17 ` Liviu Dudau
2026-10-23 21:28 ` Tejun Heo
2026-10-22 9:38 ` [PATCH v7 3/4] workqueue: Add support for real-time workers Tvrtko Ursulin
2026-10-23 13:23 ` Liviu Dudau [this message]
2026-10-23 21:28 ` Tejun Heo
2026-10-22 9:38 ` [PATCH v7 4/4] drm/panthor: Create per queue priority workqueues Tvrtko Ursulin
2026-10-23 13:26 ` Liviu Dudau
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=attfwedt31kPayyo@e142607 \
--to=liviu.dudau@arm.com \
--cc=boris.brezillon@collabora.com \
--cc=dri-devel@lists.freedesktop.org \
--cc=include@grrlz.net \
--cc=kernel-dev@igalia.com \
--cc=linux-kernel@vger.kernel.org \
--cc=matthew.brost@intel.com \
--cc=olv@google.com \
--cc=steven.price@arm.com \
--cc=tj@kernel.org \
--cc=tvrtko.ursulin@igalia.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®