← Previous Lecture | Next Lecture →
If you are following this free Linux kernel development course, you already know that kernel threads are powerful but expensive to manage by hand. This lecture on Linux kernel workqueues shows you the safer, simpler alternative that the kernel itself uses everywhere — from network drivers to storage subsystems — to run background work outside of interrupt context. By the end of this lesson you will be able to queue your own deferred work on kernel 6.x using nothing more than a handful of clean APIs.
This is part of our ongoing free linux device drivers course and free embedded systems course series, where every lecture is rewritten from scratch for current kernels, with original driver examples you can build and load yourself.
What You Will Learn
- Why workqueues exist
- The kernel-global workqueue
- INIT_WORK and schedule_work
- Creating a private workqueue
- Ordered vs unbound workqueues
- Safe cleanup on module exit
Prerequisites
You should be comfortable writing a basic loadable kernel module, and it helps to have gone through our earlier lecture on kernel threads in this same course, since workqueues solve a very similar problem in a lighter-weight way. A machine or VM running a recent Linux distribution with kernel 6.x headers installed is enough to try the examples.
Why Do We Need Workqueues?
Imagine your driver receives a hardware interrupt and needs to do something that takes time — read a sensor over I2C, write a log entry, or copy a buffer. You cannot do that inside the actual hardirq handler, because interrupt context cannot sleep or block. One option is to spin up your own kernel thread for this, but managing threads by hand quickly turns into a source of bugs: races, forgotten cleanup, and wasted memory if you create one thread per task.
A workqueue solves this by giving you a ready-made pool of worker threads maintained entirely by the kernel. You simply describe the work you want done as a function, hand it to the workqueue, and the kernel worker pool picks it up and runs it in normal, preemptible process context — meaning it is free to sleep, block on I/O, or wait on a mutex, none of which is allowed inside a hardirq, tasklet, or softirq.
The Kernel-Global Workqueue
Every Linux kernel already has a default, always-available workqueue called the kernel-global workqueue (referred to internally as system_wq). For the vast majority of drivers, using this shared workqueue is the recommended approach, because creating additional private workqueues adds more worker threads and more system overhead.
Scheduling work on the global workqueue takes just two steps: declare a struct work_struct, initialize it with the function you want run, and hand it off.
#include <linux/module.h>
#include <linux/workqueue.h>
#include <linux/slab.h>
struct my_work_data {
struct work_struct work;
int value;
};
static void my_work_handler(struct work_struct *work)
{
struct my_work_data *data = container_of(work, struct my_work_data, work);
pr_info("workqueue_demo: processing value = %d\n", data->value);
/* Safe to sleep here - we run in preemptible process context */
msleep(50);
kfree(data);
}
static int __init workqueue_demo_init(void)
{
struct my_work_data *data;
int i;
for (i = 0; i value = i;
INIT_WORK(&data->work, my_work_handler);
schedule_work(&data->work);
}
pr_info("workqueue_demo: module loaded, work items queued\n");
return 0;
}
static void __exit workqueue_demo_exit(void)
{
pr_info("workqueue_demo: module unloaded\n");
}
module_init(workqueue_demo_init);
module_exit(workqueue_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple kernel-global workqueue demo");
Notice that my_work_handler() is free to call msleep(). If you tried that inside a tasklet or a hardirq handler, it would either be silently wrong or crash the kernel. This is the entire point of workqueues: they give you thread-like freedom without the bookkeeping of a raw kernel thread.
Creating Your Own Workqueue
Sometimes the global workqueue is not the right fit — for example, if your driver’s work items must never be delayed by unrelated work from other drivers, or must run at a higher priority. In that case the modern concurrency-managed workqueue framework (cmwq) lets you allocate a private one with alloc_workqueue().
#include <linux/workqueue.h>
static struct workqueue_struct *my_wq;
static int __init custom_wq_init(void)
{
my_wq = alloc_workqueue("my_custom_wq", WQ_UNBOUND, 0);
if (!my_wq)
return -ENOMEM;
return 0;
}
static void __exit custom_wq_exit(void)
{
if (my_wq) {
flush_workqueue(my_wq);
destroy_workqueue(my_wq);
}
}
module_init(custom_wq_init);
module_exit(custom_wq_exit);
MODULE_LICENSE("GPL");
Two broad kinds of workqueues are worth knowing:
- Ordered workqueues (created with
alloc_ordered_workqueue()) guarantee that only one work item runs at a time, and strictly in the order it was queued. Use this when your work items must never run concurrently with each other. - Multi-threaded / unbound workqueues allow several work items to run concurrently across CPUs, which is the default and usually the better choice for throughput-sensitive drivers.
Always pair every alloc_workqueue() with a matching destroy_workqueue() in your module’s exit path, and call flush_workqueue() first so no work item is still running when your module is unloaded.
Workqueues vs Kernel Threads vs Tasklets
| Mechanism | Can Sleep? | Setup Effort | Best For |
|---|---|---|---|
| Tasklet / softirq | No (atomic context) | Low | Very fast, non-blocking bottom halves |
| Raw kernel thread | Yes | High — manual lifecycle management | Long-running, dedicated background loops |
| Workqueue | Yes | Low — kernel manages the pool | One-off or repeated deferred, blocking work |
Notes for Modern Kernel 6.x
On current kernel 6.x releases, the concurrency-managed workqueue (cmwq) framework remains the standard, and the APIs shown above — INIT_WORK(), schedule_work(), queue_work(), alloc_workqueue(), flush_workqueue(), and destroy_workqueue() — are all still current and stable. As with kernel threads, remember that a workqueue handler still has no user-space memory mapping, so functions like copy_to_user() cannot be called directly from inside a work handler; you must hand off any user-space interaction to a process-context path outside the handler, or restructure the driver so the work handler only touches kernel buffers.
If you want to inspect the worker pool threads live on your own machine, running ps -ef | grep kworker on any modern distribution will show you the pool the kernel is already using for the global workqueue.
Frequently Asked Questions
Q1. What is a Linux kernel workqueue used for?
A workqueue defers work to run later in a normal, sleep-capable kernel thread context, instead of running it inside an interrupt handler.
Q2. Should I always create my own workqueue?
No. Use the kernel-global workqueue by default; only allocate a private one if you need strict ordering or isolation from other drivers’ work.
Q3. Can a workqueue handler sleep?
Yes. Workqueue handlers run in preemptible process context, so blocking calls like msleep() or waiting on a mutex are safe.
Q4. What is the difference between schedule_work() and queue_work()?
schedule_work() queues work on the kernel-global workqueue, while queue_work() queues work on a workqueue you specify, such as one you created with alloc_workqueue().
Q5. What happens if I unload my module while work is still pending?
You risk a crash. Always call flush_workqueue() (for a private workqueue) or cancel_work_sync() on individual work items before your module’s exit function returns.
Q6. Are workqueues faster than kernel threads?
They are not necessarily faster, but they are far less error-prone, since the kernel manages thread creation, pooling, and scheduling for you.
Continue this free Linux kernel development course with more original, hands-on lectures on device drivers, memory management, and interrupt handling.
Visit EmbeddedPathashala
2 Comments