What is Linux Kernel Workqueues Tutorial-Free Linux Device Driver Training in Hyderabad

Linux Kernel Workqueues Tutorial
Free Linux Kernel Development Course • Kernel 6.x • INIT_WORK, schedule_work, alloc_workqueue
100% Free
Kernel 6.x API
Interrupt-Safe Pattern

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(), and schedule_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.

Deferring Work From an Interrupt
Hardware interrupt fires | v IRQ handler (atomic context, must be fast) | | schedule_work(&my_work) v Kernel worker thread picks up the work item | v my_work_fn() runs in process context (safe to sleep, take mutexes, do real I/O)

The Modern Workqueue API

FunctionPurpose
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() or flush_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_struct before 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() (or cancel_delayed_work_sync() for delayed work) in your exit/remove path.
  • Use alloc_workqueue() with sensible flags (such as WQ_UNBOUND or WQ_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

Q1. Why not just do everything inside the interrupt handler?

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.

Q2. What is the difference between schedule_work() and queue_work()?

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().

Q3. How do I safely stop pending work when my module unloads?

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.

Q4. When should I create my own dedicated workqueue?

When your driver generates frequent work items or has specific latency or isolation needs that the shared system workqueue doesn’t suit well.

Q5. What is delayed work used for?

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.

Q6. Is this Linux kernel workqueues tutorial free to follow?

Yes, it’s part of EmbeddedPathashala’s free Linux kernel development course, free Linux device drivers course, and free embedded systems course.

linux kernel workqueues tutorial free linux kernel development course free linux device drivers course INIT_WORK schedule_work
You’ve Completed This Mini-Series!

Continue with the next chapter of the free Linux kernel development course.

Next Lecture →

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *