This Linux kernel workqueues tutorial closes out our four-part mini-series on kernel timing and background work, part of EmbeddedPathashala’s free Linux kernel development course. Workqueues are the mechanism the kernel itself uses most often for deferred, sleepable work — and by the end of this Linux kernel workqueues tutorial you’ll be able to defer work safely from an interrupt handler down to process context.
What You Will Learn
- Why deferring work out of an interrupt handler is often necessary
- How
struct work_struct,INIT_WORK(), andschedule_work()fit together - How to create your own dedicated workqueue with
alloc_workqueue() - How delayed work differs from immediate work
- A complete, original interrupt-to-workqueue example
Prerequisites
This Linux kernel workqueues tutorial assumes you’ve completed the earlier lectures in this free Linux kernel development course on delay functions, kernel timers, and kernel threads.
Why Workqueues Exist
Interrupt handlers must be short and cannot sleep. But real driver work — reading a register over I2C, allocating memory, taking a mutex — often needs to sleep. The classic solution is to do the minimum in the interrupt handler and hand the rest off to a workqueue, which runs your function later in normal, sleepable process context.
The Modern Workqueue API
| Function | Purpose |
|---|---|
INIT_WORK(work, fn) | Bind a work_struct to its handler function |
schedule_work(work) | Queue work onto the kernel’s shared system workqueue |
alloc_workqueue(name, flags, max_active) | Create your own dedicated workqueue |
queue_work(wq, work) | Queue work onto a specific workqueue you created |
INIT_DELAYED_WORK(dwork, fn) | Bind a delayed work item to its handler |
schedule_delayed_work(dwork, delay) | Queue delayed work to run after a jiffies delay |
cancel_work_sync(work) | Cancel a work item and wait for it to finish if running |
A Complete Interrupt-to-Workqueue Example
#include <linux/module.h>
#include <linux/workqueue.h>
#include <linux/interrupt.h>
struct ep_dev_ctx {
struct work_struct work;
int irq;
};
static struct ep_dev_ctx ep_ctx;
static void ep_work_handler(struct work_struct *work)
{
struct ep_dev_ctx *ctx = container_of(work, struct ep_dev_ctx, work);
/* Safe to sleep here: read a register over I2C, take a mutex, etc. */
pr_info("ep_wq: handling deferred work for irq %d\n", ctx->irq);
}
static irqreturn_t ep_irq_handler(int irq, void *dev_id)
{
struct ep_dev_ctx *ctx = dev_id;
/* Keep the handler itself fast — just queue the real work */
schedule_work(&ctx->work);
return IRQ_HANDLED;
}
static int __init ep_wq_init(void)
{
INIT_WORK(&ep_ctx.work, ep_work_handler);
pr_info("ep_wq: module loaded\n");
return 0;
}
static void __exit ep_wq_exit(void)
{
cancel_work_sync(&ep_ctx.work);
pr_info("ep_wq: module unloaded\n");
}
module_init(ep_wq_init);
module_exit(ep_wq_exit);
MODULE_LICENSE("GPL");
Dedicated Workqueues vs the Shared System Workqueue
schedule_work() queues onto the kernel’s shared system workqueue, which is fine for light, infrequent work. If your driver produces a steady stream of work items, or needs to isolate itself from other subsystems for latency reasons, create a dedicated workqueue instead:
static struct workqueue_struct *ep_wq;
ep_wq = alloc_workqueue("ep_wq", WQ_UNBOUND, 0);
if (!ep_wq)
return -ENOMEM;
queue_work(ep_wq, &ep_ctx.work);
/* on exit */
destroy_workqueue(ep_wq);
Common Mistakes
- Calling
cancel_work_sync()orflush_work()from inside the work handler itself, which deadlocks. - Forgetting to cancel pending work in the module’s exit path, leaving a callback that can fire after the module has been unloaded.
- Using the shared system workqueue for high-frequency or latency-sensitive work that really deserves its own dedicated workqueue.
- Re-queuing the same
work_structbefore the previous instance has finished, without understanding that a pending item is simply left as-is rather than duplicated.
Best Practices
- Keep interrupt handlers minimal — just enough to acknowledge the hardware and call
schedule_work(). - Always
cancel_work_sync()(orcancel_delayed_work_sync()for delayed work) in your exit/remove path. - Use
alloc_workqueue()with sensible flags (such asWQ_UNBOUNDorWQ_HIGHPRI) when the default system workqueue isn’t a good fit.
Performance and Security Considerations
Workqueues scale better than one-thread-per-task designs because worker threads are pooled and reused. From a security and robustness standpoint, always validate that any device context passed into a work handler is still valid — cancel pending work synchronously during device removal so a handler never runs against memory that has already been freed.
Key Takeaways
- Workqueues defer sleepable work out of atomic contexts like interrupt handlers.
schedule_work()uses the shared system workqueue;alloc_workqueue()gives you a dedicated one.- Always cancel pending work synchronously before your module or device is removed.
- Delayed work (
INIT_DELAYED_WORK) combines a timer-like delay with sleepable execution.
Conclusion
That wraps up this four-lecture mini-series inside our free Linux kernel development course — delay functions, kernel timers, kernel threads, and now workqueues. Together these four mechanisms cover almost every timing and background-execution need you’ll run into while writing real device drivers. Keep going with the next chapter of this free Linux device drivers course and free embedded systems course to keep building on this foundation.
Frequently Asked Questions
Interrupt handlers run in atomic context and cannot sleep, so any work that needs to sleep — I2C/SPI transfers, mutexes, memory allocation with GFP_KERNEL — must be deferred to a workqueue.
schedule_work() is a convenience wrapper that queues onto the kernel’s shared system workqueue, while queue_work() queues onto a specific workqueue you created with alloc_workqueue().
Call cancel_work_sync() (or cancel_delayed_work_sync() for delayed work) before freeing any memory the work handler touches, so no callback can run after teardown.
When your driver generates frequent work items or has specific latency or isolation needs that the shared system workqueue doesn’t suit well.
INIT_DELAYED_WORK combined with schedule_delayed_work() lets you defer sleepable work to run after a jiffies-based delay, similar to a timer but with the ability to sleep once it fires.
Yes, it’s part of EmbeddedPathashala’s free Linux kernel development course, free Linux device drivers course, and free embedded systems course.
Continue with the next chapter of the free Linux kernel development course.
Next Lecture →
2 Comments