Linux Kernel Delayed Work Queue Tutorial-Free Linux Device Driver Training in Hyderabad

Linux Kernel Delayed Work Queue Tutorial | schedule_delayed_work() Explained

« Previous Lecture  |  Next Lecture »

Linux Kernel Delayed Work Queue Tutorial
Mastering schedule_delayed_work(), CPU-affinity scheduling, and safe cancellation on modern kernel 6.x
Kernel 6.x Ready
Beginner Friendly
Original Demo Driver

If you have already learned how the kernel-global workqueue runs an immediate work task with schedule_work(), the natural next question is: what if the task should not run right away? That is exactly what a linux kernel delayed work queue is for. A delayed work queue lets you tell the kernel “run this work task, but only after a certain amount of time has passed,” without you having to manage a separate kernel timer by hand. In this lecture of our free Linux kernel development course, we build a complete mental model of delayed work, then write and explain an original demo module from scratch, updated for modern kernel 6.x.

Topics covered in this lecture:
INIT_DELAYED_WORK schedule_delayed_work() schedule_delayed_work_on() cancel_delayed_work_sync() msecs_to_jiffies() self-rescheduling work
What You Will Learn
  • Why a linux kernel delayed work queue exists, and how it differs from a plain work queue
  • How to initialize and schedule delayed work with INIT_DELAYED_WORK() and schedule_delayed_work()
  • How to convert seconds or milliseconds into jiffies safely across kernel builds
  • How to pin delayed work to a specific CPU core with schedule_delayed_work_on()
  • The difference between cancel_delayed_work() and cancel_delayed_work_sync(), and why the “_sync” variant matters
  • How to build a safe, self-rescheduling delayed work driver and clean it up correctly on module unload
Prerequisites
  • Basic familiarity with writing and loading a Linux kernel module (insmod/rmmod)
  • Understanding of the kernel-global workqueue, INIT_WORK(), and schedule_work() from the previous lecture in this free linux kernel development course
  • A Linux machine running kernel 6.x with kernel headers installed, for building and testing the demo module

Why Do We Need a Delayed Work Queue?

A regular work task submitted with schedule_work() is picked up by a kernel worker thread as soon as one is free. That is perfect when the work must happen immediately, deferred only out of a hard (atomic) context into a safe process context. But many real driver situations need a task to run only after some time has elapsed — polling a sensor every few hundred milliseconds, debouncing a GPIO input, retrying a failed transaction after a backoff period, or timing out an operation. Building this by hand with a raw kernel timer plus a separate work task is possible, but a linux kernel delayed work queue folds both concerns into a single, well-tested structure: struct delayed_work.

Internally, a delayed_work structure simply wraps the familiar work_struct together with a kernel timer. When the timer expires, the kernel automatically queues the wrapped work task onto the workqueue for you. You never touch the internal timer directly — you only interact with the delayed work API.

schedule_delayed_work(): Scheduling Work in the Future

The core function you will use most often is:

bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay);

The first argument is a pointer to your delayed_work structure, which must first be initialized with INIT_DELAYED_WORK(&dwork, callback_function) — the syntax mirrors INIT_WORK() exactly. The second argument, delay, is expressed in jiffies, not seconds or milliseconds. Since the kernel’s jiffies counter increments HZ times per second on a given build, hardcoding a raw jiffies number is fragile across different kernel configurations. Always convert using the helper macro instead:

schedule_delayed_work(&ctx.dwork, msecs_to_jiffies(500)); /* run in ~500 ms */

This single line is the heart of any linux kernel delayed work queue driver: it tells the kernel “queue this work function roughly half a second from now,” and the kernel takes care of arming the internal timer, waiting, and enqueuing the work item when it fires.

Running Delayed Work on a Specific CPU Core

Sometimes a driver needs its delayed work to run on a particular CPU — for example, to keep it close to the hardware queue or interrupt it services. For that, the kernel provides a CPU-aware sibling function:

bool schedule_delayed_work_on(int cpu, struct delayed_work *dwork, unsigned long delay);

The extra leading parameter, cpu, pins execution to that logical CPU core once the delay elapses. Everything else behaves identically to schedule_delayed_work(). Most drivers never need this CPU-pinned variant; reach for it only when you have measured a real benefit from CPU affinity, such as reducing cache-line bouncing on a busy multi-core system.

Canceling a Linux Kernel Delayed Work Queue Safely

Just as important as scheduling work is un-scheduling it cleanly, especially inside your module’s exit path. Leaving a delayed work item pending when your driver unloads is a classic source of kernel crashes, because the work callback may fire after your module’s memory has already been freed. The kernel gives you three related functions, and picking the right one matters:

