← Previous Lecture | Next Lecture →
Core Mechanisms
Practice Exercises
Kernel Version
If you have been following this free Linux kernel programming course, you have now worked through four ways the kernel lets a driver “do something later”: busy-wait style delays, kernel timers, dedicated kernel threads, and the workqueue subsystem. This lecture closes out that block of the course with a compact recap and a fresh set of practice exercises — written from scratch for kernel 6.x — so you can check how solid your understanding really is before moving into kernel synchronization.
This is a summary and exercise lecture, not a new API walkthrough, so keep it open side-by-side with the earlier lectures in this free linux device drivers course while you attempt the tasks below.
What You Will Learn
- How to decide between a delay, a timer, a kthread, and a workqueue for a given driver problem
- A one-page mental model that ties all four mechanisms together
- Seven original, from-scratch exercises to practice on real kernel 6.x hardware or a VM
- Common mistakes reviewers flag in kernel timer and workqueue code during driver review
Prerequisites
- A working kernel module build environment (kernel headers for a 6.x kernel, GCC, Make)
- Comfort with
insmod,rmmod, and readingdmesg - The earlier lectures in this series on delay functions, kernel timers, kernel threads, and workqueues
Quick Recap: Four Tools, One Decision
Every one of these mechanisms solves the same underlying problem — “I need this code to run at a different moment than right now” — but each fits a different situation.
Use these only for short, known waits inside code that is already allowed to block (or busy-wait, for microsecond-level atomic waits). They are the wrong tool whenever the wait is open-ended or the caller must stay responsive.
Timers fire a callback once a deadline passes, without tying up any thread while waiting. The callback runs in softirq context, so it must be quick and non-blocking — this is the detail most new driver authors get wrong.
A kthread gives you a real, sleepable execution context you fully control — useful when the work is ongoing or needs to block. The cost is that you own its entire lifecycle, including clean, race-free shutdown.
Workqueues give you the sleepable context of a thread without the bookkeeping — the kernel manages the worker pool for you. This is why most modern drivers reach for a workqueue instead of hand-rolling a kthread.
Practice Exercises (Kernel 6.x, Try Before You Peek at Solutions)
These are original exercises written for this course. Build each as a standalone loadable module and confirm behaviour through dmesg.
Below is a deliberately broken interrupt handler sketch. Find every bug before reading further: it calls a sleeping allocation, does user-space copies, and schedules a tasklet incorrectly.
static irqreturn_t demo_isr(int irq, void *dev_id)
{
struct my_dev *mdev = dev_id;
void *buf;
/* BUG: GFP_KERNEL can sleep -- not allowed in hardirq context */
buf = kmalloc(64, GFP_KERNEL);
/* BUG: copy_to_user() must never be called from interrupt context */
copy_to_user(mdev->user_ptr, buf, 64);
kfree(buf);
return IRQ_HANDLED;
}
The fix: allocate with GFP_ATOMIC if allocation is unavoidable in the handler, and move any user-space copy into a bottom half (tasklet or, better, a workqueue item scheduled from the handler).
Write a module that arms a kernel timer for a 50 ms timeout, records the arm time with ktime_get_ns(), and in the callback computes and logs how late the timer actually fired versus the requested 50 ms.
static struct timer_list my_timer;
static u64 armed_at_ns;
static void timer_cb(struct timer_list *t)
{
u64 now = ktime_get_ns();
pr_info("timer_latency: fired %llu ns after arming (target 50000000 ns)\n",
now - armed_at_ns);
}
static int __init timer_latency_init(void)
{
timer_setup(&my_timer, timer_cb, 0);
armed_at_ns = ktime_get_ns();
mod_timer(&my_timer, jiffies + msecs_to_jiffies(50));
return 0;
}
Build a module that re-arms its own timer every second and logs a running tick count, effectively a tiny “clock” living entirely in kernel space.
static struct timer_list tick_timer;
static unsigned long tick_count;
static void tick_cb(struct timer_list *t)
{
tick_count++;
pr_info("kernel_ticker: tick #%lu\n", tick_count);
mod_timer(&tick_timer, jiffies + msecs_to_jiffies(1000));
}
Remember to call timer_shutdown_sync(&tick_timer) in your module’s exit path so the timer cannot fire after unload.
Accept a module parameter count (default 0, an error if left at 0). If count is, say, 4, create four independent timers that fire at 4, 3, 2 and 1 seconds respectively, each logging which position in the countdown it represents.
static int count = 0;
module_param(count, int, 0444);
static struct timer_list *timers;
static void countdown_cb(struct timer_list *t)
{
pr_info("countdown_timers: a timer in the countdown fired\n");
}
static int __init countdown_init(void)
{
int i;
if (count 0\n");
return -EINVAL;
}
timers = kcalloc(count, sizeof(*timers), GFP_KERNEL);
if (!timers)
return -ENOMEM;
for (i = 0; i < count; i++) {
timer_setup(&timers[i], countdown_cb, 0);
mod_timer(&timers[i], jiffies + msecs_to_jiffies((count - i) * 1000));
}
return 0;
}
Rebuild the timer, kthread, and workqueue mini-project drivers from the earlier lectures in this series on a current 6.x kernel and confirm through dmesg that each still behaves as documented. Note anything that needed updating for your specific kernel version.
Extend a basic workqueue module so it queues two separate work items on module load, each logging its own identity and the PID of the kworker thread that ran it.
static void work_one(struct work_struct *w) { pr_info("workqueue_pair: item ONE, pid %d\n", current->pid); }
static void work_two(struct work_struct *w) { pr_info("workqueue_pair: item TWO, pid %d\n", current->pid); }
static DECLARE_WORK(w1, work_one);
static DECLARE_WORK(w2, work_two);
static int __init workqueue_pair_init(void)
{
schedule_work(&w1);
schedule_work(&w2);
return 0;
}
Building on Exercise 6, add a third work item that is delayed rather than immediate. Take the delay as a module parameter named extra_delay_ms, defaulting to 500 ms, and use INIT_DELAYED_WORK plus schedule_delayed_work().
static int extra_delay_ms = 500;
module_param(extra_delay_ms, int, 0444);
static void work_three(struct work_struct *w)
{
pr_info("workqueue_delayed_extra: delayed item fired, pid %d\n", current->pid);
}
static DECLARE_DELAYED_WORK(w3, work_three);
static int __init workqueue_delayed_extra_init(void)
{
schedule_delayed_work(&w3, msecs_to_jiffies(extra_delay_ms));
return 0;
}
Tip: inside a delayed work callback, if you need the enclosing structure, remember container_of() must reference the work member of struct delayed_work, not the delayed_work struct itself.
Frequently Asked Questions
Q1. Should I always prefer a workqueue over a raw kernel thread?
For most modern drivers, yes — workqueues remove the manual lifecycle management a kthread requires. Reach for a dedicated kthread only when you need continuous control over a long-lived context, such as a custom scheduling policy.
Q2. Why did my timer callback crash when I called a sleeping function inside it?
Kernel timer callbacks run in softirq context, which cannot sleep. Any sleeping work must be handed off to a workqueue or kthread from inside the callback.
Q3. Is del_timer() still valid on a current 6.x kernel?
No. The legacy del_timer() and del_timer_sync() wrapper names were removed in kernel 6.15; use timer_delete_sync() for a running cancellation and timer_shutdown_sync() in your exit path.
Q4. What is the single biggest mistake in student workqueue code?
Forgetting to flush or destroy a custom workqueue before module exit, which can leave pending work referencing freed memory.
Q5. Do I need to memorize all four mechanisms before moving on?
You need the decision model in the diagram above more than memorized function signatures — the exact APIs are one documentation lookup away once you know which tool fits.
Q6. Where should I go next in this free Linux kernel development course?
The next block covers kernel synchronization: spinlocks, mutexes, and the race conditions these timer/thread/workqueue mechanisms can introduce when they share data.
Continue the Free Linux Kernel Programming Course
More hands-on lectures on kernel synchronization are coming next in this series.
Browse All Lectures Join the Community
2 Comments