]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
lib: add light-weight queuing mechanism.
authorNeilBrown <neilb@suse.de>
Mon, 11 Sep 2023 14:39:43 +0000 (10:39 -0400)
committerChuck Lever <chuck.lever@oracle.com>
Mon, 16 Oct 2023 16:44:06 +0000 (12:44 -0400)
lwq is a FIFO single-linked queue that only requires a spinlock
for dequeueing, which happens in process context.  Enqueueing is atomic
with no spinlock and can happen in any context.

This is particularly useful when work items are queued from BH or IRQ
context, and when they are handled one at a time by dedicated threads.

Avoiding any locking when enqueueing means there is no need to disable
BH or interrupts, which is generally best avoided (particularly when
there are any RT tasks on the machine).

This solution is superior to using "list_head" links because we need
half as many pointers in the data structures, and because list_head
lists would need locking to add items to the queue.

This solution is superior to a bespoke solution as all locking and
container_of casting is integrated, so the interface is simple.

Despite the similar name, this solution meets a distinctly different
need to kfifo.  kfifo provides a fixed sized circular buffer to which
data can be added at one end and removed at the other, and does not
provide any locking.  lwq does not have any size limit and works with
data structures (objects?) rather than data (bytes).

A unit test for basic functionality, which runs at boot time, is included.

Signed-off-by: NeilBrown <neilb@suse.de>
Cc: Andrew Morton <akpm@linux-foundation.org>
Cc: "Liam R. Howlett" <Liam.Howlett@oracle.com>
Cc: Kees Cook <keescook@chromium.org>
Cc: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Cc: David Gow <davidgow@google.com>
Cc: linux-kernel@vger.kernel.org
Message-Id: <20230911111333.4d1a872330e924a00acb905b@linux-foundation.org>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
include/linux/lwq.h [new file with mode: 0644]
lib/Kconfig
lib/Makefile
lib/lwq.c [new file with mode: 0644]

diff --git a/include/linux/lwq.h b/include/linux/lwq.h
new file mode 100644 (file)
index 0000000..d081d5c
--- /dev/null
@@ -0,0 +1,124 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
+
+#ifndef LWQ_H
+#define LWQ_H
+/*
+ * Light-weight single-linked queue built from llist
+ *
+ * Entries can be enqueued from any context with no locking.
+ * Entries can be dequeued from process context with integrated locking.
+ *
+ * This is particularly suitable when work items are queued in
+ * BH or IRQ context, and where work items are handled one at a time
+ * by dedicated threads.
+ */
+#include <linux/container_of.h>
+#include <linux/spinlock.h>
+#include <linux/llist.h>
+
+struct lwq_node {
+       struct llist_node node;
+};
+
+struct lwq {
+       spinlock_t              lock;
+       struct llist_node       *ready;         /* entries to be dequeued */
+       struct llist_head       new;            /* entries being enqueued */
+};
+
+/**
+ * lwq_init - initialise a lwq
+ * @q: the lwq object
+ */
+static inline void lwq_init(struct lwq *q)
+{
+       spin_lock_init(&q->lock);
+       q->ready = NULL;
+       init_llist_head(&q->new);
+}
+
+/**
+ * lwq_empty - test if lwq contains any entry
+ * @q: the lwq object
+ *
+ * This empty test contains an acquire barrier so that if a wakeup
+ * is sent when lwq_dequeue returns true, it is safe to go to sleep after
+ * a test on lwq_empty().
+ */
+static inline bool lwq_empty(struct lwq *q)
+{
+       /* acquire ensures ordering wrt lwq_enqueue() */
+       return smp_load_acquire(&q->ready) == NULL && llist_empty(&q->new);
+}
+
+struct llist_node *__lwq_dequeue(struct lwq *q);
+/**
+ * lwq_dequeue - dequeue first (oldest) entry from lwq
+ * @q:         the queue to dequeue from
+ * @type:      the type of object to return
+ * @member:    them member in returned object which is an lwq_node.
+ *
+ * Remove a single object from the lwq and return it.  This will take
+ * a spinlock and so must always be called in the same context, typcially
+ * process contet.
+ */
+#define lwq_dequeue(q, type, member)                                   \
+       ({ struct llist_node *_n = __lwq_dequeue(q);                    \
+         _n ? container_of(_n, type, member.node) : NULL; })
+
+struct llist_node *lwq_dequeue_all(struct lwq *q);
+
+/**
+ * lwq_for_each_safe - iterate over detached queue allowing deletion
+ * @_n:                iterator variable
+ * @_t1:       temporary struct llist_node **
+ * @_t2:       temporary struct llist_node *
+ * @_l:                address of llist_node pointer from lwq_dequeue_all()
+ * @_member:   member in _n where lwq_node is found.
+ *
+ * Iterate over members in a dequeued list.  If the iterator variable
+ * is set to NULL, the iterator removes that entry from the queue.
+ */
+#define lwq_for_each_safe(_n, _t1, _t2, _l, _member)                   \
+       for (_t1 = (_l);                                                \
+            *(_t1) ? (_n = container_of(*(_t1), typeof(*(_n)), _member.node),\
+                      _t2 = ((*_t1)->next),                            \
+                      true)                                            \
+            : false;                                                   \
+            (_n) ? (_t1 = &(_n)->_member.node.next, 0)                 \
+            : ((*(_t1) = (_t2)),  0))
+
+/**
+ * lwq_enqueue - add a new item to the end of the queue
+ * @n  - the lwq_node embedded in the item to be added
+ * @q  - the lwq to append to.
+ *
+ * No locking is needed to append to the queue so this can
+ * be called from any context.
+ * Return %true is the list may have previously been empty.
+ */
+static inline bool lwq_enqueue(struct lwq_node *n, struct lwq *q)
+{
+       /* acquire enqures ordering wrt lwq_dequeue */
+       return llist_add(&n->node, &q->new) &&
+               smp_load_acquire(&q->ready) == NULL;
+}
+
+/**
+ * lwq_enqueue_batch - add a list of new items to the end of the queue
+ * @n  - the lwq_node embedded in the first item to be added
+ * @q  - the lwq to append to.
+ *
+ * No locking is needed to append to the queue so this can
+ * be called from any context.
+ * Return %true is the list may have previously been empty.
+ */
+static inline bool lwq_enqueue_batch(struct llist_node *n, struct lwq *q)
+{
+       struct llist_node *e = n;
+
+       /* acquire enqures ordering wrt lwq_dequeue */
+       return llist_add_batch(llist_reverse_order(n), e, &q->new) &&
+               smp_load_acquire(&q->ready) == NULL;
+}
+#endif /* LWQ_H */
index c686f4adc1246a0e1d65f7c15a35d10e992ba2ca..76fe64f933fc7b25dca28744a7474fa7ad24568e 100644 (file)
@@ -729,6 +729,11 @@ config PARMAN
 config OBJAGG
        tristate "objagg" if COMPILE_TEST
 