FunctionApplies ToBlocks Until Done?When To Use
cancel_work_sync()Plain work_struct (immediate work)YesCleaning up work scheduled via schedule_work()
cancel_delayed_work()delayed_workNoFast, non-blocking cancel attempt; may return while the callback is still running
cancel_delayed_work_sync()delayed_workYesModule/driver cleanup paths — the recommended default

The “_sync” suffix is the detail beginners most often miss. A synchronous cancel waits for any already-running instance of the callback to fully finish before returning, which is exactly the guarantee you want before you free the memory that the callback touches. Both functions return a boolean: true if there was pending or running work that got canceled, false if there was nothing to do.

The kernel also exposes flush_delayed_work() and flush_workqueue(), but these are easy to misuse and can lead to deadlocks if called from the wrong context. For almost all driver cleanup code, prefer the cancel_*_sync() family instead.

Hands-On Example: A Self-Rescheduling Delayed Work Demo

Let’s put this together with an original, minimal demo module. The idea: on load, the module schedules a delayed work task. Each time the task runs, it prints a tick count and reschedules itself for the next delay — a simple software poll loop — until it reaches a maximum count. On unload, it cancels any pending work safely with cancel_delayed_work_sync().

#include <linux/module.h>
#include <linux/workqueue.h>
#include <linux/jiffies.h>

#define POLL_INTERVAL_MS 500
#define MAX_TICKS        10

static struct dwq_demo_ctx {
    struct delayed_work dwork;
    int tick;
} ctx;

static void poll_work_func(struct work_struct *work)
{
    struct dwq_demo_ctx *priv =
        container_of(work, struct dwq_demo_ctx, dwork.work);

    priv->tick++;
    pr_info("dwq_demo: tick %d of %d\n", priv->tick, MAX_TICKS);

    if (priv->tick < MAX_TICKS)
        schedule_delayed_work(&priv->dwork,
                              msecs_to_jiffies(POLL_INTERVAL_MS));
    else
        pr_info("dwq_demo: reached max ticks, stopping\n");
}

static int __init dwq_demo_init(void)
{
    ctx.tick = 0;
    INIT_DELAYED_WORK(&ctx.dwork, poll_work_func);
    schedule_delayed_work(&ctx.dwork, msecs_to_jiffies(POLL_INTERVAL_MS));
    pr_info("dwq_demo: module loaded, first tick scheduled\n");
    return 0;
}

static void __exit dwq_demo_exit(void)
{
    cancel_delayed_work_sync(&ctx.dwork);
    pr_info("dwq_demo: module unloaded, work canceled safely\n");
}

