« Previous Lecture | Next Lecture »
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.
- 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()andschedule_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()andcancel_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
- Basic familiarity with writing and loading a Linux kernel module (insmod/rmmod)
- Understanding of the kernel-global workqueue,
INIT_WORK(), andschedule_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:
| Function | Applies To | Blocks Until Done? | When To Use |
|---|---|---|---|
cancel_work_sync() | Plain work_struct (immediate work) | Yes | Cleaning up work scheduled via schedule_work() |
cancel_delayed_work() | delayed_work | No | Fast, non-blocking cancel attempt; may return while the callback is still running |
cancel_delayed_work_sync() | delayed_work | Yes | Module/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.
register callback
timer armed for delay
work queued
(process context)
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=100build than on aHZ=250build. Always go throughmsecs_to_jiffies()orsecs_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_workinside your own context structure, exactly as you would with a plainwork_struct, and retrieve it withcontainer_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 viamsecs_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
2 Comments