Kernel Korner - The New Work Queue Interface in the 2.6 Kernel


For most UNIX systems, Linux included, device drivers typically divide the work of processing interrupts into two parts or halves. The first part, the top half, is the familiar interrupt handler, which the kernel invokes in response to an interrupt signal from the hardware device. Unfortunately, the interrupt handler is true to its name: it interrupts whatever code is executing when the hardware device issues the interrupt. That is, interrupt handlers (top halves) run asynchronously with respect to the currently executing code. Because interrupt handlers interrupt already executing code (whether it is other kernel code, a user-space process or even another interrupt handler), it is important that they run as quickly as possible.

Worse, some interrupt handlers (known in Linux as fast interrupt handlers) run with all interrupts on the local processor disabled. This is done to ensure that the interrupt handler runs without interruption, as quickly as possible. More so, all interrupt handlers run with their current interrupt line disabled on all processors. This ensures that two interrupt handlers for the same interrupt line do not run concurrently. It also prevents device driver writers from having to handle recursive interrupts, which complicate programming. If interrupts are disabled, however, other interrupt handlers cannot run. Interrupt latency (how long it takes the kernel to respond to a hardware device's interrupt request) is a key factor in the performance of the system. Again, the speed of interrupt handlers is crucial.

To facilitate small and fast interrupt handlers, the second part or bottom half of interrupt handling is used to defer as much of the work as possible away from the top half and until a later time. The bottom half runs with all interrupts enabled. Therefore, a running bottom half does not prevent other interrupts from running and does not contribute adversely to interrupt latency.

Nearly every device driver employs bottom halves in one form or another. The device driver uses the top half (the interrupt handler) to respond to the hardware and perform any requisite time-sensitive operations, such as resetting a device register or copying data from the device into main memory. The interrupt handler then marks the bottom half, instructing the kernel to run it as soon as possible, and exits.

In most cases, then, the real work takes place in the bottom half. At a later point—often as soon as the interrupt handler returns—the kernel executes the bottom half. Then the bottom half runs, performing all of the remaining work not carried out by the interrupt handler. The actual division of work between the top and bottom halves is a decision made by the device driver's author. Generally, device driver authors attempt to defer as much work as possible to the bottom half.

Confusingly, Linux offers many mechanisms for implementing bottom halves. Currently, the 2.6 kernel provides softirqs, tasklets and work queues as available types of bottom halves. In previous kernels, other forms of bottom halves were available; they included BHs and task queues. This article deals with the new work queue interface only, which was introduced during the 2.5 development series to replace the ailing keventd part of the task queue interface.

Introducing Work Queues

Work queues are interesting for two main reasons. First, they are the simplest to use of all the bottom-half mechanisms. Second, they are the only bottom-half mechanism that runs in process context; thus, work queues often are the only option device driver writers have when their bottom half must sleep. In addition, the work queue mechanism is brand new, and new is cool.

Let's discuss the fact that work queues run in process context. This is in contrast to the other bottom-half mechanisms, which all run in interrupt context. Code running in interrupt context is unable to sleep, or block, because interrupt context does not have a backing process with which to reschedule. Therefore, because interrupt handlers are not associated with a process, there is nothing for the scheduler to put to sleep and, more importantly, nothing for the scheduler to wake up. Consequently, interrupt context cannot perform certain actions that can result in the kernel putting the current context to sleep, such as downing a semaphore, copying to or from user-space memory or non-atomically allocating memory. Because work queues run in process context (they are executed by kernel threads, as we shall see), they are fully capable of sleeping. The kernel schedules bottom halves running in work queues, in fact, the same as any other process on the system. As with any other kernel thread, work queues can sleep, invoke the scheduler and so on.

Normally, a default set of kernel threads handles work queues. One of these default kernel threads runs per processor, and these threads are named events/n where n is the processor number to which the thread is bound. For example, a uniprocessor machine would have only an events/0 kernel thread, whereas a dual-processor machine would have an events/1 thread as well.

It is possible, however, to run your work queues in your own kernel thread. Whenever your bottom half is activated, your unique kernel thread, instead of one of the default threads, wakes up and handles it. Having a unique work queue thread is useful only in certain performance-critical situations. For most bottom halves, using the default thread is the preferred solution. Nonetheless, we look at how to create new work queue threads later on.

The work queue threads execute your bottom half as a specific function, called a work queue handler. The work queue handler is where you implement your bottom half. Using the work queue interface is easy; the only hard part is writing the bottom half (that is, the work queue handler).



Comment viewing options


A Simple Introduction to Device Drivers under Linux

Lokesh Venkateshiah's picture


Preparation: Installing Kernel Module Compilation Requirements



Preparation for Compiling Kernel Modules Under Debian


$ sudo apt-get install module-assistant

Fedora Kernel Source and Configuration


$ sudo yum install kernel-devel

Vanilla Kernel Source and Configuration



$ wget -c ""
Unpack the kernel source:

$ tar xjvf linux-.tar.bz2

$ cd linux-
$ make menuconfig

$ make help
A target that will work on almost every distro is:

$ make tar-pkg
When finished building, install your new kernel with:

$ sudo tar -C / -xvf linux-.tar


"Hello, World!" Using printk()


Download the hello_printk module tarball and extract it:

$ tar xzvf hello_printk.tar.gz

obj-m := hello_printk.o


PWD := $(shell pwd)


Now, let's run through the code in hello_printk.c.


static int __init
printk("Hello, world!\n");
return 0;


static void __exit
printk("Goodbye, world!\n");


MODULE_AUTHOR("Valerie Henson ");


$ cd hello_printk
$ make

$ sudo insmod ./hello_printk.ko
$ dmesg | tail

$ sudo rmmod hello_printk
$ dmesg | tail
You have successfully compiled and installed a kernel module!

Hello, World! Using /proc





static int
void *data)