module_init(dwq_demo_init);
module_exit(dwq_demo_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Self-rescheduling linux kernel delayed work queue demo");

Notice the pattern: the work callback uses container_of() to walk back from the embedded work_struct (accessed via .dwork.work) to our own context structure, exactly the technique used for plain work queues. The only new idea here is that the callback re-arms itself with schedule_delayed_work() at the end, building a lightweight periodic poll loop entirely inside process context — no separate kernel timer required.

Delayed Work Lifecycle
1. INIT_DELAYED_WORK()
register callback
→
2. schedule_delayed_work()
timer armed for delay
→
3. Timer fires
work queued
→
4. Worker thread runs callback
(process context)
Reschedule → back to step 2
Module unload → cancel_delayed_work_sync()

Common Mistakes and Troubleshooting

  • Forgetting to cancel on exit: if you skip cancel_delayed_work_sync() in your exit function, the callback can fire after the module is unloaded and its memory freed, corrupting the kernel. Always cancel first, free memory second.
  • Using cancel_delayed_work() instead of the sync variant during cleanup: the non-blocking version can return while the callback is mid-execution on another CPU, which is unsafe right before you tear down data structures.
  • Hardcoding jiffies values: a delay of “50” jiffies means something different on a HZ=100 build than on a HZ=250 build. Always go through msecs_to_jiffies() or secs_to_jiffies() style helpers.
  • Re-arming after cancellation: double-check that your callback does not call schedule_delayed_work() again after the module has begun its exit path — guard the reschedule with a shutdown flag if your driver can be unloaded mid-cycle.

Best Practices for a Linux Kernel Delayed Work Queue Driver

  • Embed struct delayed_work inside your own context structure, exactly as you would with a plain work_struct, and retrieve it with container_of().
  • Prefer the kernel-global workqueue via schedule_delayed_work() unless you have a measured reason (ordering guarantees, isolation, high volume) to allocate a private workqueue.
  • Always pair scheduling with a matching, synchronous cancel in your cleanup path.
  • Keep delayed work callbacks short and non-blocking; if the task needs to sleep for a long time, consider a kernel thread instead.

Performance Considerations

Delayed work relies on the kernel timer wheel, so extremely large numbers of independent delayed work items can add scheduling overhead. For drivers that need many periodic tasks, consider batching related work into a single delayed work item where possible, and avoid unnecessarily short intervals that wake the CPU out of low-power states more often than needed.

Security Considerations

Because a delayed work callback can execute well after the code that scheduled it has moved on, validate that any data the callback reads (device state, buffers, pointers) is still valid at execution time, and always ensure cancellation completes before freeing that data during driver removal or error/rollback paths.

Real-World Use Cases

  • Polling a sensor or peripheral register at a fixed interval without blocking a dedicated thread
  • Debouncing noisy GPIO interrupt lines
  • Implementing a watchdog-style timeout that fires unless canceled by other activity
  • Retrying a failed I/O operation after a backoff delay

Kernel 6.x Notes

The delayed work API described here — INIT_DELAYED_WORK(), schedule_delayed_work(), schedule_delayed_work_on(), and cancel_delayed_work[_sync]() — remains part of the modern concurrency-managed workqueue (cmwq) subsystem and is fully supported on current kernel 6.x releases. There is no deprecated legacy variant to worry about for delayed work itself, unlike some of the older workqueue-creation helpers covered earlier in this series.

Summary / Key Takeaways

  • A linux kernel delayed work queue combines a kernel timer and a work task into one structure, struct delayed_work.
  • schedule_delayed_work() queues a callback to run after a delay expressed in jiffies, best set via msecs_to_jiffies().
  • schedule_delayed_work_on() adds CPU-core pinning on top of the same behavior.
  • cancel_delayed_work_sync() is the safe, blocking way to guarantee no callback runs after your cleanup code proceeds.

Conclusion

Delayed work queues remove the need to hand-roll a timer-plus-work-task combination every time your driver needs to act after some delay. Once you are comfortable with INIT_DELAYED_WORK(), schedule_delayed_work(), and the synchronous cancel function, you have a reusable pattern for polling, debouncing, retries, and timeouts across almost any driver you write. In the next lecture of this free linux kernel development course, we will build on this foundation with a private, custom-allocated workqueue for cases where the kernel-global one is not the right fit.

FAQ: Linux Kernel Delayed Work Queue

Q1. What is the difference between schedule_work() and schedule_delayed_work()?
schedule_work() queues a task to run as soon as a worker thread is free. schedule_delayed_work() waits for a specified delay, expressed in jiffies, before queuing the same task.

Q2. Why should I use msecs_to_jiffies() instead of a raw number?
The jiffies counter’s rate (HZ) varies by kernel build configuration, so a hardcoded number of jiffies represents a different real-world time on different systems. msecs_to_jiffies() converts a fixed millisecond value correctly regardless of HZ.

Q3. What does cancel_delayed_work_sync() actually guarantee?
It guarantees that the delayed work is no longer pending, and if the callback happened to be running on another CPU at the time, the function blocks until that execution finishes before returning.

Q4. Is cancel_delayed_work() ever the right choice over the sync version?
Rarely for cleanup paths. It is a non-blocking best-effort cancel and can return while the callback is still running, so it is unsuitable right before freeing memory the callback touches.

Q5. When should I use schedule_delayed_work_on() instead of schedule_delayed_work()?
Only when you specifically need the work to execute on a particular CPU core, such as for cache locality with a hardware queue tied to that core. Most drivers can rely on the default scheduler placement.

Q6. Can a delayed work callback reschedule itself?
Yes, this is a common and valid pattern for building a lightweight periodic poll loop, as shown in the demo module in this lecture.

Q7. Do I still need a separate kernel timer if I use delayed work?
No. struct delayed_work already contains its own internal timer; you interact only with the delayed work API and never touch that internal timer directly.

Q8. Is this delayed work API still current on kernel 6.x?
Yes, INIT_DELAYED_WORK(), schedule_delayed_work(), schedule_delayed_work_on(), and cancel_delayed_work[_sync]() are all part of the modern cmwq subsystem and remain fully supported on current kernel 6.x releases.

Continue Your Free Linux Kernel Development Course

Keep building real driver skills, one lecture at a time, at EmbeddedPathashala.

Browse the Full Course Join the Community

« Previous Lecture  |  Next Lecture »

2 Comments

Leave a Reply

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