Introduction
In our last lesson, we established the fundamental "why" of deferred work in the Linux kernelโthe top-half/bottom-half model. We saw that to keep interrupt latency low, the interrupt service routine (ISR) must be as fast as possible, deferring any longer-running tasks to a later time.
Today, we address the "how." This lesson focuses on the learning outcome: Compare softirqs, tasklets, and workqueues, and select the appropriate deferred work mechanism for a given scenario. We will explore the three primary mechanisms the kernel provides for deferring work, analyzing their characteristics, performance trade-offs, and constraints. For an aspiring kernel developer, mastering this choice is crucial; it's a core design decision that directly impacts driver performance, correctness, and system stability.
1. The Fundamental Dividing Line: Execution Context
The most important factor distinguishing these mechanisms is their execution context. In the kernel, code can run in one of two major contexts:
-
Interrupt Context: This is a special, restricted context. Code running here is not associated with a specific process. It executes with interrupts enabled but is not allowed to do anything that might sleep or block. This includes acquiring a mutex, allocating memory with
kmalloc(which can sleep), or performing blocking I/O. Softirqs and tasklets run in this context. -
Process Context: This is the normal execution context where the kernel is running on behalf of a process (e.g., during a system call or as a kernel thread). Code in this context is allowed to sleep and block. Workqueues run in this context.
This distinction is the root of most of the trade-offs we'll discuss.

The kernel itself needs to track which context the CPU is currently in. It does this using a per-CPU variable called preempt_count. Different bits within this variable are incremented when the kernel enters a hard IRQ, soft IRQ, or NMI context.