Now, for the body of the function:

* We only support reading the whole string at once.
Then, we're ready to compile and load the module:

$ cd hello_proc
$ make
$ sudo insmod ./hello_proc.ko

$ cat /proc/hello_world
Hello, world!

Hello, World! Using /dev/hello_world


Download the hello world module tarball. We'll go through hello_dev.c.

#include <



size_t count, loff_t *ppos)
* We only support reading the whole string at once.
if (count < len)
return -EINVAL;
if (*ppos != 0)
return 0;
* Besides copying the string to the user provided buffer,
* this function also checks that the user has permission to
if (copy_to_user(buf, hello_str, len))
return -EINVAL;
* Tell the user how much data we wrote.
*ppos = len;

return len;


static const struct file_operations hello_fops = {
.owner = THIS_MODULE,
.read = hello_read,

static struct miscdevice hello_dev = {
* kernel to just pick one.
* Name ourselves /dev/hello.
* What functions to call when a program performs file
* operations on the device.

static int __init
int ret;

* Udev will automatically create the /dev/hello device using
* the default rules.
ret = misc_register(&hello_dev);
if (ret)

return ret;

And remember to unregister the device in the exit function.

static void __exit


MODULE_AUTHOR("Valerie Henson ");
Compile and load the module:

$ cd hello_dev
$ make
$ sudo insmod ./hello_dev.ko

$ sudo cat /dev/hello
Hello, world!
But we can't read it as a regular user:

$ cat /dev/hello
cat: /dev/hello: Permission denied
$ ls -l /dev/hello


We'll break the rule down into parts and explain each part.







$ sudo cp hello.rules /etc/udev/

$ sudo rmmod hello_dev
$ sudo insmod ./hello_dev.ko
$ ls -l /dev/hello*

$ cat /dev/hello_world
Hello, world!
$ cat /dev/hello
Hello, world!

Worker Thread Execution in SMP System

Anil123's picture


thread will be scheduled in SMP system.


Could this be updated for the 2.6.20 kernel?

chuck's picture


Thanks for the wonderful information

kasiviswanathan's picture


Thanks very much! I've been

Anonymous's picture



Priority of custom threads?

Anonymous's picture
