* [PATCH v4 0/2] sched_ext: lockless peek operation for DSQs @ 2025-10-15 1:57 Ryan Newton 2025-10-15 1:57 ` [PATCH v4 1/2] sched_ext: Add " Ryan Newton 2025-10-15 1:57 ` [PATCH v4 2/2] sched_ext: Add a selftest for scx_bpf_dsq_peek Ryan Newton 0 siblings, 2 replies; 7+ messages in thread From: Ryan Newton @ 2025-10-15 1:57 UTC (permalink / raw) To: linux-kernel; +Cc: sched-ext, tj, arighi, rrnewton, newton This allows sched_ext schedulers an inexpensive operation to peek at the first element in a queue (DSQ), without creating an iterator and acquiring the lock on that queue. Note that manual testing has thus far included a modified version of the example qmap scheduler that exercises peek, as well as a modified modified LAVD (from the SCX repo) that exercises peek. The attached test passes >1000 stress tests when run in concurrent VMs, and when run sequentially on the host kernel. Presently, tested on the below workstation and server processors. - AMD Ryzen Threadripper PRO 7975WX 32-Cores - AMD EPYC 9D64 88-Core Processor Initial experiments indicate a substantial speedup (on schbench) when running an SCX scheduler with per-cpu DSQs and peeking each queue to retrieve the task with the minimum vruntime across all the CPUs. --- Changes in v4: - review comments (from arighi@nvidia.com) addressed - make the test much lighter weight with 4 rather than 100 workers Changes in v3: - inline helpers and simplify - coding style tweaks - link: https://lore.kernel.org/lkml/20251006170403.3584204-1-rrnewton@gmail.com/ Changes in v2: - make peek() only work for user DSQs and error otherwise - added a stress test component to the selftest that performs many peeks - responded to review comments from tj@kernel.org and arighi@nvidia.com - link: https://lore.kernel.org/lkml/20251003195408.675527-1-rrnewton@gmail.com/ v1 link: https://lore.kernel.org/lkml/20251002025722.3420916-1-rrnewton@gmail.com/ Ryan Newton (2): sched_ext: Add lockless peek operation for DSQs sched_ext: Add a selftest for scx_bpf_dsq_peek include/linux/sched/ext.h | 1 + kernel/sched/ext.c | 56 +++- tools/sched_ext/include/scx/common.bpf.h | 1 + tools/sched_ext/include/scx/compat.bpf.h | 18 ++ tools/testing/selftests/sched_ext/Makefile | 1 + .../selftests/sched_ext/peek_dsq.bpf.c | 251 ++++++++++++++++++ tools/testing/selftests/sched_ext/peek_dsq.c | 222 ++++++++++++++++ 7 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 tools/testing/selftests/sched_ext/peek_dsq.bpf.c create mode 100644 tools/testing/selftests/sched_ext/peek_dsq.c -- 2.51.0 ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v4 1/2] sched_ext: Add lockless peek operation for DSQs 2025-10-15 1:57 [PATCH v4 0/2] sched_ext: lockless peek operation for DSQs Ryan Newton @ 2025-10-15 1:57 ` Ryan Newton 2025-10-15 9:46 ` Christian Loehle 2025-10-15 1:57 ` [PATCH v4 2/2] sched_ext: Add a selftest for scx_bpf_dsq_peek Ryan Newton 1 sibling, 1 reply; 7+ messages in thread From: Ryan Newton @ 2025-10-15 1:57 UTC (permalink / raw) To: linux-kernel; +Cc: sched-ext, tj, arighi, rrnewton, newton From: Ryan Newton <newton@meta.com> The builtin DSQ queue data structures are meant to be used by a wide range of different sched_ext schedulers with different demands on these data structures. They might be per-cpu with low-contention, or high-contention shared queues. Unfortunately, DSQs have a coarse-grained lock around the whole data structure. Without going all the way to a lock-free, more scalable implementation, a small step we can take to reduce lock contention is to allow a lockless, small-fixed-cost peek at the head of the queue. This change allows certain custom SCX schedulers to cheaply peek at queues, e.g. during load balancing, before locking them. But it represents a few extra memory operations to update the pointer each time the DSQ is modified, including a memory barrier on ARM so the write appears correctly ordered. This commit adds a first_task pointer field which is updated atomically when the DSQ is modified, and allows any thread to peek at the head of the queue without holding the lock. Signed-off-by: Ryan Newton <newton@meta.com> Reviewed-by: Andrea Righi <arighi@nvidia.com> --- include/linux/sched/ext.h | 1 + kernel/sched/ext.c | 56 +++++++++++++++++++++++- tools/sched_ext/include/scx/common.bpf.h | 1 + tools/sched_ext/include/scx/compat.bpf.h | 18 ++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/include/linux/sched/ext.h b/include/linux/sched/ext.h index d82b7a9b0658..81478d4ae782 100644 --- a/include/linux/sched/ext.h +++ b/include/linux/sched/ext.h @@ -58,6 +58,7 @@ enum scx_dsq_id_flags { */ struct scx_dispatch_q { raw_spinlock_t lock; + struct task_struct __rcu *first_task; /* lockless peek at head */ struct list_head list; /* tasks in dispatch order */ struct rb_root priq; /* used to order by p->scx.dsq_vtime */ u32 nr; diff --git a/kernel/sched/ext.c b/kernel/sched/ext.c index 4b1467d3541a..430eaa7cfbbe 100644 --- a/kernel/sched/ext.c +++ b/kernel/sched/ext.c @@ -965,8 +965,11 @@ static void dispatch_enqueue(struct scx_sched *sch, struct scx_dispatch_q *dsq, container_of(rbp, struct task_struct, scx.dsq_priq); list_add(&p->scx.dsq_list.node, &prev->scx.dsq_list.node); + /* first task unchanged - no update needed */ } else { list_add(&p->scx.dsq_list.node, &dsq->list); + /* not builtin and new task is at head - use fastpath */ + rcu_assign_pointer(dsq->first_task, p); } } else { /* a FIFO DSQ shouldn't be using PRIQ enqueuing */ @@ -974,10 +977,19 @@ static void dispatch_enqueue(struct scx_sched *sch, struct scx_dispatch_q *dsq, scx_error(sch, "DSQ ID 0x%016llx already had PRIQ-enqueued tasks", dsq->id); - if (enq_flags & (SCX_ENQ_HEAD | SCX_ENQ_PREEMPT)) + if (enq_flags & (SCX_ENQ_HEAD | SCX_ENQ_PREEMPT)) { list_add(&p->scx.dsq_list.node, &dsq->list); - else + /* new task inserted at head - use fastpath */ + if (!(dsq->id & SCX_DSQ_FLAG_BUILTIN)) + rcu_assign_pointer(dsq->first_task, p); + } else { + bool was_empty; + + was_empty = list_empty(&dsq->list); list_add_tail(&p->scx.dsq_list.node, &dsq->list); + if (was_empty && !(dsq->id & SCX_DSQ_FLAG_BUILTIN)) + rcu_assign_pointer(dsq->first_task, p); + } } /* seq records the order tasks are queued, used by BPF DSQ iterator */ @@ -1032,6 +1044,13 @@ static void task_unlink_from_dsq(struct task_struct *p, p->scx.dsq_flags &= ~SCX_TASK_DSQ_ON_PRIQ; } + if (!(dsq->id & SCX_DSQ_FLAG_BUILTIN) && dsq->first_task == p) { + struct task_struct *first_task; + + first_task = nldsq_next_task(dsq, NULL, false); + rcu_assign_pointer(dsq->first_task, first_task); + } + list_del_init(&p->scx.dsq_list.node); dsq_mod_nr(dsq, -1); } @@ -6278,6 +6297,38 @@ __bpf_kfunc void bpf_iter_scx_dsq_destroy(struct bpf_iter_scx_dsq *it) kit->dsq = NULL; } +/** + * scx_bpf_dsq_peek - Lockless peek at the first element. + * @dsq_id: DSQ to examine. + * + * Read the first element in the DSQ. This is semantically equivalent to using + * the DSQ iterator, but is lockfree. + * + * Returns the pointer, or NULL indicates an empty queue OR internal error. + */ +__bpf_kfunc struct task_struct *scx_bpf_dsq_peek(u64 dsq_id) +{ + struct scx_sched *sch; + struct scx_dispatch_q *dsq; + + sch = rcu_dereference(scx_root); + if (unlikely(!sch)) + return NULL; + + if (unlikely(dsq_id & SCX_DSQ_FLAG_BUILTIN)) { + scx_error(sch, "peek disallowed on builtin DSQ 0x%llx", dsq_id); + return NULL; + } + + dsq = find_user_dsq(sch, dsq_id); + if (unlikely(!dsq)) { + scx_error(sch, "peek on non-existent DSQ 0x%llx", dsq_id); + return NULL; + } + + return rcu_dereference(dsq->first_task); +} + __bpf_kfunc_end_defs(); static s32 __bstr_format(struct scx_sched *sch, u64 *data_buf, char *line_buf, @@ -6837,6 +6888,7 @@ BTF_ID_FLAGS(func, scx_bpf_task_set_dsq_vtime, KF_RCU); BTF_ID_FLAGS(func, scx_bpf_kick_cpu) BTF_ID_FLAGS(func, scx_bpf_dsq_nr_queued) BTF_ID_FLAGS(func, scx_bpf_destroy_dsq) +BTF_ID_FLAGS(func, scx_bpf_dsq_peek, KF_RCU_PROTECTED | KF_RET_NULL) BTF_ID_FLAGS(func, bpf_iter_scx_dsq_new, KF_ITER_NEW | KF_RCU_PROTECTED) BTF_ID_FLAGS(func, bpf_iter_scx_dsq_next, KF_ITER_NEXT | KF_RET_NULL) BTF_ID_FLAGS(func, bpf_iter_scx_dsq_destroy, KF_ITER_DESTROY) diff --git a/tools/sched_ext/include/scx/common.bpf.h b/tools/sched_ext/include/scx/common.bpf.h index eb3c99445cb3..e65b1eb668ea 100644 --- a/tools/sched_ext/include/scx/common.bpf.h +++ b/tools/sched_ext/include/scx/common.bpf.h @@ -74,6 +74,7 @@ u32 scx_bpf_reenqueue_local(void) __ksym; void scx_bpf_kick_cpu(s32 cpu, u64 flags) __ksym; s32 scx_bpf_dsq_nr_queued(u64 dsq_id) __ksym; void scx_bpf_destroy_dsq(u64 dsq_id) __ksym; +struct task_struct *scx_bpf_dsq_peek(u64 dsq_id) __ksym __weak; int bpf_iter_scx_dsq_new(struct bpf_iter_scx_dsq *it, u64 dsq_id, u64 flags) __ksym __weak; struct task_struct *bpf_iter_scx_dsq_next(struct bpf_iter_scx_dsq *it) __ksym __weak; void bpf_iter_scx_dsq_destroy(struct bpf_iter_scx_dsq *it) __ksym __weak; diff --git a/tools/sched_ext/include/scx/compat.bpf.h b/tools/sched_ext/include/scx/compat.bpf.h index e487c10b5e07..619a16f0d39a 100644 --- a/tools/sched_ext/include/scx/compat.bpf.h +++ b/tools/sched_ext/include/scx/compat.bpf.h @@ -26,6 +26,24 @@ int bpf_cpumask_populate(struct cpumask *dst, void *src, size_t src__sz) __ksym (bpf_ksym_exists(bpf_cpumask_populate) ? \ (bpf_cpumask_populate(cpumask, src, size__sz)) : -EOPNOTSUPP) +/* + * v6.19: Introduce lockless peek API for user DSQs. + * + * Preserve the following macro until v6.21. + */ +static inline struct task_struct *__COMPAT_scx_bpf_dsq_peek(u64 dsq_id) +{ + struct task_struct *p = NULL; + struct bpf_iter_scx_dsq it; + + if (bpf_ksym_exists(scx_bpf_dsq_peek)) + return scx_bpf_dsq_peek(dsq_id); + if (!bpf_iter_scx_dsq_new(&it, dsq_id, 0)) + p = bpf_iter_scx_dsq_next(&it); + bpf_iter_scx_dsq_destroy(&it); + return p; +} + /** * __COMPAT_is_enq_cpu_selected - Test if SCX_ENQ_CPU_SELECTED is on * in a compatible way. We will preserve this __COMPAT helper until v6.16. -- 2.51.0 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH v4 1/2] sched_ext: Add lockless peek operation for DSQs 2025-10-15 1:57 ` [PATCH v4 1/2] sched_ext: Add " Ryan Newton @ 2025-10-15 9:46 ` Christian Loehle 2025-10-15 14:05 ` Ryan Newton 2025-10-15 16:02 ` Andrea Righi 0 siblings, 2 replies; 7+ messages in thread From: Christian Loehle @ 2025-10-15 9:46 UTC (permalink / raw) To: Ryan Newton, linux-kernel; +Cc: sched-ext, tj, arighi, newton On 10/15/25 02:57, Ryan Newton wrote: > From: Ryan Newton <newton@meta.com> > > The builtin DSQ queue data structures are meant to be used by a wide > range of different sched_ext schedulers with different demands on these > data structures. They might be per-cpu with low-contention, or > high-contention shared queues. Unfortunately, DSQs have a coarse-grained > lock around the whole data structure. Without going all the way to a > lock-free, more scalable implementation, a small step we can take to > reduce lock contention is to allow a lockless, small-fixed-cost peek at > the head of the queue. > > This change allows certain custom SCX schedulers to cheaply peek at > queues, e.g. during load balancing, before locking them. But it > represents a few extra memory operations to update the pointer each > time the DSQ is modified, including a memory barrier on ARM so the write > appears correctly ordered. > > This commit adds a first_task pointer field which is updated > atomically when the DSQ is modified, and allows any thread to peek at > the head of the queue without holding the lock. > > Signed-off-by: Ryan Newton <newton@meta.com> > Reviewed-by: Andrea Righi <arighi@nvidia.com> > --- > include/linux/sched/ext.h | 1 + > kernel/sched/ext.c | 56 +++++++++++++++++++++++- > tools/sched_ext/include/scx/common.bpf.h | 1 + > tools/sched_ext/include/scx/compat.bpf.h | 18 ++++++++ > 4 files changed, 74 insertions(+), 2 deletions(-) > > diff --git a/include/linux/sched/ext.h b/include/linux/sched/ext.h > index d82b7a9b0658..81478d4ae782 100644 > --- a/include/linux/sched/ext.h > +++ b/include/linux/sched/ext.h > @@ -58,6 +58,7 @@ enum scx_dsq_id_flags { > */ > struct scx_dispatch_q { > raw_spinlock_t lock; > + struct task_struct __rcu *first_task; /* lockless peek at head */ > struct list_head list; /* tasks in dispatch order */ > struct rb_root priq; /* used to order by p->scx.dsq_vtime */ > u32 nr; > diff --git a/kernel/sched/ext.c b/kernel/sched/ext.c > index 4b1467d3541a..430eaa7cfbbe 100644 > --- a/kernel/sched/ext.c > +++ b/kernel/sched/ext.c > @@ -965,8 +965,11 @@ static void dispatch_enqueue(struct scx_sched *sch, struct scx_dispatch_q *dsq, > container_of(rbp, struct task_struct, > scx.dsq_priq); > list_add(&p->scx.dsq_list.node, &prev->scx.dsq_list.node); > + /* first task unchanged - no update needed */ > } else { > list_add(&p->scx.dsq_list.node, &dsq->list); > + /* not builtin and new task is at head - use fastpath */ > + rcu_assign_pointer(dsq->first_task, p); > } > } else { > /* a FIFO DSQ shouldn't be using PRIQ enqueuing */ > @@ -974,10 +977,19 @@ static void dispatch_enqueue(struct scx_sched *sch, struct scx_dispatch_q *dsq, > scx_error(sch, "DSQ ID 0x%016llx already had PRIQ-enqueued tasks", > dsq->id); > > - if (enq_flags & (SCX_ENQ_HEAD | SCX_ENQ_PREEMPT)) > + if (enq_flags & (SCX_ENQ_HEAD | SCX_ENQ_PREEMPT)) { > list_add(&p->scx.dsq_list.node, &dsq->list); > - else > + /* new task inserted at head - use fastpath */ > + if (!(dsq->id & SCX_DSQ_FLAG_BUILTIN)) > + rcu_assign_pointer(dsq->first_task, p); > + } else { > + bool was_empty; > + > + was_empty = list_empty(&dsq->list); > list_add_tail(&p->scx.dsq_list.node, &dsq->list); > + if (was_empty && !(dsq->id & SCX_DSQ_FLAG_BUILTIN)) > + rcu_assign_pointer(dsq->first_task, p); > + } > } > > /* seq records the order tasks are queued, used by BPF DSQ iterator */ > @@ -1032,6 +1044,13 @@ static void task_unlink_from_dsq(struct task_struct *p, > p->scx.dsq_flags &= ~SCX_TASK_DSQ_ON_PRIQ; > } > > + if (!(dsq->id & SCX_DSQ_FLAG_BUILTIN) && dsq->first_task == p) { > + struct task_struct *first_task; > + > + first_task = nldsq_next_task(dsq, NULL, false); > + rcu_assign_pointer(dsq->first_task, first_task); > + } > + > list_del_init(&p->scx.dsq_list.node); > dsq_mod_nr(dsq, -1); > } > @@ -6278,6 +6297,38 @@ __bpf_kfunc void bpf_iter_scx_dsq_destroy(struct bpf_iter_scx_dsq *it) > kit->dsq = NULL; > } > > +/** > + * scx_bpf_dsq_peek - Lockless peek at the first element. > + * @dsq_id: DSQ to examine. > + * > + * Read the first element in the DSQ. This is semantically equivalent to using > + * the DSQ iterator, but is lockfree. > + * > + * Returns the pointer, or NULL indicates an empty queue OR internal error. > + */ > +__bpf_kfunc struct task_struct *scx_bpf_dsq_peek(u64 dsq_id) > +{ Obviously there's no guarantee that scx_bpf_dsq_peek() will return the task you're gonna get when moving a task to another DSQ or a local one. It's pretty obvious from the kernel perspective, but I wonder if it's worth documenting here too. > + struct scx_sched *sch; > + struct scx_dispatch_q *dsq; > + > + sch = rcu_dereference(scx_root); > + if (unlikely(!sch)) > + return NULL; > + > + if (unlikely(dsq_id & SCX_DSQ_FLAG_BUILTIN)) { > + scx_error(sch, "peek disallowed on builtin DSQ 0x%llx", dsq_id); > + return NULL; > + } > + > + dsq = find_user_dsq(sch, dsq_id); > + if (unlikely(!dsq)) { > + scx_error(sch, "peek on non-existent DSQ 0x%llx", dsq_id); > + return NULL; > + } > + > + return rcu_dereference(dsq->first_task); > +} > + > __bpf_kfunc_end_defs(); > > static s32 __bstr_format(struct scx_sched *sch, u64 *data_buf, char *line_buf, > @@ -6837,6 +6888,7 @@ BTF_ID_FLAGS(func, scx_bpf_task_set_dsq_vtime, KF_RCU); > BTF_ID_FLAGS(func, scx_bpf_kick_cpu) > BTF_ID_FLAGS(func, scx_bpf_dsq_nr_queued) > BTF_ID_FLAGS(func, scx_bpf_destroy_dsq) > +BTF_ID_FLAGS(func, scx_bpf_dsq_peek, KF_RCU_PROTECTED | KF_RET_NULL) > BTF_ID_FLAGS(func, bpf_iter_scx_dsq_new, KF_ITER_NEW | KF_RCU_PROTECTED) > BTF_ID_FLAGS(func, bpf_iter_scx_dsq_next, KF_ITER_NEXT | KF_RET_NULL) > BTF_ID_FLAGS(func, bpf_iter_scx_dsq_destroy, KF_ITER_DESTROY) > diff --git a/tools/sched_ext/include/scx/common.bpf.h b/tools/sched_ext/include/scx/common.bpf.h > index eb3c99445cb3..e65b1eb668ea 100644 > --- a/tools/sched_ext/include/scx/common.bpf.h > +++ b/tools/sched_ext/include/scx/common.bpf.h > @@ -74,6 +74,7 @@ u32 scx_bpf_reenqueue_local(void) __ksym; > void scx_bpf_kick_cpu(s32 cpu, u64 flags) __ksym; > s32 scx_bpf_dsq_nr_queued(u64 dsq_id) __ksym; > void scx_bpf_destroy_dsq(u64 dsq_id) __ksym; > +struct task_struct *scx_bpf_dsq_peek(u64 dsq_id) __ksym __weak; > int bpf_iter_scx_dsq_new(struct bpf_iter_scx_dsq *it, u64 dsq_id, u64 flags) __ksym __weak; > struct task_struct *bpf_iter_scx_dsq_next(struct bpf_iter_scx_dsq *it) __ksym __weak; > void bpf_iter_scx_dsq_destroy(struct bpf_iter_scx_dsq *it) __ksym __weak; > diff --git a/tools/sched_ext/include/scx/compat.bpf.h b/tools/sched_ext/include/scx/compat.bpf.h > index e487c10b5e07..619a16f0d39a 100644 > --- a/tools/sched_ext/include/scx/compat.bpf.h > +++ b/tools/sched_ext/include/scx/compat.bpf.h > @@ -26,6 +26,24 @@ int bpf_cpumask_populate(struct cpumask *dst, void *src, size_t src__sz) __ksym > (bpf_ksym_exists(bpf_cpumask_populate) ? \ > (bpf_cpumask_populate(cpumask, src, size__sz)) : -EOPNOTSUPP) > > +/* > + * v6.19: Introduce lockless peek API for user DSQs. > + * > + * Preserve the following macro until v6.21. > + */ > +static inline struct task_struct *__COMPAT_scx_bpf_dsq_peek(u64 dsq_id) > +{ > + struct task_struct *p = NULL; > + struct bpf_iter_scx_dsq it; > + > + if (bpf_ksym_exists(scx_bpf_dsq_peek)) > + return scx_bpf_dsq_peek(dsq_id); > + if (!bpf_iter_scx_dsq_new(&it, dsq_id, 0)) > + p = bpf_iter_scx_dsq_next(&it); > + bpf_iter_scx_dsq_destroy(&it); > + return p; > +} > + I think there's an argument to be made for this to just return NULL on !bpf_ksym_exists(scx_bpf_dsq_peek), the caller needs to handle that anyway and at least it's guaranteed to be lockfree then? I don't have a strong leaning either way, probably is fine if documented. ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4 1/2] sched_ext: Add lockless peek operation for DSQs 2025-10-15 9:46 ` Christian Loehle @ 2025-10-15 14:05 ` Ryan Newton 2025-10-15 16:02 ` Andrea Righi 1 sibling, 0 replies; 7+ messages in thread From: Ryan Newton @ 2025-10-15 14:05 UTC (permalink / raw) To: Christian Loehle; +Cc: linux-kernel, sched-ext, tj, arighi, newton On Wed, Oct 15, 2025 at 5:46 AM Christian Loehle <christian.loehle@arm.com> wrote: > > +/** > > + * scx_bpf_dsq_peek - Lockless peek at the first element. > > + * @dsq_id: DSQ to examine. > > + * > > + * Read the first element in the DSQ. This is semantically equivalent to using > > + * the DSQ iterator, but is lockfree. > > + * > > + * Returns the pointer, or NULL indicates an empty queue OR internal error. > > + */ > > +__bpf_kfunc struct task_struct *scx_bpf_dsq_peek(u64 dsq_id) > > +{ > > Obviously there's no guarantee that scx_bpf_dsq_peek() will return the task you're gonna > get when moving a task to another DSQ or a local one. It's pretty obvious from the kernel > perspective, but I wonder if it's worth documenting here too. Hello Christian, thanks for looking at the patches. Absolutely, whenever using a lockless operation you have to watch out for TOCTOU bugs. In the schedulers we've experimented with, doing a sweep of peeks just gives you a higher chance of getting what you're looking for when you subsequently lock the remote DSQ for popping/stealing a task. I can add a reminder in the comment on the function. > > +/* > > + * v6.19: Introduce lockless peek API for user DSQs. > > + * > > + * Preserve the following macro until v6.21. > > + */ > > +static inline struct task_struct *__COMPAT_scx_bpf_dsq_peek(u64 dsq_id) > > +{ > > + struct task_struct *p = NULL; > > + struct bpf_iter_scx_dsq it; > > + > > + if (bpf_ksym_exists(scx_bpf_dsq_peek)) > > + return scx_bpf_dsq_peek(dsq_id); > > + if (!bpf_iter_scx_dsq_new(&it, dsq_id, 0)) > > + p = bpf_iter_scx_dsq_next(&it); > > + bpf_iter_scx_dsq_destroy(&it); > > + return p; > > +} > > + > I think there's an argument to be made for this to just return NULL on > !bpf_ksym_exists(scx_bpf_dsq_peek), the caller needs to handle that anyway > and at least it's guaranteed to be lockfree then? > I don't have a strong leaning either way, probably is fine if documented. We went back and forth on this patch a lot, and did explore versions where peek is "best effort" and its semantics allow it to return NULL for any reason. But that provides much less information to the caller, because now NULL is supposed to indicate that the queue was empty at a point in time. For that reason we ended up with this version where peek provides both positive info (something is/was there) and negative (nothing is/was there). Best, -Ryan ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH v4 1/2] sched_ext: Add lockless peek operation for DSQs 2025-10-15 9:46 ` Christian Loehle 2025-10-15 14:05 ` Ryan Newton @ 2025-10-15 16:02 ` Andrea Righi 1 sibling, 0 replies; 7+ messages in thread From: Andrea Righi @ 2025-10-15 16:02 UTC (permalink / raw) To: Christian Loehle; +Cc: Ryan Newton, linux-kernel, sched-ext, tj, newton On Wed, Oct 15, 2025 at 10:46:52AM +0100, Christian Loehle wrote: > On 10/15/25 02:57, Ryan Newton wrote: ... > > --- a/tools/sched_ext/include/scx/compat.bpf.h > > +++ b/tools/sched_ext/include/scx/compat.bpf.h > > @@ -26,6 +26,24 @@ int bpf_cpumask_populate(struct cpumask *dst, void *src, size_t src__sz) __ksym > > (bpf_ksym_exists(bpf_cpumask_populate) ? \ > > (bpf_cpumask_populate(cpumask, src, size__sz)) : -EOPNOTSUPP) > > > > +/* > > + * v6.19: Introduce lockless peek API for user DSQs. > > + * > > + * Preserve the following macro until v6.21. > > + */ > > +static inline struct task_struct *__COMPAT_scx_bpf_dsq_peek(u64 dsq_id) > > +{ > > + struct task_struct *p = NULL; > > + struct bpf_iter_scx_dsq it; > > + > > + if (bpf_ksym_exists(scx_bpf_dsq_peek)) > > + return scx_bpf_dsq_peek(dsq_id); > > + if (!bpf_iter_scx_dsq_new(&it, dsq_id, 0)) > > + p = bpf_iter_scx_dsq_next(&it); > > + bpf_iter_scx_dsq_destroy(&it); > > + return p; > > +} > > + > I think there's an argument to be made for this to just return NULL on > !bpf_ksym_exists(scx_bpf_dsq_peek), the caller needs to handle that anyway > and at least it's guaranteed to be lockfree then? > I don't have a strong leaning either way, probably is fine if documented. If we return NULL, the caller cannot tell whether the DSQ is empty or the kfunc is missing. Honestly, I don't think being lockless is a strict requirement from a scheduler's perspective, but if there's a case where it matters, we can add a separate helper, i.e., scx_bpf_dsq_peek_nolock(), that explicitly returns NULL when scx_bpf_dsq_peek() is unavailable. Thanks, -Andrea ^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH v4 2/2] sched_ext: Add a selftest for scx_bpf_dsq_peek 2025-10-15 1:57 [PATCH v4 0/2] sched_ext: lockless peek operation for DSQs Ryan Newton 2025-10-15 1:57 ` [PATCH v4 1/2] sched_ext: Add " Ryan Newton @ 2025-10-15 1:57 ` Ryan Newton 2025-10-15 9:41 ` Christian Loehle 1 sibling, 1 reply; 7+ messages in thread From: Ryan Newton @ 2025-10-15 1:57 UTC (permalink / raw) To: linux-kernel; +Cc: sched-ext, tj, arighi, rrnewton, newton From: Ryan Newton <newton@meta.com> This is the most basic unit test: make sure an empty queue peeks as empty, and when we put one element in the queue, make sure peek returns that element. However, even this simple test is a little complicated by the different behavior of scx_bpf_dsq_insert in different calling contexts: - insert is for direct dispatch in enqueue - insert is delayed when called from select_cpu In this case we split the insert and the peek that verifies the result between enqueue/dispatch. Note: An alternative would be to call `scx_bpf_dsq_move_to_local` on an empty queue, which in turn calls `flush_dispatch_buf`, in order to flush the buffered insert. Unfortunately, this is not viable within the enqueue path, as it attempts a voluntary context switch within an RCU read-side critical section. Signed-off-by: Ryan Newton <newton@meta.com> --- tools/testing/selftests/sched_ext/Makefile | 1 + .../selftests/sched_ext/peek_dsq.bpf.c | 251 ++++++++++++++++++ tools/testing/selftests/sched_ext/peek_dsq.c | 222 ++++++++++++++++ 3 files changed, 474 insertions(+) create mode 100644 tools/testing/selftests/sched_ext/peek_dsq.bpf.c create mode 100644 tools/testing/selftests/sched_ext/peek_dsq.c diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile index 9d9d6b4c38b0..5fe45f9c5f8f 100644 --- a/tools/testing/selftests/sched_ext/Makefile +++ b/tools/testing/selftests/sched_ext/Makefile @@ -174,6 +174,7 @@ auto-test-targets := \ minimal \ numa \ allowed_cpus \ + peek_dsq \ prog_run \ reload_loop \ select_cpu_dfl \ diff --git a/tools/testing/selftests/sched_ext/peek_dsq.bpf.c b/tools/testing/selftests/sched_ext/peek_dsq.bpf.c new file mode 100644 index 000000000000..65e3f688ea39 --- /dev/null +++ b/tools/testing/selftests/sched_ext/peek_dsq.bpf.c @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * A BPF program for testing DSQ operations and peek in particular. + * + * Copyright (c) 2025 Meta Platforms, Inc. and affiliates. + * Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu> + */ + +#include <scx/common.bpf.h> +#include <scx/compat.bpf.h> + +char _license[] SEC("license") = "GPL"; + +UEI_DEFINE(uei); /* Error handling */ + +#define MAX_SAMPLES 100 +#define MAX_CPUS 512 +#define DSQ_POOL_SIZE 8 +int max_samples = MAX_SAMPLES; +int max_cpus = MAX_CPUS; +int dsq_pool_size = DSQ_POOL_SIZE; + +/* Global variables to store test results */ +int dsq_peek_result1 = -1; +long dsq_inserted_pid = -1; +int insert_test_cpu = -1; /* Set to the cpu that performs the test */ +long dsq_peek_result2 = -1; +long dsq_peek_result2_pid = -1; +long dsq_peek_result2_expected = -1; +int test_dsq_id = 1234; /* Use a simple ID like create_dsq example */ +int real_dsq_id = 1235; /* DSQ for normal operation */ +int enqueue_count = -1; +int dispatch_count = -1; +bool debug_ksym_exists; + +/* DSQ pool for stress testing */ +int dsq_pool_base_id = 2000; +int phase1_complete = -1; +int total_peek_attempts = -1; +int successful_peeks = -1; + +/* BPF map for sharing peek results with userspace */ +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, MAX_SAMPLES); + __type(key, u32); + __type(value, long); +} peek_results SEC(".maps"); + +static int get_random_dsq_id(void) +{ + u64 time = bpf_ktime_get_ns(); + + return dsq_pool_base_id + (time % DSQ_POOL_SIZE); +} + +static void record_peek_result(long pid) +{ + u32 slot_key; + long *slot_pid_ptr; + int ix; + + if (pid <= 0) + return; + + /* Find an empty slot or one with the same PID */ + bpf_for(ix, 0, 10) { + slot_key = (pid + ix) % MAX_SAMPLES; + slot_pid_ptr = bpf_map_lookup_elem(&peek_results, &slot_key); + if (!slot_pid_ptr) + continue; + + if (*slot_pid_ptr == -1 || *slot_pid_ptr == pid) { + *slot_pid_ptr = pid; + break; + } + } +} + +/* Scan all DSQs in the pool and try to move a task to local */ +static int scan_dsq_pool(void) +{ + struct task_struct *task; + int moved = 0; + int i; + + bpf_for(i, 0, DSQ_POOL_SIZE) { + int dsq_id = dsq_pool_base_id + i; + + total_peek_attempts++; + + task = __COMPAT_scx_bpf_dsq_peek(dsq_id); + if (task) { + successful_peeks++; + record_peek_result(task->pid); + + /* Try to move this task to local */ + if (!moved && scx_bpf_dsq_move_to_local(dsq_id) == 0) { + moved = 1; + break; + } + } + } + return moved; +} + +/* Struct_ops scheduler for testing DSQ peek operations */ +void BPF_STRUCT_OPS(peek_dsq_enqueue, struct task_struct *p, u64 enq_flags) +{ + struct task_struct *peek_result; + int last_insert_test_cpu, cpu; + + enqueue_count++; + cpu = bpf_get_smp_processor_id(); + last_insert_test_cpu = __sync_val_compare_and_swap(&insert_test_cpu, -1, cpu); + + /* Phase 1: Simple insert-then-peek test (only on first task) */ + if (last_insert_test_cpu == -1) { + bpf_printk("peek_dsq_enqueue beginning phase 1 peek test on cpu %d", cpu); + + /* Test 1: Peek empty DSQ - should return NULL */ + peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id); + dsq_peek_result1 = (long)peek_result; /* Should be 0 (NULL) */ + + /* Test 2: Insert task into test DSQ for testing in dispatch callback */ + dsq_inserted_pid = p->pid; + scx_bpf_dsq_insert(p, test_dsq_id, 0, enq_flags); + dsq_peek_result2_expected = (long)p; /* Expected the task we just inserted */ + } else if (!phase1_complete) { + /* Still in phase 1, use real DSQ */ + scx_bpf_dsq_insert(p, real_dsq_id, 0, enq_flags); + } else { + /* Phase 2: Random DSQ insertion for stress testing */ + int random_dsq_id = get_random_dsq_id(); + + scx_bpf_dsq_insert(p, random_dsq_id, 0, enq_flags); + } +} + +void BPF_STRUCT_OPS(peek_dsq_dispatch, s32 cpu, struct task_struct *prev) +{ + dispatch_count++; + + /* Phase 1: Complete the simple peek test if we inserted a task but + * haven't tested peek yet + */ + if (insert_test_cpu == cpu && dsq_peek_result2 == -1) { + struct task_struct *peek_result; + + bpf_printk("peek_dsq_dispatch completing phase 1 peek test on cpu %d", cpu); + + /* Test 3: Peek DSQ after insert - should return the task we inserted */ + peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id); + /* Store the PID of the peeked task for comparison */ + dsq_peek_result2 = (long)peek_result; + dsq_peek_result2_pid = peek_result ? peek_result->pid : -1; + + /* Now consume the task since we've peeked at it */ + scx_bpf_dsq_move_to_local(test_dsq_id); + + /* Mark phase 1 as complete */ + phase1_complete = 1; + bpf_printk("Phase 1 complete, starting phase 2 stress testing"); + } else if (!phase1_complete) { + /* Still in phase 1, use real DSQ */ + scx_bpf_dsq_move_to_local(real_dsq_id); + } else { + /* Phase 2: Scan all DSQs in the pool and try to move a task */ + if (!scan_dsq_pool()) { + /* No tasks found in DSQ pool, fall back to real DSQ */ + scx_bpf_dsq_move_to_local(real_dsq_id); + } + } +} + +s32 BPF_STRUCT_OPS_SLEEPABLE(peek_dsq_init) +{ + s32 err; + int i; + + /* Always set debug values so we can see which version we're using */ + debug_ksym_exists = bpf_ksym_exists(scx_bpf_dsq_peek) ? 1 : 0; + + /* Initialize state first */ + insert_test_cpu = -1; + enqueue_count = 0; + dispatch_count = 0; + phase1_complete = 0; + total_peek_attempts = 0; + successful_peeks = 0; + + /* Create the test and real DSQs */ + err = scx_bpf_create_dsq(test_dsq_id, -1); + if (err) { + scx_bpf_error("Failed to create DSQ %d: %d", test_dsq_id, err); + return err; + } + err = scx_bpf_create_dsq(real_dsq_id, -1); + if (err) { + scx_bpf_error("Failed to create DSQ %d: %d", test_dsq_id, err); + return err; + } + + /* Create the DSQ pool for stress testing */ + bpf_for(i, 0, DSQ_POOL_SIZE) { + int dsq_id = dsq_pool_base_id + i; + + err = scx_bpf_create_dsq(dsq_id, -1); + if (err) { + scx_bpf_error("Failed to create DSQ pool entry %d: %d", dsq_id, err); + return err; + } + } + + /* Initialize the peek results map */ + bpf_for(i, 0, MAX_SAMPLES) { + u32 key = i; + long pid = -1; + + bpf_map_update_elem(&peek_results, &key, &pid, BPF_ANY); + } + + return 0; +} + +void BPF_STRUCT_OPS(peek_dsq_exit, struct scx_exit_info *ei) +{ + int i; + + /* Destroy the primary DSQs */ + scx_bpf_destroy_dsq(test_dsq_id); + scx_bpf_destroy_dsq(real_dsq_id); + + /* Destroy the DSQ pool */ + bpf_for(i, 0, DSQ_POOL_SIZE) { + int dsq_id = dsq_pool_base_id + i; + + scx_bpf_destroy_dsq(dsq_id); + } + + UEI_RECORD(uei, ei); +} + +SEC(".struct_ops.link") +struct sched_ext_ops peek_dsq_ops = { + .enqueue = (void *)peek_dsq_enqueue, + .dispatch = (void *)peek_dsq_dispatch, + .init = (void *)peek_dsq_init, + .exit = (void *)peek_dsq_exit, + .name = "peek_dsq", +}; diff --git a/tools/testing/selftests/sched_ext/peek_dsq.c b/tools/testing/selftests/sched_ext/peek_dsq.c new file mode 100644 index 000000000000..f1c72b898979 --- /dev/null +++ b/tools/testing/selftests/sched_ext/peek_dsq.c @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Test for DSQ operations including create, destroy, and peek operations. + * + * Copyright (c) 2025 Meta Platforms, Inc. and affiliates. + * Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu> + */ +#include <bpf/bpf.h> +#include <scx/common.h> +#include <sys/wait.h> +#include <unistd.h> +#include <pthread.h> +#include <string.h> +#include <sched.h> +#include "peek_dsq.bpf.skel.h" +#include "scx_test.h" + +#define NUM_WORKERS 4 + +static bool workload_running = true; +static pthread_t workload_threads[NUM_WORKERS]; + +/** + * Background workload thread that sleeps and wakes rapidly to exercise + * the scheduler's enqueue operations and ensure DSQ operations get tested. + */ +static void *workload_thread_fn(void *arg) +{ + while (workload_running) { + /* Sleep for a very short time to trigger scheduler activity */ + usleep(1000); /* 1ms sleep */ + /* Yield to ensure we go through the scheduler */ + sched_yield(); + } + return NULL; +} + +static enum scx_test_status setup(void **ctx) +{ + struct peek_dsq *skel; + + skel = peek_dsq__open(); + SCX_FAIL_IF(!skel, "Failed to open"); + SCX_ENUM_INIT(skel); + SCX_FAIL_IF(peek_dsq__load(skel), "Failed to load skel"); + + *ctx = skel; + + return SCX_TEST_PASS; +} + +static int print_observed_pids(struct bpf_map *map, int max_samples, const char *dsq_name) +{ + long count = 0; + + printf("Observed %s DSQ peek pids:\n", dsq_name); + for (int i = 0; i < max_samples; i++) { + long pid; + int err; + + err = bpf_map_lookup_elem(bpf_map__fd(map), &i, &pid); + if (err == 0) { + if (pid == 0) { + printf(" Sample %d: NULL peek\n", i); + } else if (pid > 0) { + printf(" Sample %d: pid %ld\n", i, pid); + count++; + } + } else { + printf(" Sample %d: error reading pid (err=%d)\n", i, err); + } + } + printf("Observed ~%ld pids in the %s DSQ(s)\n", count, dsq_name); + return count; +} + +static enum scx_test_status run(void *ctx) +{ + struct peek_dsq *skel = ctx; + bool failed = false; + int seconds = 3; + int err; + + /* Enable the scheduler to test DSQ operations */ + printf("Enabling scheduler to test DSQ insert operations...\n"); + + struct bpf_link *link = + bpf_map__attach_struct_ops(skel->maps.peek_dsq_ops); + + if (!link) { + SCX_ERR("Failed to attach struct_ops"); + return SCX_TEST_FAIL; + } + + printf("Starting %d background workload threads...\n", NUM_WORKERS); + workload_running = true; + for (int i = 0; i < NUM_WORKERS; i++) { + err = pthread_create(&workload_threads[i], NULL, workload_thread_fn, NULL); + if (err) { + SCX_ERR("Failed to create workload thread %d: %s", i, strerror(err)); + /* Stop already created threads */ + workload_running = false; + for (int j = 0; j < i; j++) + pthread_join(workload_threads[j], NULL); + bpf_link__destroy(link); + return SCX_TEST_FAIL; + } + } + + printf("Waiting for enqueue events.\n"); + sleep(seconds); + while (skel->data->enqueue_count <= 0) { + printf("."); + fflush(stdout); + sleep(1); + seconds++; + if (seconds >= 30) { + printf("\n\u2717 Timeout waiting for enqueue events\n"); + /* Stop workload threads and cleanup */ + workload_running = false; + for (int i = 0; i < NUM_WORKERS; i++) + pthread_join(workload_threads[i], NULL); + bpf_link__destroy(link); + return SCX_TEST_FAIL; + } + } + + workload_running = false; + for (int i = 0; i < NUM_WORKERS; i++) { + err = pthread_join(workload_threads[i], NULL); + if (err) { + SCX_ERR("Failed to join workload thread %d: %s", i, strerror(err)); + bpf_link__destroy(link); + return SCX_TEST_FAIL; + } + } + printf("Background workload threads stopped.\n"); + + SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_NONE)); + + /* Detach the scheduler */ + bpf_link__destroy(link); + + printf("Enqueue/dispatch count over %d seconds: %d / %d\n", seconds, + skel->data->enqueue_count, skel->data->dispatch_count); + printf("Debug: ksym_exists=%d\n", + skel->bss->debug_ksym_exists); + + /* Check DSQ insert result */ + printf("DSQ insert test done on cpu: %d\n", skel->data->insert_test_cpu); + if (skel->data->insert_test_cpu != -1) + printf("\u2713 DSQ insert succeeded !\n"); + else { + printf("\u2717 DSQ insert failed or not attempted\n"); + failed = true; + } + + /* Check DSQ peek results */ + printf(" DSQ peek result 1 (before insert): %d\n", + skel->data->dsq_peek_result1); + if (skel->data->dsq_peek_result1 == 0) + printf("\u2713 DSQ peek verification success: peek returned NULL!\n"); + else { + printf("\u2717 DSQ peek verification failed\n"); + failed = true; + } + + printf(" DSQ peek result 2 (after insert): %ld\n", + skel->data->dsq_peek_result2); + printf(" DSQ peek result 2, expected: %ld\n", + skel->data->dsq_peek_result2_expected); + if (skel->data->dsq_peek_result2 == + skel->data->dsq_peek_result2_expected) + printf("\u2713 DSQ peek verification success: peek returned the inserted task!\n"); + else { + printf("\u2717 DSQ peek verification failed\n"); + failed = true; + } + + printf(" Inserted test task -> pid: %ld\n", skel->data->dsq_inserted_pid); + printf(" DSQ peek result 2 -> pid: %ld\n", skel->data->dsq_peek_result2_pid); + + int pid_count; + + pid_count = print_observed_pids(skel->maps.peek_results, + skel->data->max_samples, "DSQ pool"); + + if (skel->bss->debug_ksym_exists && pid_count == 0) { + printf("\u2717 DSQ pool test failed: no successful peeks in native mode\n"); + failed = true; + } + if (skel->bss->debug_ksym_exists && pid_count > 0) + printf("\u2713 DSQ pool test success: observed successful peeks in native mode\n"); + + if (failed) + return SCX_TEST_FAIL; + else + return SCX_TEST_PASS; +} + +static void cleanup(void *ctx) +{ + struct peek_dsq *skel = ctx; + + if (workload_running) { + workload_running = false; + for (int i = 0; i < NUM_WORKERS; i++) + pthread_join(workload_threads[i], NULL); + } + + peek_dsq__destroy(skel); +} + +struct scx_test peek_dsq = { + .name = "peek_dsq", + .description = + "Test DSQ create/destroy operations and future peek functionality", + .setup = setup, + .run = run, + .cleanup = cleanup, +}; +REGISTER_SCX_TEST(&peek_dsq) -- 2.51.0 ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH v4 2/2] sched_ext: Add a selftest for scx_bpf_dsq_peek 2025-10-15 1:57 ` [PATCH v4 2/2] sched_ext: Add a selftest for scx_bpf_dsq_peek Ryan Newton @ 2025-10-15 9:41 ` Christian Loehle 0 siblings, 0 replies; 7+ messages in thread From: Christian Loehle @ 2025-10-15 9:41 UTC (permalink / raw) To: Ryan Newton, linux-kernel; +Cc: sched-ext, tj, arighi, newton On 10/15/25 02:57, Ryan Newton wrote: > From: Ryan Newton <newton@meta.com> > > This is the most basic unit test: make sure an empty queue peeks as > empty, and when we put one element in the queue, make sure peek returns > that element. > > However, even this simple test is a little complicated by the different > behavior of scx_bpf_dsq_insert in different calling contexts: > - insert is for direct dispatch in enqueue > - insert is delayed when called from select_cpu > > In this case we split the insert and the peek that verifies the > result between enqueue/dispatch. > > Note: An alternative would be to call `scx_bpf_dsq_move_to_local` on an > empty queue, which in turn calls `flush_dispatch_buf`, in order to flush > the buffered insert. Unfortunately, this is not viable within the > enqueue path, as it attempts a voluntary context switch within an RCU > read-side critical section. > > Signed-off-by: Ryan Newton <newton@meta.com> > --- > tools/testing/selftests/sched_ext/Makefile | 1 + > .../selftests/sched_ext/peek_dsq.bpf.c | 251 ++++++++++++++++++ > tools/testing/selftests/sched_ext/peek_dsq.c | 222 ++++++++++++++++ > 3 files changed, 474 insertions(+) > create mode 100644 tools/testing/selftests/sched_ext/peek_dsq.bpf.c > create mode 100644 tools/testing/selftests/sched_ext/peek_dsq.c > > diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile > index 9d9d6b4c38b0..5fe45f9c5f8f 100644 > --- a/tools/testing/selftests/sched_ext/Makefile > +++ b/tools/testing/selftests/sched_ext/Makefile > @@ -174,6 +174,7 @@ auto-test-targets := \ > minimal \ > numa \ > allowed_cpus \ > + peek_dsq \ > prog_run \ > reload_loop \ > select_cpu_dfl \ > diff --git a/tools/testing/selftests/sched_ext/peek_dsq.bpf.c b/tools/testing/selftests/sched_ext/peek_dsq.bpf.c > new file mode 100644 > index 000000000000..65e3f688ea39 > --- /dev/null > +++ b/tools/testing/selftests/sched_ext/peek_dsq.bpf.c > @@ -0,0 +1,251 @@ > +// SPDX-License-Identifier: GPL-2.0 > +/* > + * A BPF program for testing DSQ operations and peek in particular. > + * > + * Copyright (c) 2025 Meta Platforms, Inc. and affiliates. > + * Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu> > + */ > + > +#include <scx/common.bpf.h> > +#include <scx/compat.bpf.h> > + > +char _license[] SEC("license") = "GPL"; > + > +UEI_DEFINE(uei); /* Error handling */ > + > +#define MAX_SAMPLES 100 > +#define MAX_CPUS 512 > +#define DSQ_POOL_SIZE 8 > +int max_samples = MAX_SAMPLES; > +int max_cpus = MAX_CPUS; > +int dsq_pool_size = DSQ_POOL_SIZE; > + > +/* Global variables to store test results */ > +int dsq_peek_result1 = -1; > +long dsq_inserted_pid = -1; > +int insert_test_cpu = -1; /* Set to the cpu that performs the test */ > +long dsq_peek_result2 = -1; > +long dsq_peek_result2_pid = -1; > +long dsq_peek_result2_expected = -1; > +int test_dsq_id = 1234; /* Use a simple ID like create_dsq example */ > +int real_dsq_id = 1235; /* DSQ for normal operation */ > +int enqueue_count = -1; > +int dispatch_count = -1; > +bool debug_ksym_exists; > + > +/* DSQ pool for stress testing */ > +int dsq_pool_base_id = 2000; > +int phase1_complete = -1; > +int total_peek_attempts = -1; > +int successful_peeks = -1; > + > +/* BPF map for sharing peek results with userspace */ > +struct { > + __uint(type, BPF_MAP_TYPE_ARRAY); > + __uint(max_entries, MAX_SAMPLES); > + __type(key, u32); > + __type(value, long); > +} peek_results SEC(".maps"); > + > +static int get_random_dsq_id(void) > +{ > + u64 time = bpf_ktime_get_ns(); > + > + return dsq_pool_base_id + (time % DSQ_POOL_SIZE); > +} > + > +static void record_peek_result(long pid) > +{ > + u32 slot_key; > + long *slot_pid_ptr; > + int ix; > + > + if (pid <= 0) > + return; > + > + /* Find an empty slot or one with the same PID */ > + bpf_for(ix, 0, 10) { > + slot_key = (pid + ix) % MAX_SAMPLES; > + slot_pid_ptr = bpf_map_lookup_elem(&peek_results, &slot_key); > + if (!slot_pid_ptr) > + continue; > + > + if (*slot_pid_ptr == -1 || *slot_pid_ptr == pid) { > + *slot_pid_ptr = pid; > + break; > + } > + } > +} > + > +/* Scan all DSQs in the pool and try to move a task to local */ > +static int scan_dsq_pool(void) > +{ > + struct task_struct *task; > + int moved = 0; > + int i; > + > + bpf_for(i, 0, DSQ_POOL_SIZE) { > + int dsq_id = dsq_pool_base_id + i; > + > + total_peek_attempts++; > + > + task = __COMPAT_scx_bpf_dsq_peek(dsq_id); > + if (task) { > + successful_peeks++; > + record_peek_result(task->pid); > + > + /* Try to move this task to local */ > + if (!moved && scx_bpf_dsq_move_to_local(dsq_id) == 0) { > + moved = 1; > + break; > + } > + } > + } > + return moved; > +} > + > +/* Struct_ops scheduler for testing DSQ peek operations */ > +void BPF_STRUCT_OPS(peek_dsq_enqueue, struct task_struct *p, u64 enq_flags) > +{ > + struct task_struct *peek_result; > + int last_insert_test_cpu, cpu; > + > + enqueue_count++; > + cpu = bpf_get_smp_processor_id(); > + last_insert_test_cpu = __sync_val_compare_and_swap(&insert_test_cpu, -1, cpu); > + > + /* Phase 1: Simple insert-then-peek test (only on first task) */ > + if (last_insert_test_cpu == -1) { > + bpf_printk("peek_dsq_enqueue beginning phase 1 peek test on cpu %d", cpu); > + > + /* Test 1: Peek empty DSQ - should return NULL */ > + peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id); > + dsq_peek_result1 = (long)peek_result; /* Should be 0 (NULL) */ > + > + /* Test 2: Insert task into test DSQ for testing in dispatch callback */ > + dsq_inserted_pid = p->pid; > + scx_bpf_dsq_insert(p, test_dsq_id, 0, enq_flags); > + dsq_peek_result2_expected = (long)p; /* Expected the task we just inserted */ > + } else if (!phase1_complete) { > + /* Still in phase 1, use real DSQ */ > + scx_bpf_dsq_insert(p, real_dsq_id, 0, enq_flags); > + } else { > + /* Phase 2: Random DSQ insertion for stress testing */ > + int random_dsq_id = get_random_dsq_id(); > + > + scx_bpf_dsq_insert(p, random_dsq_id, 0, enq_flags); > + } > +} > + > +void BPF_STRUCT_OPS(peek_dsq_dispatch, s32 cpu, struct task_struct *prev) > +{ > + dispatch_count++; > + > + /* Phase 1: Complete the simple peek test if we inserted a task but > + * haven't tested peek yet > + */ > + if (insert_test_cpu == cpu && dsq_peek_result2 == -1) { > + struct task_struct *peek_result; > + > + bpf_printk("peek_dsq_dispatch completing phase 1 peek test on cpu %d", cpu); > + > + /* Test 3: Peek DSQ after insert - should return the task we inserted */ > + peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id); > + /* Store the PID of the peeked task for comparison */ > + dsq_peek_result2 = (long)peek_result; > + dsq_peek_result2_pid = peek_result ? peek_result->pid : -1; > + > + /* Now consume the task since we've peeked at it */ > + scx_bpf_dsq_move_to_local(test_dsq_id); > + > + /* Mark phase 1 as complete */ > + phase1_complete = 1; > + bpf_printk("Phase 1 complete, starting phase 2 stress testing"); > + } else if (!phase1_complete) { > + /* Still in phase 1, use real DSQ */ > + scx_bpf_dsq_move_to_local(real_dsq_id); > + } else { > + /* Phase 2: Scan all DSQs in the pool and try to move a task */ > + if (!scan_dsq_pool()) { > + /* No tasks found in DSQ pool, fall back to real DSQ */ > + scx_bpf_dsq_move_to_local(real_dsq_id); > + } > + } > +} > + > +s32 BPF_STRUCT_OPS_SLEEPABLE(peek_dsq_init) > +{ > + s32 err; > + int i; > + > + /* Always set debug values so we can see which version we're using */ > + debug_ksym_exists = bpf_ksym_exists(scx_bpf_dsq_peek) ? 1 : 0; > + > + /* Initialize state first */ > + insert_test_cpu = -1; > + enqueue_count = 0; > + dispatch_count = 0; > + phase1_complete = 0; > + total_peek_attempts = 0; > + successful_peeks = 0; > + > + /* Create the test and real DSQs */ > + err = scx_bpf_create_dsq(test_dsq_id, -1); > + if (err) { > + scx_bpf_error("Failed to create DSQ %d: %d", test_dsq_id, err); > + return err; indentation The rest looks okay to me. ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2025-10-15 16:02 UTC | newest] Thread overview: 7+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2025-10-15 1:57 [PATCH v4 0/2] sched_ext: lockless peek operation for DSQs Ryan Newton 2025-10-15 1:57 ` [PATCH v4 1/2] sched_ext: Add " Ryan Newton 2025-10-15 9:46 ` Christian Loehle 2025-10-15 14:05 ` Ryan Newton 2025-10-15 16:02 ` Andrea Righi 2025-10-15 1:57 ` [PATCH v4 2/2] sched_ext: Add a selftest for scx_bpf_dsq_peek Ryan Newton 2025-10-15 9:41 ` Christian Loehle
This is an external index of several public inboxes, see mirroring instructions on how to clone and mirror all data and code used by this external index.