+config LWQ_TEST
+       bool "Boot-time test for lwq queuing"
+       help
+          Run boot-time test of light-weight queuing.
+
 endmenu
 
 config GENERIC_IOREMAP
index 740109b6e2c89f0748d4ed5013139db0803c9bb2..d0c116b706e6bae0fb8520f0f2e6c7aa0237578e 100644 (file)
@@ -45,7 +45,7 @@ obj-y += lockref.o
 obj-y += bcd.o sort.o parser.o debug_locks.o random32.o \
         bust_spinlocks.o kasprintf.o bitmap.o scatterlist.o \
         list_sort.o uuid.o iov_iter.o clz_ctz.o \
-        bsearch.o find_bit.o llist.o memweight.o kfifo.o \
+        bsearch.o find_bit.o llist.o lwq.o memweight.o kfifo.o \
         percpu-refcount.o rhashtable.o base64.o \
         once.o refcount.o rcuref.o usercopy.o errseq.o bucket_locks.o \
         generic-radix-tree.o
diff --git a/lib/lwq.c b/lib/lwq.c
new file mode 100644 (file)
index 0000000..57d080a
--- /dev/null
+++ b/lib/lwq.c
@@ -0,0 +1,158 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Light-weight single-linked queue.
+ *
+ * Entries are enqueued to the head of an llist, with no blocking.
+ * This can happen in any context.
+ *
+ * Entries are dequeued using a spinlock to protect against multiple
+ * access.  The llist is staged in reverse order, and refreshed
+ * from the llist when it exhausts.
+ *
+ * This is particularly suitable when work items are queued in BH or
+ * IRQ context, and where work items are handled one at a time by
+ * dedicated threads.
+ */
+#include <linux/rcupdate.h>
+#include <linux/lwq.h>
+
+struct llist_node *__lwq_dequeue(struct lwq *q)
+{
+       struct llist_node *this;
+
+       if (lwq_empty(q))
+               return NULL;
+       spin_lock(&q->lock);
+       this = q->ready;
+       if (!this && !llist_empty(&q->new)) {
+               /* ensure queue doesn't appear transiently lwq_empty */
+               smp_store_release(&q->ready, (void *)1);
+               this = llist_reverse_order(llist_del_all(&q->new));
+               if (!this)
+                       q->ready = NULL;
+       }
+       if (this)
+               q->ready = llist_next(this);
+       spin_unlock(&q->lock);
+       return this;
+}
+EXPORT_SYMBOL_GPL(__lwq_dequeue);
+
+/**
+ * lwq_dequeue_all - dequeue all currently enqueued objects
+ * @q: the queue to dequeue from
+ *
+ * Remove and return a linked list of llist_nodes of all the objects that were
+ * in the queue. The first on the list will be the object that was least
+ * recently enqueued.
+ */
+struct llist_node *lwq_dequeue_all(struct lwq *q)
+{
+       struct llist_node *r, *t, **ep;
+
+       if (lwq_empty(q))
+               return NULL;
+
+       spin_lock(&q->lock);
+       r = q->ready;
+       q->ready = NULL;
+       t = llist_del_all(&q->new);
+       spin_unlock(&q->lock);
+       ep = &r;
+       while (*ep)
+               ep = &(*ep)->next;
+       *ep = llist_reverse_order(t);
+       return r;
+}
+EXPORT_SYMBOL_GPL(lwq_dequeue_all);
+
+#if IS_ENABLED(CONFIG_LWQ_TEST)
+
+#include <linux/module.h>
+#include <linux/slab.h>
+#include <linux/wait_bit.h>
+#include <linux/kthread.h>
+#include <linux/delay.h>
+struct tnode {
+       struct lwq_node n;
+       int i;
+       int c;
+};
+
+static int lwq_exercise(void *qv)
+{
+       struct lwq *q = qv;
+       int cnt;
+       struct tnode *t;
+
+       for (cnt = 0; cnt < 10000; cnt++) {
+               wait_var_event(q, (t = lwq_dequeue(q, struct tnode, n)) != NULL);
+               t->c++;
+               if (lwq_enqueue(&t->n, q))
+                       wake_up_var(q);
+       }
+       while (!kthread_should_stop())
+               schedule_timeout_idle(1);
+       return 0;
+}
+
+static int lwq_test(void)
+{
+       int i;
+       struct lwq q;
+       struct llist_node *l, **t1, *t2;
+       struct tnode *t;
+       struct task_struct *threads[8];
+
+       printk(KERN_INFO "testing lwq....\n");
+       lwq_init(&q);
+       printk(KERN_INFO " lwq: run some threads\n");
+       for (i = 0; i < ARRAY_SIZE(threads); i++)
+               threads[i] = kthread_run(lwq_exercise, &q, "lwq-test-%d", i);
+       for (i = 0; i < 100; i++) {
+               t = kmalloc(sizeof(*t), GFP_KERNEL);
+               if (!t)
+                       break;
+               t->i = i;
+               t->c = 0;
+               if (lwq_enqueue(&t->n, &q))
+                       wake_up_var(&q);
+       }
+       /* wait for threads to exit */
+       for (i = 0; i < ARRAY_SIZE(threads); i++)
+               if (!IS_ERR_OR_NULL(threads[i]))
+                       kthread_stop(threads[i]);
+       printk(KERN_INFO " lwq: dequeue first 50:");
+       for (i = 0; i < 50 ; i++) {
+               if (i && (i % 10) == 0) {
+                       printk(KERN_CONT "\n");
+                       printk(KERN_INFO " lwq: ... ");
+               }
+               t = lwq_dequeue(&q, struct tnode, n);
+               if (t)
+                       printk(KERN_CONT " %d(%d)", t->i, t->c);
+               kfree(t);
+       }
+       printk(KERN_CONT "\n");
+       l = lwq_dequeue_all(&q);
+       printk(KERN_INFO " lwq: delete the multiples of 3 (test lwq_for_each_safe())\n");
+       lwq_for_each_safe(t, t1, t2, &l, n) {
+               if ((t->i % 3) == 0) {
+                       t->i = -1;
+                       kfree(t);
+                       t = NULL;
+               }
+       }
+       if (l)
+               lwq_enqueue_batch(l, &q);
+       printk(KERN_INFO " lwq: dequeue remaining:");
+       while ((t = lwq_dequeue(&q, struct tnode, n)) != NULL) {
+               printk(KERN_CONT " %d", t->i);
+               kfree(t);
+       }
+       printk(KERN_CONT "\n");
+       return 0;
+}
+
+module_init(lwq_test);
+#endif /* CONFIG_LWQ_TEST*/