2. The Deferred Work Mechanisms
Let's now dive into the details of each mechanism. We will start with the lowest-level and highest-performance option and move towards the most flexible.
Softirq, Tasklets and Workqueues
To start, let's get a high-level overview of the three mechanisms. This document from the "Linux Inside" series provides detailed explanations of each.
Please begin by reading the introductory section, which sets the stage for our discussion.
A. Softirqs
Softirqs are the most fundamental and highest-priority deferred work mechanism. They are designed for the most performance-critical and high-frequency tasks in the kernel.
Softirq, Tasklets and Workqueues
Now, let's focus on softirqs using the "Linux Inside" guide. This section goes into the implementation details that are valuable for an in-depth understanding.
Read the "Softirqs" section. Pay close attention to these key characteristics: They are statically allocated at compile time (NR_SOFTIRQS). They are associated with per-CPU ksoftirqd kernel threads, which run pending softirqs if they are being triggered too frequently. A softirq is "raised" using raise_softirq(). You can see their activity in /proc/softirqs.
Key Properties of Softirqs:
- Context: Interrupt context (cannot sleep).
- Concurrency: A softirq handler must be reentrant. The same softirq (e.g.,
NET_RX_SOFTIRQfor network packet reception) can run simultaneously on multiple CPUs. This means any data it accesses must be protected with per-CPU variables or spinlocks. - Allocation: Static. There's a fixed number of softirq types defined in the kernel (
enumininclude/linux/interrupt.h). You cannot dynamically register a new type of softirq from a loadable module. - Use Cases: Reserved for core, high-performance subsystems like networking, timers, and RCU. You should almost never use a softirq directly in a device driver.
B. Tasklets
Tasklets are a more flexible mechanism built on top of softirqs. They are designed to simplify deferred work for most driver developers.
Softirq, Tasklets and Workqueues
Next, let's explore tasklets. They are a layer of abstraction over softirqs that makes them much easier to use.
Read the "Tasklets" section. Focus on the key differences from softirqs: They are implemented using two dedicated softirqs: TASKLET_SOFTIRQ and HI_SOFTIRQ. They can be dynamically allocated, making them suitable for modules. The same tasklet is guaranteed not to run on multiple CPUs simultaneously. If a tasklet is scheduled to run, it will run on the CPU it was scheduled on, and another attempt to schedule it will be ignored until it has finished.
Key Properties of Tasklets:
- Context: Interrupt context (cannot sleep).
- Concurrency: A specific tasklet handler is serialized. It is guaranteed to only run on one CPU at a time. This greatly simplifies locking, as you don't need to worry about the handler re-entering itself. However, different tasklets can still run in parallel on different CPUs.
- Allocation: Dynamic. You can initialize a
tasklet_structat runtime withtasklet_init(). - Use Cases: The "classic" bottom-half mechanism. Good for short, atomic tasks that are deferred from an ISR but don't need the complexity or re-entrancy of a softirq, and most importantly, do not need to sleep.
C. Workqueues
Workqueues are the most different and, for most modern drivers, the most useful of the three. They move the deferred work out of interrupt context entirely.
Softirq, Tasklets and Workqueues
Finally, we examine workqueues, the most flexible and powerful of the deferred work mechanisms.
Read the "Workqueues" section. The most critical takeaway is: Workqueue functions run in process context via kernel worker threads (kworker). This means they are allowed to sleep and block.
Key Properties of Workqueues:
- Context: Process context (can sleep).
- Concurrency: Handled by the scheduler. By default, a work item is executed by a shared
kworkerthread. Since the handler can sleep, you can use standard blocking primitives like mutexes for synchronization. - Allocation: Dynamic. You can initialize a
work_structand schedule it withschedule_work(). - Use Cases: The default and recommended choice for almost all deferred work in device drivers. If your deferred task needs to perform I/O, allocate memory, or acquire a mutex, you must use a workqueue.
3. Summary and Practical Demonstration
Let's consolidate what we've learned. The following reading provides a very clear "cheat sheet" that summarizes the key differences.
Interrupts โ The Linux Kernel documentation
This documentation page from the Linux Kernel Labs provides a succinct summary that is perfect for reinforcement.
Please read the sections titled "Deferrable actions", "Soft IRQs", "Tasklets", "Workqueues", and especially the "Deferrable actions summary". This summary table is a perfect key takeaway for comparing the mechanisms.
A Real-World Example: Tracing Network Softirqs
Theory is useful, but seeing these mechanisms in action provides a much deeper understanding. The networking subsystem is a heavy user of softirqs for processing incoming packets. The following video uses kernel tracing to show this in action.
Linux Networking: How The Kernel Handles A TCP Connection
This video from Deep Linux traces a simple TCP connection. It brilliantly illustrates the hand-off from a hardware interrupt to the network softirq (NET_RX_SOFTIRQ).
Please watch the following segments. As you watch, correlate the events in the trace with the concepts we've discussed: 27:43 - 29:28: The presenter first observes a softirq/net-rx being triggered and links it back to a hardware interrupt. This is the top-half to bottom-half delegation in action. 55:42 - 1:05:05: A more detailed walkthrough. Notice the sequence: Hardware IRQ -> irq_handler_entry -> softirq is raised -> ksoftirqd (or another context) runs the softirq handler (net_rx_action) -> the packet is processed by the network stack -> finally, the application is woken up. 1:15:22 - 1:18:21: A final, concise recap of the entire flow from hardware interrupt to softirq processing to unblocking the user application. \nThis demonstrates why softirqs are used for networking: packet processing is a high-frequency, performance-critical task that fits the softirq model perfectly.
4. Interview Scenario: Choosing the Right Mechanism
Now, let's apply this knowledge to a typical interview question designed to test your judgment as a kernel developer.
Scenario: You are writing a Linux driver for a device on a shared I2C bus. The device is a pressure sensor that signals via an interrupt when a new reading is available. Your top-half ISR has confirmed the interrupt and now needs to schedule a bottom half to read the 16-bit pressure value from the device's register over the I2C bus and make it available to user space.
Which deferred work mechanism (tasklet or workqueue) would you choose and why? What would be the consequence of choosing the wrong one?
Click for Analysis
-
The Correct Choice: Workqueue
The key to this problem lies in the nature of the I2C bus. I2C bus transactions are inherently blocking. When you call a function like
i2c_transfer(), the kernel may need to:- Acquire a lock to gain exclusive access to the I2C bus adapter.
- Wait for the previous transaction on the bus to complete.
- Sleep while waiting for the hardware to complete the byte transfers.
Because the I2C read operation can sleep, it must be performed in process context. Therefore, a workqueue is the only correct choice.
-
Consequence of Choosing a Tasklet (The Wrong Choice):
A tasklet runs in interrupt context, where sleeping is strictly forbidden. If you were to call
i2c_transfer()from a tasklet handler, the system would detect that you are trying to sleep in an atomic context. This would trigger a kernel warning (a "BUG" message) in the system logs, often looking something like "BUG: sleeping function called from invalid context" and print a full stack trace. In a non-debug kernel, this could lead to unpredictable behavior, deadlocks, or system crashes. -
Why a Softirq is Even More Wrong:
While also running in interrupt context, using a softirq would be extreme overkill. They are not designed for simple driver tasks, require static allocation, and demand complex reentrant-safe logic that is completely unnecessary here. An interviewer would see this as a sign of not understanding the purpose and complexity of the different mechanisms.
This scenario demonstrates that understanding the "can it sleep?" rule is the single most important factor when choosing between tasklets and workqueues.
Conclusion
We have now explored the "how" of deferred work in the Linux kernel. By understanding the trade-offs between softirqs, tasklets, and workqueues, you can make informed design decisions that lead to robust and efficient drivers.
Key Takeaways:
- The primary distinction is execution context: softirqs and tasklets run in interrupt context (cannot sleep), while workqueues run in process context (can sleep).
- Softirqs are statically allocated, can run concurrently on multiple CPUs, and are reserved for core, high-performance subsystems.
- Tasklets are dynamically allocated, serialized (the same tasklet won't run on two CPUs at once), and are a good choice for quick, non-sleeping deferred tasks.
- Workqueues are the most flexible and safest option. They are the go-to choice for any deferred work that might block, such as performing I/O, allocating memory, or acquiring mutexes.
- The Golden Rule: When in doubt, use a workqueue. The performance overhead is usually negligible compared to the risk of deadlocking the system by sleeping in an atomic context.
In our next lesson, we will put this theory into practice. Our goal will be to implement a kernel module that registers an interrupt handler and defers work using a workqueue, tying together the concepts from this lesson and the previous one into a complete, working example.