The timer callback should do as little as possible. Move the real work somewhere safer.
Driver 04 worked, but it was doing too much in softirq context — carefully shuffling data around spinlocks to avoid sleeping. The right pattern is simpler: the timer callback should do one thing only — schedule work — and hand everything else to a workqueue running in process context, where sleeping is allowed again.
| Mechanism | Context | Can sleep? |
|---|---|---|
| Workqueue | Process (kernel thread) | ✅ Yes |
| Threaded IRQ | Process (dedicated thread) | ✅ Yes |
| Softirq | Atomic | ❌ No |
| Tasklet | Atomic | ❌ No |
void timer_callback(struct timer_list *data)
{
schedule_work(&workqueue); /* hand off immediately */
if (kernel_logger.timer_active)
mod_timer(&etx_timer,
jiffies + msecs_to_jiffies(TIMEOUT));
}void workqueue_fn(struct work_struct *work)
{
unsigned long flags;
spin_lock_irqsave(&kernel_logger.lock, flags);
snprintf(kernel_logger.kernel_buffer[kernel_logger.write_indexer],
MEM_SIZE, "worker_event_%ld\n",
kernel_logger.timer_count++);
/* advance write pointer, update count */
spin_unlock_irqrestore(&kernel_logger.lock, flags);
wake_up_interruptible(&etx_wait_queue); /* safe — we're in process ctx */
}del_timer_sync(&etx_timer); /* stop timer, wait for callback */
flush_work(&workqueue); /* wait for any queued work to finish */Order matters: stop the timer first so no new work gets scheduled, then flush any work already queued.