| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638 |
- // SPDX-License-Identifier: GPL-2.0-only
- /*
- * Copyright 2023 Red Hat
- */
- #include "funnel-workqueue.h"
- #include <linux/atomic.h>
- #include <linux/cache.h>
- #include <linux/completion.h>
- #include <linux/err.h>
- #include <linux/kthread.h>
- #include <linux/percpu.h>
- #include "funnel-queue.h"
- #include "logger.h"
- #include "memory-alloc.h"
- #include "numeric.h"
- #include "permassert.h"
- #include "string-utils.h"
- #include "completion.h"
- #include "status-codes.h"
- static DEFINE_PER_CPU(unsigned int, service_queue_rotor);
- /**
- * DOC: Work queue definition.
- *
- * There are two types of work queues: simple, with one worker thread, and round-robin, which uses
- * a group of the former to do the work, and assigns work to them in round-robin fashion (roughly).
- * Externally, both are represented via the same common sub-structure, though there's actually not
- * a great deal of overlap between the two types internally.
- */
- struct vdo_work_queue {
- /* Name of just the work queue (e.g., "cpuQ12") */
- char *name;
- bool round_robin_mode;
- struct vdo_thread *owner;
- /* Life cycle functions, etc */
- const struct vdo_work_queue_type *type;
- };
- struct simple_work_queue {
- struct vdo_work_queue common;
- struct funnel_queue *priority_lists[VDO_WORK_Q_MAX_PRIORITY + 1];
- void *private;
- /*
- * The fields above are unchanged after setup but often read, and are good candidates for
- * caching -- and if the max priority is 2, just fit in one x86-64 cache line if aligned.
- * The fields below are often modified as we sleep and wake, so we want a separate cache
- * line for performance.
- */
- /* Any (0 or 1) worker threads waiting for new work to do */
- wait_queue_head_t waiting_worker_threads ____cacheline_aligned;
- /* Hack to reduce wakeup calls if the worker thread is running */
- atomic_t idle;
- /* These are infrequently used so in terms of performance we don't care where they land. */
- struct task_struct *thread;
- /* Notify creator once worker has initialized */
- struct completion *started;
- };
- struct round_robin_work_queue {
- struct vdo_work_queue common;
- struct simple_work_queue **service_queues;
- unsigned int num_service_queues;
- };
- static inline struct simple_work_queue *as_simple_work_queue(struct vdo_work_queue *queue)
- {
- return ((queue == NULL) ?
- NULL : container_of(queue, struct simple_work_queue, common));
- }
- static inline struct round_robin_work_queue *as_round_robin_work_queue(struct vdo_work_queue *queue)
- {
- return ((queue == NULL) ?
- NULL :
- container_of(queue, struct round_robin_work_queue, common));
- }
- /* Processing normal completions. */
- /*
- * Dequeue and return the next waiting completion, if any.
- *
- * We scan the funnel queues from highest priority to lowest, once; there is therefore a race
- * condition where a high-priority completion can be enqueued followed by a lower-priority one, and
- * we'll grab the latter (but we'll catch the high-priority item on the next call). If strict
- * enforcement of priorities becomes necessary, this function will need fixing.
- */
- static struct vdo_completion *poll_for_completion(struct simple_work_queue *queue)
- {
- int i;
- for (i = queue->common.type->max_priority; i >= 0; i--) {
- struct funnel_queue_entry *link = vdo_funnel_queue_poll(queue->priority_lists[i]);
- if (link != NULL)
- return container_of(link, struct vdo_completion, work_queue_entry_link);
- }
- return NULL;
- }
- static void enqueue_work_queue_completion(struct simple_work_queue *queue,
- struct vdo_completion *completion)
- {
- VDO_ASSERT_LOG_ONLY(completion->my_queue == NULL,
- "completion %px (fn %px) to enqueue (%px) is not already queued (%px)",
- completion, completion->callback, queue, completion->my_queue);
- if (completion->priority == VDO_WORK_Q_DEFAULT_PRIORITY)
- completion->priority = queue->common.type->default_priority;
- if (VDO_ASSERT(completion->priority <= queue->common.type->max_priority,
- "priority is in range for queue") != VDO_SUCCESS)
- completion->priority = 0;
- completion->my_queue = &queue->common;
- /* Funnel queue handles the synchronization for the put. */
- vdo_funnel_queue_put(queue->priority_lists[completion->priority],
- &completion->work_queue_entry_link);
- /*
- * Due to how funnel queue synchronization is handled (just atomic operations), the
- * simplest safe implementation here would be to wake-up any waiting threads after
- * enqueueing each item. Even if the funnel queue is not empty at the time of adding an
- * item to the queue, the consumer thread may not see this since it is not guaranteed to
- * have the same view of the queue as a producer thread.
- *
- * However, the above is wasteful so instead we attempt to minimize the number of thread
- * wakeups. Using an idle flag, and careful ordering using memory barriers, we should be
- * able to determine when the worker thread might be asleep or going to sleep. We use
- * cmpxchg to try to take ownership (vs other producer threads) of the responsibility for
- * waking the worker thread, so multiple wakeups aren't tried at once.
- *
- * This was tuned for some x86 boxes that were handy; it's untested whether doing the read
- * first is any better or worse for other platforms, even other x86 configurations.
- */
- smp_mb();
- if ((atomic_read(&queue->idle) != 1) || (atomic_cmpxchg(&queue->idle, 1, 0) != 1))
- return;
- /* There's a maximum of one thread in this list. */
- wake_up(&queue->waiting_worker_threads);
- }
- static void run_start_hook(struct simple_work_queue *queue)
- {
- if (queue->common.type->start != NULL)
- queue->common.type->start(queue->private);
- }
- static void run_finish_hook(struct simple_work_queue *queue)
- {
- if (queue->common.type->finish != NULL)
- queue->common.type->finish(queue->private);
- }
- /*
- * Wait for the next completion to process, or until kthread_should_stop indicates that it's time
- * for us to shut down.
- *
- * If kthread_should_stop says it's time to stop but we have pending completions return a
- * completion.
- *
- * Also update statistics relating to scheduler interactions.
- */
- static struct vdo_completion *wait_for_next_completion(struct simple_work_queue *queue)
- {
- struct vdo_completion *completion;
- DEFINE_WAIT(wait);
- while (true) {
- prepare_to_wait(&queue->waiting_worker_threads, &wait,
- TASK_INTERRUPTIBLE);
- /*
- * Don't set the idle flag until a wakeup will not be lost.
- *
- * Force synchronization between setting the idle flag and checking the funnel
- * queue; the producer side will do them in the reverse order. (There's still a
- * race condition we've chosen to allow, because we've got a timeout below that
- * unwedges us if we hit it, but this may narrow the window a little.)
- */
- atomic_set(&queue->idle, 1);
- smp_mb(); /* store-load barrier between "idle" and funnel queue */
- completion = poll_for_completion(queue);
- if (completion != NULL)
- break;
- /*
- * We need to check for thread-stop after setting TASK_INTERRUPTIBLE state up
- * above. Otherwise, schedule() will put the thread to sleep and might miss a
- * wakeup from kthread_stop() call in vdo_finish_work_queue().
- */
- if (kthread_should_stop())
- break;
- schedule();
- /*
- * Most of the time when we wake, it should be because there's work to do. If it
- * was a spurious wakeup, continue looping.
- */
- completion = poll_for_completion(queue);
- if (completion != NULL)
- break;
- }
- finish_wait(&queue->waiting_worker_threads, &wait);
- atomic_set(&queue->idle, 0);
- return completion;
- }
- static void process_completion(struct simple_work_queue *queue,
- struct vdo_completion *completion)
- {
- if (VDO_ASSERT(completion->my_queue == &queue->common,
- "completion %px from queue %px marked as being in this queue (%px)",
- completion, queue, completion->my_queue) == VDO_SUCCESS)
- completion->my_queue = NULL;
- vdo_run_completion(completion);
- }
- static void service_work_queue(struct simple_work_queue *queue)
- {
- run_start_hook(queue);
- while (true) {
- struct vdo_completion *completion = poll_for_completion(queue);
- if (completion == NULL)
- completion = wait_for_next_completion(queue);
- if (completion == NULL) {
- /* No completions but kthread_should_stop() was triggered. */
- break;
- }
- process_completion(queue, completion);
- /*
- * Be friendly to a CPU that has other work to do, if the kernel has told us to.
- * This speeds up some performance tests; that "other work" might include other VDO
- * threads.
- */
- if (need_resched())
- cond_resched();
- }
- run_finish_hook(queue);
- }
- static int work_queue_runner(void *ptr)
- {
- struct simple_work_queue *queue = ptr;
- complete(queue->started);
- service_work_queue(queue);
- return 0;
- }
- /* Creation & teardown */
- static void free_simple_work_queue(struct simple_work_queue *queue)
- {
- unsigned int i;
- for (i = 0; i <= VDO_WORK_Q_MAX_PRIORITY; i++)
- vdo_free_funnel_queue(queue->priority_lists[i]);
- vdo_free(queue->common.name);
- vdo_free(queue);
- }
- static void free_round_robin_work_queue(struct round_robin_work_queue *queue)
- {
- struct simple_work_queue **queue_table = queue->service_queues;
- unsigned int count = queue->num_service_queues;
- unsigned int i;
- queue->service_queues = NULL;
- for (i = 0; i < count; i++)
- free_simple_work_queue(queue_table[i]);
- vdo_free(queue_table);
- vdo_free(queue->common.name);
- vdo_free(queue);
- }
- void vdo_free_work_queue(struct vdo_work_queue *queue)
- {
- if (queue == NULL)
- return;
- vdo_finish_work_queue(queue);
- if (queue->round_robin_mode)
- free_round_robin_work_queue(as_round_robin_work_queue(queue));
- else
- free_simple_work_queue(as_simple_work_queue(queue));
- }
- static int make_simple_work_queue(const char *thread_name_prefix, const char *name,
- struct vdo_thread *owner, void *private,
- const struct vdo_work_queue_type *type,
- struct simple_work_queue **queue_ptr)
- {
- DECLARE_COMPLETION_ONSTACK(started);
- struct simple_work_queue *queue;
- int i;
- struct task_struct *thread = NULL;
- int result;
- VDO_ASSERT_LOG_ONLY((type->max_priority <= VDO_WORK_Q_MAX_PRIORITY),
- "queue priority count %u within limit %u", type->max_priority,
- VDO_WORK_Q_MAX_PRIORITY);
- result = vdo_allocate(1, struct simple_work_queue, "simple work queue", &queue);
- if (result != VDO_SUCCESS)
- return result;
- queue->private = private;
- queue->started = &started;
- queue->common.type = type;
- queue->common.owner = owner;
- init_waitqueue_head(&queue->waiting_worker_threads);
- result = vdo_duplicate_string(name, "queue name", &queue->common.name);
- if (result != VDO_SUCCESS) {
- vdo_free(queue);
- return -ENOMEM;
- }
- for (i = 0; i <= type->max_priority; i++) {
- result = vdo_make_funnel_queue(&queue->priority_lists[i]);
- if (result != VDO_SUCCESS) {
- free_simple_work_queue(queue);
- return result;
- }
- }
- thread = kthread_run(work_queue_runner, queue, "%s:%s", thread_name_prefix,
- queue->common.name);
- if (IS_ERR(thread)) {
- free_simple_work_queue(queue);
- return (int) PTR_ERR(thread);
- }
- queue->thread = thread;
- /*
- * If we don't wait to ensure the thread is running VDO code, a quick kthread_stop (due to
- * errors elsewhere) could cause it to never get as far as running VDO, skipping the
- * cleanup code.
- *
- * Eventually we should just make that path safe too, and then we won't need this
- * synchronization.
- */
- wait_for_completion(&started);
- *queue_ptr = queue;
- return VDO_SUCCESS;
- }
- /**
- * vdo_make_work_queue() - Create a work queue; if multiple threads are requested, completions will
- * be distributed to them in round-robin fashion.
- *
- * Each queue is associated with a struct vdo_thread which has a single vdo thread id. Regardless
- * of the actual number of queues and threads allocated here, code outside of the queue
- * implementation will treat this as a single zone.
- */
- int vdo_make_work_queue(const char *thread_name_prefix, const char *name,
- struct vdo_thread *owner, const struct vdo_work_queue_type *type,
- unsigned int thread_count, void *thread_privates[],
- struct vdo_work_queue **queue_ptr)
- {
- struct round_robin_work_queue *queue;
- int result;
- char thread_name[TASK_COMM_LEN];
- unsigned int i;
- if (thread_count == 1) {
- struct simple_work_queue *simple_queue;
- void *context = ((thread_privates != NULL) ? thread_privates[0] : NULL);
- result = make_simple_work_queue(thread_name_prefix, name, owner, context,
- type, &simple_queue);
- if (result == VDO_SUCCESS)
- *queue_ptr = &simple_queue->common;
- return result;
- }
- result = vdo_allocate(1, struct round_robin_work_queue, "round-robin work queue",
- &queue);
- if (result != VDO_SUCCESS)
- return result;
- result = vdo_allocate(thread_count, struct simple_work_queue *,
- "subordinate work queues", &queue->service_queues);
- if (result != VDO_SUCCESS) {
- vdo_free(queue);
- return result;
- }
- queue->num_service_queues = thread_count;
- queue->common.round_robin_mode = true;
- queue->common.owner = owner;
- result = vdo_duplicate_string(name, "queue name", &queue->common.name);
- if (result != VDO_SUCCESS) {
- vdo_free(queue->service_queues);
- vdo_free(queue);
- return -ENOMEM;
- }
- *queue_ptr = &queue->common;
- for (i = 0; i < thread_count; i++) {
- void *context = ((thread_privates != NULL) ? thread_privates[i] : NULL);
- snprintf(thread_name, sizeof(thread_name), "%s%u", name, i);
- result = make_simple_work_queue(thread_name_prefix, thread_name, owner,
- context, type, &queue->service_queues[i]);
- if (result != VDO_SUCCESS) {
- queue->num_service_queues = i;
- /* Destroy previously created subordinates. */
- vdo_free_work_queue(vdo_forget(*queue_ptr));
- return result;
- }
- }
- return VDO_SUCCESS;
- }
- static void finish_simple_work_queue(struct simple_work_queue *queue)
- {
- if (queue->thread == NULL)
- return;
- /* Tells the worker thread to shut down and waits for it to exit. */
- kthread_stop(queue->thread);
- queue->thread = NULL;
- }
- static void finish_round_robin_work_queue(struct round_robin_work_queue *queue)
- {
- struct simple_work_queue **queue_table = queue->service_queues;
- unsigned int count = queue->num_service_queues;
- unsigned int i;
- for (i = 0; i < count; i++)
- finish_simple_work_queue(queue_table[i]);
- }
- /* No enqueueing of completions should be done once this function is called. */
- void vdo_finish_work_queue(struct vdo_work_queue *queue)
- {
- if (queue == NULL)
- return;
- if (queue->round_robin_mode)
- finish_round_robin_work_queue(as_round_robin_work_queue(queue));
- else
- finish_simple_work_queue(as_simple_work_queue(queue));
- }
- /* Debugging dumps */
- static void dump_simple_work_queue(struct simple_work_queue *queue)
- {
- const char *thread_status = "no threads";
- char task_state_report = '-';
- if (queue->thread != NULL) {
- task_state_report = task_state_to_char(queue->thread);
- thread_status = atomic_read(&queue->idle) ? "idle" : "running";
- }
- vdo_log_info("workQ %px (%s) %s (%c)", &queue->common, queue->common.name,
- thread_status, task_state_report);
- /* ->waiting_worker_threads wait queue status? anyone waiting? */
- }
- /*
- * Write to the buffer some info about the completion, for logging. Since the common use case is
- * dumping info about a lot of completions to syslog all at once, the format favors brevity over
- * readability.
- */
- void vdo_dump_work_queue(struct vdo_work_queue *queue)
- {
- if (queue->round_robin_mode) {
- struct round_robin_work_queue *round_robin = as_round_robin_work_queue(queue);
- unsigned int i;
- for (i = 0; i < round_robin->num_service_queues; i++)
- dump_simple_work_queue(round_robin->service_queues[i]);
- } else {
- dump_simple_work_queue(as_simple_work_queue(queue));
- }
- }
- static void get_function_name(void *pointer, char *buffer, size_t buffer_length)
- {
- if (pointer == NULL) {
- /*
- * Format "%ps" logs a null pointer as "(null)" with a bunch of leading spaces. We
- * sometimes use this when logging lots of data; don't be so verbose.
- */
- strscpy(buffer, "-", buffer_length);
- } else {
- /*
- * Use a pragma to defeat gcc's format checking, which doesn't understand that
- * "%ps" actually does support a precision spec in Linux kernel code.
- */
- char *space;
- #pragma GCC diagnostic push
- #pragma GCC diagnostic ignored "-Wformat"
- snprintf(buffer, buffer_length, "%.*ps", buffer_length - 1, pointer);
- #pragma GCC diagnostic pop
- space = strchr(buffer, ' ');
- if (space != NULL)
- *space = '\0';
- }
- }
- void vdo_dump_completion_to_buffer(struct vdo_completion *completion, char *buffer,
- size_t length)
- {
- size_t current_length =
- scnprintf(buffer, length, "%.*s/", TASK_COMM_LEN,
- (completion->my_queue == NULL ? "-" : completion->my_queue->name));
- if (current_length < length - 1) {
- get_function_name((void *) completion->callback, buffer + current_length,
- length - current_length);
- }
- }
- /* Completion submission */
- /*
- * If the completion has a timeout that has already passed, the timeout handler function may be
- * invoked by this function.
- */
- void vdo_enqueue_work_queue(struct vdo_work_queue *queue,
- struct vdo_completion *completion)
- {
- /*
- * Convert the provided generic vdo_work_queue to the simple_work_queue to actually queue
- * on.
- */
- struct simple_work_queue *simple_queue = NULL;
- if (!queue->round_robin_mode) {
- simple_queue = as_simple_work_queue(queue);
- } else {
- struct round_robin_work_queue *round_robin = as_round_robin_work_queue(queue);
- /*
- * It shouldn't be a big deal if the same rotor gets used for multiple work queues.
- * Any patterns that might develop are likely to be disrupted by random ordering of
- * multiple completions and migration between cores, unless the load is so light as
- * to be regular in ordering of tasks and the threads are confined to individual
- * cores; with a load that light we won't care.
- */
- unsigned int rotor = this_cpu_inc_return(service_queue_rotor);
- unsigned int index = rotor % round_robin->num_service_queues;
- simple_queue = round_robin->service_queues[index];
- }
- enqueue_work_queue_completion(simple_queue, completion);
- }
- /* Misc */
- /*
- * Return the work queue pointer recorded at initialization time in the work-queue stack handle
- * initialized on the stack of the current thread, if any.
- */
- static struct simple_work_queue *get_current_thread_work_queue(void)
- {
- /*
- * In interrupt context, if a vdo thread is what got interrupted, the calls below will find
- * the queue for the thread which was interrupted. However, the interrupted thread may have
- * been processing a completion, in which case starting to process another would violate
- * our concurrency assumptions.
- */
- if (in_interrupt())
- return NULL;
- if (kthread_func(current) != work_queue_runner)
- /* Not a VDO work queue thread. */
- return NULL;
- return kthread_data(current);
- }
- struct vdo_work_queue *vdo_get_current_work_queue(void)
- {
- struct simple_work_queue *queue = get_current_thread_work_queue();
- return (queue == NULL) ? NULL : &queue->common;
- }
- struct vdo_thread *vdo_get_work_queue_owner(struct vdo_work_queue *queue)
- {
- return queue->owner;
- }
- /**
- * vdo_get_work_queue_private_data() - Returns the private data for the current thread's work
- * queue, or NULL if none or if the current thread is not a
- * work queue thread.
- */
- void *vdo_get_work_queue_private_data(void)
- {
- struct simple_work_queue *queue = get_current_thread_work_queue();
- return (queue != NULL) ? queue->private : NULL;
- }
- bool vdo_work_queue_type_is(struct vdo_work_queue *queue,
- const struct vdo_work_queue_type *type)
- {
- return (queue->type == type);
- }
|