Mastering Linux Kernel Workqueues: Theory, Structures, and a Practical Driver Demo
This guide explains Linux kernel workqueues, their core structures and default system queues, shows how to use the schedule_work API, and walks through a complete driver example that captures the ESC key, queues a work item, and verifies execution via dmesg.
What Is a Workqueue?
A workqueue in the Linux kernel is a mechanism for handling the bottom half of interrupt processing. It works like a message queue, holding work_struct items that are processed by kernel threads.
The diagram illustrates the various mechanisms available for the lower half of interrupt handling and how to choose among them based on business scenarios and constraints.
Key Data Structures
Two structures are central to workqueues:
struct workqueue_struct {
struct list_head pwqs; /* all pwqs of this wq */
struct list_head list; /* all workqueues */
...
char name[WQ_NAME_LEN];
...
unsigned int flags ____cacheline_aligned;
struct pool_workqueue __percpu *cpu_pwqs;
struct pool_workqueue __rcu *numa_pwq_tbl[];
};
struct work_struct {
atomic_long_t data;
struct list_head entry;
work_func_t func; // pointer to handler
#ifdef CONFIG_LOCKDEP
struct lockdep_map lockdep_map;
#endif
};Work items are linked together in a list and are taken by kernel threads created either by drivers (via kthread_create) or pre‑allocated by the kernel.
Default System Workqueues
The kernel creates several global workqueues that are shared by all drivers, defined in include/linux/workqueue.h:
extern struct workqueue_struct *system_wq;
extern struct workqueue_struct *system_highpri_wq;
extern struct workqueue_struct *system_long_wq;
extern struct workqueue_struct *system_unbound_wq;
extern struct workqueue_struct *system_freezable_wq;
extern struct workqueue_struct *system_power_efficient_wq;
extern struct workqueue_struct *system_freezable_power_efficient_wq;These queues are instantiated in kernel/workqueue.c during early kernel initialization:
int __init workqueue_init_early(void)
{
...
system_wq = alloc_workqueue("events", 0, 0);
system_highpri_wq = alloc_workqueue("events_highpri", WQ_HIGHPRI, 0);
system_long_wq = alloc_workqueue("events_long", 0, 0);
system_unbound_wq = alloc_workqueue("events_unbound", WQ_UNBOUND, WQ_UNBOUND_MAX_ACTIVE);
system_freezable_wq = alloc_workqueue("events_freezable", WQ_FREEZABLE, 0);
system_power_efficient_wq = alloc_workqueue("events_power_efficient", WQ_POWER_EFFICIENT, 0);
system_freezable_power_efficient_wq = alloc_workqueue("events_freezable_power_efficient", WQ_FREEZABLE | WQ_POWER_EFFICIENT, 0);
...
}Because system_wq is heavily used, the kernel provides a convenience wrapper:
static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}Driver Example Using a Workqueue
The following example creates a simple driver that registers an interrupt handler for a keyboard key (ESC). When the key is pressed, the handler queues a work item on system_wq and the work handler prints a message.
Directory Layout
$ cd tmp/linux-4.15/drivers
$ mkdir my_driver_interrupt_wq
$ touch my_driver_interrupt_wq.c
$ touch MakefileFull Driver Source (my_driver_interrupt_wq.c)
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
static int irq;
static char *devname;
static struct work_struct mywork;
module_param(irq, int, 0644);
module_param(devname, charp, 0644);
#define MY_DEV_ID 1226
struct myirq { int devid; };
struct myirq mydev = { MY_DEV_ID };
#define KBD_DATA_REG 0x60
#define KBD_STATUS_REG 0x64
#define KBD_SCANCODE_MASK 0x7f
#define KBD_STATUS_MASK 0x80
static void mywork_handler(struct work_struct *work)
{
printk("mywork_handler is called.
");
}
static irqreturn_t myirq_handler(int irq, void *dev)
{
struct myirq mydev;
unsigned char key_code;
mydev = *(struct myirq *)dev;
if (MY_DEV_ID == mydev.devid) {
key_code = inb(KBD_DATA_REG);
if (key_code == 0x01) { // ESC
printk("ESC key is pressed!
");
INIT_WORK(&mywork, mywork_handler);
schedule_work(&mywork);
}
}
return IRQ_HANDLED;
}
static int __init myirq_init(void)
{
printk("myirq_init is called.
");
if (request_irq(irq, myirq_handler, IRQF_SHARED, devname, &mydev) != 0) {
printk("register irq[%d] handler failed.
", irq);
return -1;
}
printk("register irq[%d] handler success.
", irq);
return 0;
}
static void __exit myirq_exit(void)
{
printk("myirq_exit is called.
");
free_irq(irq, &mydev);
}
MODULE_LICENSE("GPL");
module_init(myirq_init);
module_exit(myirq_exit);Makefile
ifneq ($(KERNELRELEASE),)
obj-m := my_driver_interrupt_wq.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endifBuild, Load, and Test
$ make
$ sudo insmod my_driver_interrupt_wq.ko irq=1 devname=mydev
$ lsmod | grep my_driver_interrupt_wq
my_driver_interrupt_wq 16384 0
$ dmesg | tail -n 5
[ 188.247636] myirq_init is called.
[ 188.247642] register irq[1] handler success.Press the ESC key. The kernel captures the keyboard interrupt, invokes myirq_handler, which queues the work item. The workqueue thread then runs mywork_handler and prints:
[ 305.053155] ESC key is pressed!
[ 305.053177] mywork_handler is called.This confirms that the workqueue mechanism correctly defers work from the interrupt context to a kernel thread.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Liangxu Linux
Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
