Free Linux Kernel Programming Course — Module 6
Linux Kernel Execution Contexts
Process Context vs Interrupt Context — Everything You Need to Know
Beginner
~20 min
Linux 6.x
100% Free
🎓 What You Will Learn
- What kernel execution context means and why it matters for writing Linux kernel code
- The difference between process context and interrupt context
- How a user-space thread transitions into kernel space via a system call
- How hardware interrupts trigger the interrupt service routine (ISR) in interrupt context
- Why these two contexts have different restrictions and how that affects driver development
- How to write safe kernel code that respects the rules of each context
- How to check the current execution context in a Linux kernel module
Before diving in, make sure you are comfortable with:
- Basic C programming (pointers, structs, functions)
- What a Linux process is and how it runs in user space
- A rough idea of what the Linux kernel does (it manages hardware and resources)
- Basic understanding of system calls (e.g.
read(),write())
No kernel programming experience is required — this is a free Linux kernel course built for beginners.
Introduction: Why Does Execution Context Matter?
One of the most important concepts in Linux kernel programming is understanding where and how your kernel code is running at any given moment. This is called the kernel execution context.
When you write a Linux kernel module or a device driver, you are not writing a normal program. The code you write can execute in very different environments depending on what triggered it. Some code runs because a user application asked for something. Other code runs because a hardware device fired an interrupt signal. These two situations have completely different rules about what you can and cannot do.
Getting this wrong is one of the most common sources of kernel bugs, crashes, and system freezes. This free Linux kernel development tutorial explains it clearly from the ground up.
The Monolithic Kernel Architecture of Linux
Linux is a monolithic kernel. This means that the entire kernel — memory management, file systems, device drivers, network stack — all run in a single privileged address space called kernel space.
In contrast, a microkernel design would run many of these services in separate user-space processes. Linux chose the monolithic approach for performance. The trade-off is that a bug anywhere in the kernel can affect the entire system.
(e.g. bash)
(e.g. nginx)
(e.g. app)
The important point is this: user-space processes run in unprivileged mode (Ring 3 on x86). When they need to do something that requires kernel access — like reading a file or sending data over a network — they make a system call, which causes the CPU to switch to privileged kernel mode (Ring 0).
What is Process Context in the Linux Kernel?
When a user-space process or thread makes a system call, the CPU saves the user-space register state and switches execution into kernel mode. From this point, the kernel code is running — but critically, it is running on behalf of that specific user process. This is called process context.
The System Call Path into Process Context
Here is the flow step by step:
- A user-space thread calls something like
read(fd, buf, count) - The C library wraps this in a software interrupt or
syscallinstruction - The CPU switches from Ring 3 (user) to Ring 0 (kernel)
- The kernel’s system call handler takes over
- The kernel runs the relevant driver or VFS code — all in process context
- When done, it returns control back to user space
read(fd, buf, n)syscall / software interrupt
- Runs in the context of a specific process (has a valid
currentpointer) - Synchronous — the calling process blocks until done
- Can sleep / block (e.g. wait for I/O)
- Can access user-space memory (carefully)
- Can use
mutex_lock(),kmalloc(GFP_KERNEL), etc.
The key property of process context is that there is always a well-defined current process. The kernel uses a macro called current to access the task structure (struct task_struct) of the running process. In process context, current always points to something meaningful.
Another critical property: code running in process context is allowed to sleep. If the kernel needs to wait for something — like waiting for a disk read to complete — it can put the current process to sleep and let the scheduler run another process. This is perfectly safe in process context.
open(), read(), write(), ioctl(), and release() file operations are all called from process context when a user-space application calls them.Code Example: Logging the Current Process Name in Process Context
Inside a kernel module’s file operation, you can access the name of the current process like this:
#include <linux/kernel.h>
#include <linux/sched.h> /* for 'current' macro */
static ssize_t my_driver_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
/* 'current' is a macro pointing to the task_struct of the
* process that called read() on our device file.
* This is valid because we are in PROCESS CONTEXT.
*/
pr_info("my_driver: read() called by process: %s (PID %d)\n",
current->comm, /* process name, max 16 chars */
current->pid); /* process ID */
/* ... rest of read logic ... */
return 0;
}
The current macro is a per-CPU variable that always points to the struct task_struct of the currently running task. It is valid only in process context. Accessing it in interrupt context can give you garbage or point to a random process that happened to be running when the interrupt fired.
What is Interrupt Context in the Linux Kernel?
Now here is where things get more complex — and more interesting. Your code does not always enter the kernel because a process asked for something. Hardware devices can demand attention at any time. When a keyboard key is pressed, when a network packet arrives, when a disk transfer completes — the hardware sends an interrupt signal to the CPU.
The CPU temporarily stops whatever it was doing, saves its current state, and jumps to a special function called the Interrupt Service Routine (ISR) or interrupt handler. This handler runs in interrupt context.
How Hardware Interrupts Work
Every hardware device is assigned an IRQ (Interrupt Request) line. When the device needs attention:
- The device asserts its IRQ line to the interrupt controller (e.g. APIC on x86)
- The interrupt controller signals the CPU
- The CPU finishes its current instruction, saves registers onto the stack
- The CPU switches to kernel mode and calls the registered interrupt handler
- The ISR runs — in interrupt context
- After the ISR returns, the CPU restores the saved state and resumes what it was doing
Any process or kernel code
(e.g. NIC packet)
& switches to
kernel mode
Interrupt Handler Code
(no sleeping allowed!)
- Cannot sleep — will cause a kernel panic
- Cannot block — no waiting for resources
currentpointer is not meaningful (random process)- Cannot use
mutex_lock()(may sleep) - Cannot use
kmalloc(GFP_KERNEL)(may sleep) - Must use
kmalloc(GFP_ATOMIC)instead - Must be fast — keep interrupt handler as short as possible
The most important rule in interrupt context is: you cannot sleep. The reason is straightforward. When an interrupt fires, it preempts whatever was running. The kernel cannot just put the interrupt handler to sleep — there is nothing to wake it up in a meaningful way, and it would hold up the interrupted task indefinitely. Sleeping in interrupt context causes a kernel panic.
Code Example: Registering and Writing an Interrupt Handler
Here is how you register an interrupt handler in a Linux kernel module:
#include <linux/interrupt.h>
#include <linux/kernel.h>
#define MY_IRQ 11 /* example IRQ number */
#define MY_DEV "my_device"
/* The interrupt handler - runs in INTERRUPT CONTEXT */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
/* RULES in interrupt context:
* - Do NOT sleep (no mutex_lock, no wait_event, no msleep, etc.)
* - Do NOT access user space memory
* - Keep this function SHORT and FAST
* - Use GFP_ATOMIC if you need to allocate memory
* - 'current' does NOT refer to any meaningful process here
*/
pr_info("IRQ %d received - in INTERRUPT CONTEXT\n", irq);
/* Acknowledge hardware, read status register, queue work, etc. */
/* For longer work, use a tasklet or workqueue (bottom half) */
return IRQ_HANDLED;
}
/* Called during module init - runs in PROCESS CONTEXT */
static int __init my_driver_init(void)
{
int ret;
/* request_irq() can sleep, so we call it from process context */
ret = request_irq(MY_IRQ, /* IRQ number */
my_irq_handler, /* handler function */
IRQF_SHARED, /* flags */
MY_DEV, /* device name */
(void *)my_irq_handler); /* dev_id */
if (ret) {
pr_err("Failed to register IRQ %d: %d\n", MY_IRQ, ret);
return ret;
}
pr_info("IRQ %d registered successfully\n", MY_IRQ);
return 0;
}
static void __exit my_driver_exit(void)
{
/* free_irq() must also be called from process context */
free_irq(MY_IRQ, (void *)my_irq_handler);
pr_info("IRQ %d released\n", MY_IRQ);
}
mutex_lock(), down() (semaphore), msleep(), wait_event(), or any other sleeping function from inside an interrupt handler. This will crash your kernel with a message like “BUG: sleeping function called from invalid context”.Process Context vs Interrupt Context: Side-by-Side Comparison
| Property | Process Context | Interrupt Context |
|---|---|---|
| Triggered by | System call, page fault, kernel thread | Hardware IRQ from peripheral device |
| Execution flow | Synchronous (top-down) | Asynchronous (bottom-up) |
current pointer |
Valid — points to calling process | Not meaningful (random process) |
| Can sleep? | Yes | No (kernel panic if you try) |
| Can block? | Yes | No |
| Memory allocation | kmalloc(GFP_KERNEL) |
kmalloc(GFP_ATOMIC) only |
| Locking | mutex_lock(), down() |
Spinlocks only (spin_lock_irq()) |
| User memory access | Yes (via copy_to_user()) |
No |
| Must be fast? | Not strictly required | Yes — keep as short as possible |
| Example in drivers | .read(), .write(), .open(), .ioctl() |
ISR registered with request_irq() |
How to Detect the Current Execution Context in Your Kernel Code
Linux provides a kernel helper to check whether your code is running in interrupt context. This is useful for writing safe kernel modules and drivers that work correctly regardless of how they are called.
#include <linux/interrupt.h>
void my_kernel_function(void)
{
if (in_interrupt()) {
/* We are in interrupt context (hardirq or softirq) */
pr_info("Running in INTERRUPT CONTEXT\n");
/* Do NOT sleep. Use GFP_ATOMIC for allocations. */
} else {
/* We are in process context */
pr_info("Running in PROCESS CONTEXT\n");
pr_info("Current process: %s (PID %d)\n",
current->comm, current->pid);
/* Can sleep if needed. GFP_KERNEL is fine. */
}
}
There are also more specific helpers:
in_hardirq()— true if running in a hardware IRQ handlerin_softirq()— true if running in a softirq or taskletin_atomic()— true if sleeping is not allowed (includes spinlock-held sections)in_task()— true if running in a normal process/thread context
in_interrupt() returns true for both hard IRQ and soft IRQ contexts. The behaviour is consistent across Linux 5.x and 6.x. Always prefer in_hardirq() or in_softirq() when you need to distinguish between them.Top Halves and Bottom Halves: Splitting Interrupt Work
A well-written interrupt handler should do as little work as possible in interrupt context. The reason: while your ISR is running, the same interrupt (and often other interrupts) are disabled on the current CPU. If your handler takes too long, other hardware events get delayed and the system becomes unresponsive.
Linux solves this with a top-half / bottom-half design:
- Runs immediately when hardware IRQ fires
- Must be very fast (microseconds)
- Acknowledge the hardware interrupt
- Read critical data from device registers
- Schedule bottom-half work
- Cannot sleep
Static, compile-time. Used by network stack, block layer. Still in softirq context (no sleeping).
Dynamic. Built on softirq. Common in older drivers. Cannot sleep. Runs on same CPU.
Runs in PROCESS CONTEXT via kernel thread. Can sleep! Best for longer deferred work.
For new driver code, workqueues are the recommended mechanism for deferred work because they run in process context and can sleep. Tasklets are considered legacy in modern kernels (Linux 6.x onwards discourages new tasklet usage).
Common Mistakes and How to Avoid Them
Calling mutex_lock(), msleep(), wait_event(), or kmalloc(GFP_KERNEL) from an ISR. This causes a kernel panic with “BUG: sleeping function called from invalid context”.
Fix: Use spin_lock() and kmalloc(GFP_ATOMIC) in ISRs. Move slow work to a workqueue.
current in Interrupt ContextAccessing current->pid or current->comm inside an interrupt handler. In interrupt context, current points to whatever process happened to be running when the interrupt fired — which is meaningless for your driver.
Fix: Only use current in process context. Use in_interrupt() to guard if unsure.
Writing complex data processing logic directly inside the interrupt handler. This delays other interrupts and can make the system sluggish.
Fix: Keep the ISR minimal. Schedule a workqueue for the real work.
Calling request_irq() in module_init() but not calling free_irq() in module_exit(). This leaves the IRQ reserved even after the module is removed.
Fix: Always pair request_irq() with free_irq() in cleanup.
Best Practices for Writing Context-Aware Kernel Code
- Always know your context. Before writing any kernel function, decide: will this be called from process context, interrupt context, or both?
- Use
in_interrupt()defensively in shared utility functions to catch wrong usage early. - Keep ISRs minimal. Do the bare minimum (ack the hardware, read status, wake up waiting process or schedule bottom half) and return.
- Prefer workqueues over tasklets for deferred work in modern kernels (Linux 5.x / 6.x).
- Use the correct memory allocation flag:
GFP_KERNELin process context,GFP_ATOMICin interrupt or atomic context. - Use the correct locking primitives: mutexes in process context only, spinlocks in interrupt context.
- Enable
CONFIG_DEBUG_ATOMIC_SLEEPduring development to catch sleeping-in-wrong-context bugs at runtime.
Key Takeaways
- Every piece of Linux kernel code runs in one of two contexts: process context or interrupt context
- Process context is entered via system calls or processor exceptions.
currentis valid, sleeping is allowed, and most kernel APIs are available - Interrupt context is entered via hardware IRQs. Sleeping is forbidden,
currentis meaningless, and only atomic operations are allowed - Device driver file operations (
read,write,ioctl) run in process context; ISRs registered withrequest_irq()run in interrupt context - For deferred interrupt work, use workqueues (process context, can sleep) rather than tasklets (deprecated pattern)
- Use
in_interrupt(),in_hardirq(), andin_task()to programmatically check your context - Always use
GFP_ATOMICfor memory allocation in interrupt context andGFP_KERNELin process context
Frequently Asked Questions (FAQ)
Yes. A utility function inside your driver can theoretically be called from both contexts. In that case, you must write it to be safe for the most restrictive context (interrupt context) — meaning no sleeping, use GFP_ATOMIC for allocations, and use spinlocks. Alternatively, have two separate code paths guarded by in_interrupt().
mutex_lock() in interrupt context?The kernel detects this and prints a warning or panic depending on your kernel configuration. With CONFIG_DEBUG_ATOMIC_SLEEP=y, the kernel immediately prints a stack trace and BUG message. Without it, the system may silently corrupt state or freeze.
printk() safe to call from interrupt context?Yes. printk() and pr_info(), pr_err() etc. are safe to call from both process context and interrupt context. They use an internal ring buffer and are designed to be atomic-safe.
A hardirq is a true hardware interrupt triggered by a device. A softirq is a deferred software mechanism that runs after hardirqs are processed. Softirqs are statically defined in the kernel and include the network receive path (NET_RX_SOFTIRQ) and timer processing (TIMER_SOFTIRQ). Both run in interrupt context and cannot sleep.
Kernel threads run entirely in process context. They are scheduled by the Linux scheduler just like user-space threads but they never execute in user space. Since they run in process context, they can sleep, use mutexes, and call most kernel APIs. Workqueues are implemented using kernel threads.
GFP_KERNEL and GFP_ATOMIC?GFP_KERNEL tells the memory allocator that it is allowed to sleep if memory is not immediately available (it may wait for pages to be reclaimed). GFP_ATOMIC tells the allocator it must not sleep — it will either succeed immediately from a reserved memory pool or fail. Use GFP_ATOMIC in interrupt context and spinlock-held sections.
copy_to_user() or copy_from_user() in interrupt context?No. These functions may sleep (they can trigger a page fault which requires process context to resolve). They should only be called from process context, typically inside your driver’s read() and write() file operations.
The standard patterns are: (1) write data into a shared buffer protected by a spinlock, then wake up a sleeping process using wake_up(); (2) queue work onto a workqueue from the ISR; (3) use a kernel FIFO (kfifo) for lock-free data transfer between ISR and process context. The waiting process (in process context) can block on wait_event_interruptible() until the ISR signals it.
Absolutely. The process context / interrupt context distinction is fundamental and unchanged since the earliest Linux kernels. It remains essential knowledge for anyone learning Linux kernel development or writing device drivers on Linux 6.x. Every driver that handles hardware interrupts must understand these rules.
EmbeddedPathashala offers a completely free Linux kernel programming course covering LKM development, memory management, synchronization, device drivers, and more. Continue with the next lecture in this series below.
Continue Your Free Linux Kernel Journey
EmbeddedPathashala offers 100% free courses on Linux kernel programming, device drivers, embedded systems, and Bluetooth/BLE development.
🎓 Browse All Free Courses