Arc I · Linux Driver Lab
Chapter 05

Workqueue Driver

The timer callback should do as little as possible. Move the real work somewhere safer.

workqueue schedule_work process context DECLARE_WORK

Why this driver?

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.

Linux bottom-half mechanisms

MechanismContextCan sleep?
WorkqueueProcess (kernel thread)✅ Yes
Threaded IRQProcess (dedicated thread)✅ Yes
SoftirqAtomic❌ No
TaskletAtomic❌ No

Two-stage design

Stage 1 — timer callback (softirq, does almost nothing)

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));
}

Stage 2 — work function (process context, does the real work)

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 */
}
wake_up_interruptible is now legal — it's called from process context, not from inside the spinlock. This is the core benefit of the workqueue pattern.

Clean exit

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.