From nobody Sun Dec 7 15:08:49 2025 Received: from mail-io1-f51.google.com (mail-io1-f51.google.com [209.85.166.51]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id AF838334394 for ; Wed, 15 Oct 2025 15:50:44 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.166.51 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1760543446; cv=none; b=alm6cJY6dDFre/HksAHl0VNDR3Fu1/Z/hpzvubviczFktfXkSbukfUfavHYNtlL8kOtnKplWN8Es/BjoAkO7GeoX+fqSTCLcv2Prpuo3OALRmLASqE+RR9m3/9dB/fiSoYVz6XjHV0aVsEyClLjnEYgUwx975/16X4wcScigTfc= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1760543446; c=relaxed/simple; bh=RnHvzys4uJQqhoEI4QYtcs/hmY10zyRT368i9reWevk=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=OgqVAKLzJxkUIWjgDXURIja/wKr7a8n8WmQo+Dd2/nstl607xKrN3wq7US8pwmRa4qU869EUjYX0mfVWPODQxCubWfTk6wV2pGaxnjGfNtq38ksE7bfzD1ItRUsL5dJAhaBRKhEtfMA0JfKFrBZAqsTq+xim/ysoUIBT5W2FXEQ= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=ezLKIrcV; arc=none smtp.client-ip=209.85.166.51 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="ezLKIrcV" Received: by mail-io1-f51.google.com with SMTP id ca18e2360f4ac-90a0b3ddebeso277708939f.0 for ; Wed, 15 Oct 2025 08:50:44 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1760543442; x=1761148242; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=NdUqKoPzfH0SGw1iBPVZD7em5JOQyR55dqZlLIrI0Rw=; b=ezLKIrcVNJ/xD4RMgDGRlBjOr6dKvrGRwWye7PYWA0zZsI/QBkoYCO1U7s/5HlhcCR FkUaq9FVknxCb7bz7OjflKgvpKRslCsod9Ox2cSTfruSqsySMz0s72ART18kcBfPPCaK tqeTk07VUFFdUdBVT2aqLuU2oBOtrnLE7UzesAih7pjBcdwKpBjFPMRN6q4PUOKEe8yY BrKtry4IPkWmBxeSyi5EJqJMD9VtQa2FaC3PTFBDIstbHm43YzIX7fm8tdDCVQc6YakI utv9hD/B5lR/JZQFHx1McPfxI6I3N06hnaaJWtQZsR9kgMW0VDZ16S76K6nCRsUfk6lJ CGEA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1760543442; x=1761148242; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=NdUqKoPzfH0SGw1iBPVZD7em5JOQyR55dqZlLIrI0Rw=; b=YJd1Gbe/EoteyxxqV+Tv1fGhJa9hIMRif54nLEXcfEnCGlz/np5sZJixDSZ2mxyowP C5cS2jSgCecj1PvXCe2HdKREZtWXCRTam+2igr5iklKKa5hNYkHuDRM1egff2o+SL3GW HA5UL9fSmCNRD4M4Wov2xb+bmssJNMt8/4ihImhY256cbCUB5K3WS5Z/gIYEDd3QdPNW Hp9tb+Jei9CIaDKJcO6r2b2jKCG2R11SkqkhSUtPufRVDPyUQOy9T5XDpoWxkH4bhdq0 QgDdFGYH5bxEOIgFJ4JoSPGL1WEfEt8zYAulqfoZ2Jt5B3pODaq7y3j8ea4rKq/5Fh1N pTHQ== X-Gm-Message-State: AOJu0YytznqmpgIq8+BMFM+udsgoMH1qZgH7cxRlTjPaz4pUr0Hm20ca D5DfsGwHP+qws0ziyIlEJI8rOoqFU5SEW8m4YtBdPgZf9vKPEbJNhoOXPpzT08Oh X-Gm-Gg: ASbGnctuf7b8BMR5XV/UvgVEf7Qj4ETWUIKRhFoePVM3i8XtJ4aNqhLMTU1CYUA18sc AVS8GvEpP4VXZEr+Uy5dLlBuJEeVJxo1T2oNTOLMKiUkZ90WWtpSPYGPdJNkRWJYSu6JPkT/QqT OUq14F7dcfNcE6lsxmQlK4rhdqsu3BcLPQBX6FcI78JSi5uUmYpCKbBOwVr3zays2zbi3Hvd4qi xyEmqXX4EGxd07iot2Cormu4hRsO4VpSjIouTMUw7NDhnVZCVmqKA/6ZyAl7Mr+OrMJl+qwIiHY BVxT4+N4BaQmGxKctMThd4nGfqz9wP0sdT3NPeroCVJ0lQtC8wvq3zoFgRJfgkjATHebVZYQR8t 7tOt7n9d7a1FPj/hMaXbKOSA2IRGN+lc70OUZAuAl6beiL37PqA5TJ5IMGzBbpxVgYz0fxizZvg 7gk1q64EPQ6tmBUb99S+DYhNM6Uvuzlkn7xt6dDXNllE87rFy3k3mwy54PBRTJ9A== X-Google-Smtp-Source: AGHT+IF5PSGLY6efInvx8zBXdQYWj5DHd01ETNCWEaP6Qvxx9zbNYafZlxVTtMeV+WG0G8QZQkd6EA== X-Received: by 2002:a05:6602:340d:b0:887:628c:7ff4 with SMTP id ca18e2360f4ac-93bd1851609mr3699273839f.1.1760543442018; Wed, 15 Oct 2025 08:50:42 -0700 (PDT) Received: from newton-fedora-MZ01GC9H (c-68-45-22-229.hsd1.in.comcast.net. [68.45.22.229]) by smtp.gmail.com with ESMTPSA id ca18e2360f4ac-93e25a67790sm617361639f.21.2025.10.15.08.50.40 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Oct 2025 08:50:40 -0700 (PDT) From: Ryan Newton To: linux-kernel@vger.kernel.org Cc: sched-ext@lists.linux.dev, tj@kernel.org, arighi@nvidia.com, christian.loehle@arm.com, rrnewton@gmail.com, newton@meta.com Subject: [PATCH v5 1/2] sched_ext: Add lockless peek operation for DSQs Date: Wed, 15 Oct 2025 11:50:35 -0400 Message-ID: <20251015155036.420486-2-rrnewton@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20251015155036.420486-1-rrnewton@gmail.com> References: <20251015155036.420486-1-rrnewton@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" From: Ryan Newton 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 Reviewed-by: Andrea Righi Reviewed-by: Christian Loehle --- include/linux/sched/ext.h | 1 + kernel/sched/ext.c | 58 +++++++++++++++++++++++- tools/sched_ext/include/scx/common.bpf.h | 1 + tools/sched_ext/include/scx/compat.bpf.h | 18 ++++++++ 4 files changed, 76 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..925daf281d99 100644 --- a/kernel/sched/ext.c +++ b/kernel/sched/ext.c @@ -965,8 +965,11 @@ static void dispatch_enqueue(struct scx_sched *sch, st= ruct 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, s= truct scx_dispatch_q *dsq, scx_error(sch, "DSQ ID 0x%016llx already had PRIQ-enqueued tasks", dsq->id); =20 - 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 =3D 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); + } } =20 /* 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 &=3D ~SCX_TASK_DSQ_ON_PRIQ; } =20 + if (!(dsq->id & SCX_DSQ_FLAG_BUILTIN) && dsq->first_task =3D=3D p) { + struct task_struct *first_task; + + first_task =3D 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,40 @@ __bpf_kfunc void bpf_iter_scx_dsq_destroy(struct bpf= _iter_scx_dsq *it) kit->dsq =3D NULL; } =20 +/** + * 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 u= sing + * the DSQ iterator, but is lockfree. Of course, like any lockless operati= on, + * this provides only a point-in-time snapshot, and the contents may change + * by the time any subsequent locking operation reads the queue. + * + * 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 =3D 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 =3D 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(); =20 static s32 __bstr_format(struct scx_sched *sch, u64 *data_buf, char *line_= buf, @@ -6837,6 +6890,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/inc= lude/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 flag= s) __ksym __weak; struct task_struct *bpf_iter_scx_dsq_next(struct bpf_iter_scx_dsq *it) __k= sym __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/inc= lude/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) =20 +/* + * 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 =3D 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 =3D 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. --=20 2.51.0 From nobody Sun Dec 7 15:08:49 2025 Received: from mail-io1-f49.google.com (mail-io1-f49.google.com [209.85.166.49]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3D8CB335BA6 for ; Wed, 15 Oct 2025 15:50:44 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.166.49 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1760543447; cv=none; b=PrZ8iMqYvrG3j6xoJCNcfxPdaD428XnQApwAIPIvaXfO4ldCj0zJaEAXCuHcmWoTEmOv22uJp4H6UKdLPl53ov58Zzw1nadxovQJ3X+HzoD6P/P6qCwLFmimEgf7naLZauyVe0ar+4j40Al7mUm3IhkE5mJSja5qDun2woCHeMQ= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1760543447; c=relaxed/simple; bh=AFvzN7HlbZeeG4MK20M8eAuW4Da20TRs/i+EEp1sbAg=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=tcmF6MPWUFmoL3O0Cr5GvBS37kVbD1pr2YEvn9b4EObtinh7GeeA6U/z2zUKITAjFke/tqyTlJ0m70Y/g/rAi/PQKG+Py37X/hJKohymAd8T6hNdXrqgfPOzLwauzHBXU/fsC+kAzrSQJ5EtFFuTHB9oQEP6Na40YY1pJR7cJG8= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b=TUwkJJQN; arc=none smtp.client-ip=209.85.166.49 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=gmail.com header.i=@gmail.com header.b="TUwkJJQN" Received: by mail-io1-f49.google.com with SMTP id ca18e2360f4ac-91fbba9a8f5so576342739f.0 for ; Wed, 15 Oct 2025 08:50:44 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1760543444; x=1761148244; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to; bh=gAaWNfCL4OgyY333y3vIsUzib4Fjn1LdXJW4mbycsiY=; b=TUwkJJQNq7N/IA44vyH0702DLydlhk/bmj0WWuvr8oF/6ylEvPvatfXU+r1t3tw3fz 2V30tzCERUm4VYq95uNnjikCcmn0hXEF0ftmy0xJq8vmW0xiYc3Juo79g+d7JEzAEcBX D8BMOZPEOzV00Bv5gS0JRiVUWdRo/K6FkMMfen32WAzKcCmUkg4l8yjjEDLtHIsUUcYX 0judRGE94Ktfiz5CThUAxx/oEqBWAjgzqVnMQPwsSPbwk8djNhWHBFUvUrYYCZx8HqT6 dz3a6VuQ9e988IshOJnt6McLEefZw/Heoh/ulmXRkm2YXLd0Gv39SmLIDVVet7llbtCf 79yw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1760543444; x=1761148244; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=gAaWNfCL4OgyY333y3vIsUzib4Fjn1LdXJW4mbycsiY=; b=c4ZPxBAd1Gp2SbpjWZ8JKmrX2WSAnIstQJCgyndkAjrOfDAi7Ov152kK85tqEIofbc r1xGB7UTtZASFC6OHbPgENnjELZ0nqeGLMC7ZJS+/ASLenIETHkaosfsDKzVonHB9+A7 DyqSSiTXnF5CViob8jfwNbGGACSx0fHU4Bjdmrs9t7H4aTOj5ihXJOC26bR69IbebcFk JDKNTHt5R3IMj3+2XZPFlZULtFpBv8JqSn+fqC1yFd8TWnCEeBtJJ+2IKOz/WgVGbvjr yqKiNnKLiZd4ahkBlplDT8gKxOukW4by/ZiXfnCjEPGVP/HAeYISX5EMuf+MGVIVwxKV +/uA== X-Gm-Message-State: AOJu0Yws5cpmviLgRCzKUa2BAINvtXEGM2m/9xYmWPn00ZEs5tDvV6E6 u4JP5u+i6xJ3pKNUsJ1tvy8n8Pn3JOF0cN+yn4bgLICTA7emjM4ugTuj4SZCjUxm X-Gm-Gg: ASbGncvgINSZpuR5TpML1sYLHKDvqrUBi//+FNFPcUe5rxmShiwa9b373JzsNs4WYtL ARKEkJKfNT7u3i+8Xt/kt+d9ciqlOZT0Dsm54rLbR1JIpSO0QU+zOI/o9JvNfUKjEe15JuN8UqI m6O9zpAcdqevDbBA16vQ1LsFw2mSQjLIe5pidlIcoGOL5aK8TzCzmldDu/TZDJp/zFWzQVzRk1w DPAR/GxRyxI3UbLFqA99l8CsqfIFT2M9R9jcaLjmySr1KLCHiFOmayGbW3qU5ibMrTC6FiYdSQg pyRH77Y/xm91eq1aCOPxcZInx5UIXSKyTAc68XzZGHcr31teC/tU+qr7ARh4F/8fw8B6VByKE48 rZw0yvCs4H556Kh8hvmbjhmEsJxaiRmQcL69ASRZvsUroYO6ltlB+78NuxSQVcaEcd7IL96Or2g 1mGYF0QCCuIrFca6Xbc9mC5sZEirWRc/TgSGxSrQ9y X-Google-Smtp-Source: AGHT+IHP3CT9WZG/L+PPEYcjOCgz3+jtqRpYk6RqYxbJlTwAAy7gFRTrHaRL1k6ti9yjWOxziFAZjg== X-Received: by 2002:a05:6602:6cc9:b0:928:6b27:1d75 with SMTP id ca18e2360f4ac-93bd19b0dacmr4074749739f.19.1760543443663; Wed, 15 Oct 2025 08:50:43 -0700 (PDT) Received: from newton-fedora-MZ01GC9H (c-68-45-22-229.hsd1.in.comcast.net. [68.45.22.229]) by smtp.gmail.com with ESMTPSA id ca18e2360f4ac-93e25a67790sm617361639f.21.2025.10.15.08.50.42 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Wed, 15 Oct 2025 08:50:42 -0700 (PDT) From: Ryan Newton To: linux-kernel@vger.kernel.org Cc: sched-ext@lists.linux.dev, tj@kernel.org, arighi@nvidia.com, christian.loehle@arm.com, rrnewton@gmail.com, newton@meta.com Subject: [PATCH v5 2/2] sched_ext: Add a selftest for scx_bpf_dsq_peek Date: Wed, 15 Oct 2025 11:50:36 -0400 Message-ID: <20251015155036.420486-3-rrnewton@gmail.com> X-Mailer: git-send-email 2.51.0 In-Reply-To: <20251015155036.420486-1-rrnewton@gmail.com> References: <20251015155036.420486-1-rrnewton@gmail.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" From: Ryan Newton This commit adds two tests. The first 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. The second test is a stress test that performs many peeks on all DSQs and records the observed tasks. Signed-off-by: Ryan Newton Reviewed-by: Christian Loehle --- tools/testing/selftests/sched_ext/Makefile | 1 + .../selftests/sched_ext/peek_dsq.bpf.c | 251 ++++++++++++++++++ tools/testing/selftests/sched_ext/peek_dsq.c | 224 ++++++++++++++++ 3 files changed, 476 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/sel= ftests/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 :=3D \ 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/testi= ng/selftests/sched_ext/peek_dsq.bpf.c new file mode 100644 index 000000000000..a3faf5bb49d6 --- /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 + */ + +#include +#include + +char _license[] SEC("license") =3D "GPL"; + +UEI_DEFINE(uei); /* Error handling */ + +#define MAX_SAMPLES 100 +#define MAX_CPUS 512 +#define DSQ_POOL_SIZE 8 +int max_samples =3D MAX_SAMPLES; +int max_cpus =3D MAX_CPUS; +int dsq_pool_size =3D DSQ_POOL_SIZE; + +/* Global variables to store test results */ +int dsq_peek_result1 =3D -1; +long dsq_inserted_pid =3D -1; +int insert_test_cpu =3D -1; /* Set to the cpu that performs the test */ +long dsq_peek_result2 =3D -1; +long dsq_peek_result2_pid =3D -1; +long dsq_peek_result2_expected =3D -1; +int test_dsq_id =3D 1234; /* Use a simple ID like create_dsq example */ +int real_dsq_id =3D 1235; /* DSQ for normal operation */ +int enqueue_count =3D -1; +int dispatch_count =3D -1; +bool debug_ksym_exists; + +/* DSQ pool for stress testing */ +int dsq_pool_base_id =3D 2000; +int phase1_complete =3D -1; +long total_peek_attempts =3D -1; +long successful_peeks =3D -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 =3D 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 <=3D 0) + return; + + /* Find an empty slot or one with the same PID */ + bpf_for(ix, 0, 10) { + slot_key =3D (pid + ix) % MAX_SAMPLES; + slot_pid_ptr =3D bpf_map_lookup_elem(&peek_results, &slot_key); + if (!slot_pid_ptr) + continue; + + if (*slot_pid_ptr =3D=3D -1 || *slot_pid_ptr =3D=3D pid) { + *slot_pid_ptr =3D 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 =3D 0; + int i; + + bpf_for(i, 0, DSQ_POOL_SIZE) { + int dsq_id =3D dsq_pool_base_id + i; + + total_peek_attempts++; + + task =3D __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) =3D=3D 0) { + moved =3D 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 =3D bpf_get_smp_processor_id(); + last_insert_test_cpu =3D __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 =3D=3D -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 =3D __COMPAT_scx_bpf_dsq_peek(test_dsq_id); + dsq_peek_result1 =3D (long)peek_result; /* Should be 0 (NULL) */ + + /* Test 2: Insert task into test DSQ for testing in dispatch callback */ + dsq_inserted_pid =3D p->pid; + scx_bpf_dsq_insert(p, test_dsq_id, 0, enq_flags); + dsq_peek_result2_expected =3D (long)p; /* Expected the task we just inse= rted */ + } 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 =3D 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 =3D=3D cpu && dsq_peek_result2 =3D=3D -1) { + struct task_struct *peek_result; + + bpf_printk("peek_dsq_dispatch completing phase 1 peek test on cpu %d", c= pu); + + /* Test 3: Peek DSQ after insert - should return the task we inserted */ + peek_result =3D __COMPAT_scx_bpf_dsq_peek(test_dsq_id); + /* Store the PID of the peeked task for comparison */ + dsq_peek_result2 =3D (long)peek_result; + dsq_peek_result2_pid =3D 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 =3D 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 =3D bpf_ksym_exists(scx_bpf_dsq_peek) ? 1 : 0; + + /* Initialize state first */ + insert_test_cpu =3D -1; + enqueue_count =3D 0; + dispatch_count =3D 0; + phase1_complete =3D 0; + total_peek_attempts =3D 0; + successful_peeks =3D 0; + + /* Create the test and real DSQs */ + err =3D 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 =3D 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 =3D dsq_pool_base_id + i; + + err =3D 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 =3D i; + long pid =3D -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 =3D 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 =3D { + .enqueue =3D (void *)peek_dsq_enqueue, + .dispatch =3D (void *)peek_dsq_dispatch, + .init =3D (void *)peek_dsq_init, + .exit =3D (void *)peek_dsq_exit, + .name =3D "peek_dsq", +}; diff --git a/tools/testing/selftests/sched_ext/peek_dsq.c b/tools/testing/s= elftests/sched_ext/peek_dsq.c new file mode 100644 index 000000000000..a717384a3224 --- /dev/null +++ b/tools/testing/selftests/sched_ext/peek_dsq.c @@ -0,0 +1,224 @@ +// 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 + */ +#include +#include +#include +#include +#include +#include +#include +#include "peek_dsq.bpf.skel.h" +#include "scx_test.h" + +#define NUM_WORKERS 4 + +static bool workload_running =3D 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 =3D 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 =3D skel; + + return SCX_TEST_PASS; +} + +static int print_observed_pids(struct bpf_map *map, int max_samples, const= char *dsq_name) +{ + long count =3D 0; + + printf("Observed %s DSQ peek pids:\n", dsq_name); + for (int i =3D 0; i < max_samples; i++) { + long pid; + int err; + + err =3D bpf_map_lookup_elem(bpf_map__fd(map), &i, &pid); + if (err =3D=3D 0) { + if (pid =3D=3D 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=3D%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 =3D ctx; + bool failed =3D false; + int seconds =3D 3; + int err; + + /* Enable the scheduler to test DSQ operations */ + printf("Enabling scheduler to test DSQ insert operations...\n"); + + struct bpf_link *link =3D + 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 =3D true; + for (int i =3D 0; i < NUM_WORKERS; i++) { + err =3D pthread_create(&workload_threads[i], NULL, workload_thread_fn, N= ULL); + if (err) { + SCX_ERR("Failed to create workload thread %d: %s", i, strerror(err)); + /* Stop already created threads */ + workload_running =3D false; + for (int j =3D 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 <=3D 0) { + printf("."); + fflush(stdout); + sleep(1); + seconds++; + if (seconds >=3D 30) { + printf("\n\u2717 Timeout waiting for enqueue events\n"); + /* Stop workload threads and cleanup */ + workload_running =3D false; + for (int i =3D 0; i < NUM_WORKERS; i++) + pthread_join(workload_threads[i], NULL); + bpf_link__destroy(link); + return SCX_TEST_FAIL; + } + } + + workload_running =3D false; + for (int i =3D 0; i < NUM_WORKERS; i++) { + err =3D 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=3D%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 !=3D -1) + printf("\u2713 DSQ insert succeeded !\n"); + else { + printf("\u2717 DSQ insert failed or not attempted\n"); + failed =3D 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 =3D=3D 0) + printf("\u2713 DSQ peek verification success: peek returned NULL!\n"); + else { + printf("\u2717 DSQ peek verification failed\n"); + failed =3D 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 =3D=3D + 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 =3D 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 =3D print_observed_pids(skel->maps.peek_results, + skel->data->max_samples, "DSQ pool"); + printf("Total non-null peek observations: %ld out of %ld\n", + skel->data->successful_peeks, skel->data->total_peek_attempts); + + if (skel->bss->debug_ksym_exists && pid_count =3D=3D 0) { + printf("\u2717 DSQ pool test failed: no successful peeks in native mode\= n"); + failed =3D true; + } + if (skel->bss->debug_ksym_exists && pid_count > 0) + printf("\u2713 DSQ pool test success: observed successful peeks in nativ= e mode\n"); + + if (failed) + return SCX_TEST_FAIL; + else + return SCX_TEST_PASS; +} + +static void cleanup(void *ctx) +{ + struct peek_dsq *skel =3D ctx; + + if (workload_running) { + workload_running =3D false; + for (int i =3D 0; i < NUM_WORKERS; i++) + pthread_join(workload_threads[i], NULL); + } + + peek_dsq__destroy(skel); +} + +struct scx_test peek_dsq =3D { + .name =3D "peek_dsq", + .description =3D + "Test DSQ create/destroy operations and future peek functionality", + .setup =3D setup, + .run =3D run, + .cleanup =3D cleanup, +}; +REGISTER_SCX_TEST(&peek_dsq) --=20 2.51.0