Understanding how Linux manages processes and execution contexts is essential for any systems programmer or curious engineer. In this post, we’ll break down the core concepts, with clear tables and takeaways perfect for beginners and intermediate users alike.
The Process
Definition
A process is a program in execution, meaning it is more than just code; it includes several associated resources, such as:
- Open files.
- Pending signals.
- Internal kernel data.
- Processor state.
- A memory address space (with memory mappings).
- One or more threads of execution.
- A data section (global variables).
Essentially, a process is a running instance of a program that actively utilizes system resources.
Threads of Execution
Character devices are hardware or virtual devices that transmit data as a stream of bytes. These devices include serial ports, keyboards, and mice. They allow reading and writing operations at the byte level, without requiring buffering mechanisms like block devices. Imagine sending a text message one character at a time instead of an entire paragraph now you get the idea.
A thread is the active component of a process. Each process can have one or more threads. Each thread has its own:
- Program counter (keeps track of execution).
- Process stack (for function calls, local variables).
- Processor registers (store temporary execution states).
The kernel schedules threads individually, not whole processes. Most modern programs use multithreading, where multiple threads run within a single process.
In Linux, threads are implemented as a special kind of process that shares the same memory space but has its own execution context.
Virtualization in Processes
A process provides two critical virtualizations:
- Virtual Processor – Each process perceives that it has exclusive access to the CPU, even though the OS manages CPU time between multiple processes.
- Virtual Memory – The process gets an isolated memory space, creating the illusion that it has access to the entire system’s memory.
Threads within a process share the virtual memory space but have separate virtual processors for execution.
Program vs Process
- A program is a static file containing executable code.
- A process is the dynamic execution of that code.
- Multiple processes can run the same program and may or may not share resources.
Process Lifecycle
- Creation
- A new process is created using the fork() system call, which duplicates an existing process.
- The original process is called the parent, and the newly created one is the child.
- The fork() call returns twice once in the parent and once in the child.
- Execution
- The exec() family of system calls replaces the current process’s address space with a new program, effectively starting fresh.
- Termination
- A process ends when it calls exit(), which releases allocated resources.
- The parent process can retrieve the exit status using wait4() or other wait() calls.
- Until the parent acknowledges termination, the process remains in a zombie state (waiting for cleanup).
Task vs. Process
- The Linux kernel internally refers to processes as “tasks.”
- While the terms are interchangeable, “task” is more commonly used in a kernel-level context when referring to processes.
Important Points
– A process is a running program with associated resources.
– Threads are separate execution units within a process.
– Linux treats threads as special processes that share the same memory space.
– Processes provide virtualized CPU and memory for execution.
– system calls:
fork()
→ Creates a new process.exec()
→ Loads a new program into a process.exit()
→ Terminates a process.
Zombie processes are processes that have exited but are waiting for the parent to acknowledge their termination.
Process Descriptor and the Task Structure
Character devices are one of the fundamental types of devices in a Linux system. Unlike block devices, which deal with large chunks of data (such as hard drives), character devices handle data one character at a time just like reading a book one letter at a time instead of an entire page. Slow? Maybe. But efficient in its own way! This article provides a comprehensive guide on character devices and drivers, starting from the basics and gradually moving toward advanced topics, including practical examples.
Task List
The kernel maintains a circular doubly linked list called the task list, which contains all running processes in the system. Each element in this list is a struct task_struct, defined in <linux/sched.h>. This task_struct is the process descriptor, holding all essential details about a process.
task_struct: The Process Descriptor
The task_struct is a large data structure (about 1.7 KB on a 32-bit machine) and contains all kernel-level information related to a process, including:
- Open files
- Address space (memory management)
- Pending signals
- Process state
- And much more
Allocating the Process Descriptor
- The task_struct is allocated using the slab allocator for object reuse and cache coloring, optimizing performance.
- Before Linux 2.6, task_struct was located at the end of the kernel stack.
- Now, struct thread_info is stored at the end (or top) of the kernel stack instead.
struct thread_info (x86 example)
struct thread_info {
struct task_struct *task; // Pointer to the process descriptor
struct exec_domain *exec_domain;
__u32 flags;
__u32 status;
__u32 cpu;
int preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void *sysenter_return;
int uaccess_err;
};
The task element inside thread_info points to the corresponding task_struct, allowing easy access to process information.
Storing the Process Descriptor
Process ID (PID)
- Each process has a unique Process ID (PID) of type pid_t (typically an integer).
- Default Maximum PID:
- 32,768
- Can be increased to ~4 million.
- PID Storage:
- Stored in the pid member of the task_struct.
- Maximum PID Definition:
- Defined in
<linux/threads.h>
- Defined in
Can be modified at runtime via: /proc/sys/kernel/pid_max
Kernel code usually references processes using pointers to task_struct rather than PIDs directly.
Fast Access to Process Descriptor: current Macro
- The current macro provides a quick way to obtain the task_struct of the currently running process.
- It is architecture-specific.
On x86 Architecture
- Uses
current_thread_info()
to get thread_info from the stack pointer.
Assembly code to retrieve thread_info:movl $-8192, %eax
andl %esp, %eax
Then:current_thread_info()->task
: returns the task_struct.
On PowerPC (PPC) Architecture
- The task_struct pointer is stored in register r2.
- The current macro simply returns its value.
Process State
The state field in task_struct describes the current condition of a process. There are five possible states:
- TASK_RUNNING
- The process is running or ready to run.
- This is the only state for processes executing in user space.
- TASK_INTERRUPTIBLE
- The process is sleeping (blocked), waiting for a specific condition.
- It can wake up when the condition is met or if a signal is received.
- TASK_UNINTERRUPTIBLE
- Similar to TASK_INTERRUPTIBLE, but does not wake up on signals.
- Used in critical situations where sleep must not be interrupted.
- __TASK_TRACED
- The process is being traced by another process (e.g., debugger like gdb).
- __TASK_STOPPED
- The process is paused due to signals such as SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU.
- Also used during debugging.
Important Points
– task_struct is the central kernel structure holding process information.
– thread_info helps efficiently locate the corresponding task_struct.
– Processes are identified by a unique PID, which can be adjusted as needed.
– The current macro provides fast access to the currently running process’s task_struct.
– Process states define the execution condition of a process, influencing scheduling and behavior.
How to Control the Current Process State
Changing Process State in the Linux Kernel
1. set_task_state()
- Purpose: Preferred method for changing a process’s state.
- Header File: Defined in
<linux/sched.h>
Syntax:
set_task_state(task, state);
Description:
- Sets the specified task to the given state.
- On SMP (Symmetric Multiprocessing) systems:
Includes a memory barrier to ensure proper ordering of memory operations.
On non-SMP systems:
Equivalent to directly assigning the state:task->state = state;
2. set_current_state()
- Purpose: Shorthand for setting the state of the current process.
- Header File: Defined in
<linux/sched.h>
Syntax:
set_current_state(state);
Description:
Equivalent to: set_task_state(current, state);
NOTE: Both functions are defined in <linux/sched.h>
Process Context
User-Space vs. Kernel-Space
- Normal program execution occurs in user-space.
- A system call or exception causes a transition to kernel-space.
- When executing in kernel-space on behalf of a process, the process is in “process context.”
Current Macro
- The current macro is only valid in process context.
- It returns the task_struct of the currently executing process.
Kernel Entry & Exit
- Entry: The only ways to enter kernel-space are:
- System calls
- Exception handlers
- Exit: Once kernel execution is complete, the process returns to user-space.
- If a higher-priority process is runnable, the scheduler is invoked before returning.
- If a higher-priority process is runnable, the scheduler is invoked before returning.
Interrupt Context in the Linux Kernel
What is Interrupt Context?
In a multitasking operating system like Linux, the CPU does not always execute user-space processes. Sometimes, it must handle hardware-generated interrupts that demand immediate attention. When this happens, the kernel temporarily pauses whatever it was doing and switches to what is known as interrupt context.
In interrupt context, the CPU is no longer executing on behalf of a specific process. Instead, it is responding to an interrupt request (IRQ) from a hardware device, such as a keyboard, network card, or timer.
Characteristics of Interrupt Context
- No Associated Process
- Unlike process context, where the kernel executes system calls on behalf of a specific user-space process, interrupt context does not belong to any process.
- Since interrupts can occur at any time, the kernel cannot assume that process-related data structures are valid or accessible.
- Minimal and Fast Execution
- The kernel must handle interrupts as quickly as possible to ensure system responsiveness.
- Interrupt handlers should avoid complex operations like memory allocation or accessing user-space memory.
- No Sleeping or Blocking Operations
- In interrupt context, the kernel cannot block or sleep, meaning it cannot perform operations like waiting for I/O, acquiring mutexes, or scheduling other tasks.
- This is because sleeping would cause the CPU to switch to another process while still inside an interrupt handler, leading to deadlocks or instability.
- Limited Access to Kernel Functions
- Some kernel functions are not safe to call from interrupt context, especially those that might sleep or perform long operations.
- Functions like kmalloc() with GFP_KERNEL flags (which may sleep) or copy_to_user() (which accesses user memory) cannot be used in interrupt handlers.
Interrupt Handling Workflow
- An Interrupt Occurs
- A hardware device signals an interrupt request (IRQ) to the CPU, indicating that it needs immediate attention.
- CPU Switches to Interrupt Context
- The kernel pauses the current execution (whether in user mode or kernel mode) and calls the appropriate interrupt handler.
- Interrupt Handler Executes
- The interrupt handler processes the interrupt, such as reading data from a hardware buffer or acknowledging the device.
- The handler must complete quickly to allow the CPU to resume normal execution.
- Return to Previous Execution
- After the interrupt is handled, the CPU resumes the execution of the interrupted task.
- After the interrupt is handled, the CPU resumes the execution of the interrupted task.
Process Context vs. Interrupt Context
Feature | Process Context | Interrupt Context |
---|---|---|
Associated with a Process? | Yes, executes system calls on behalf of a user-space process. | No, executes due to a hardware interrupt. |
Can Sleep/Block? | Yes, it can sleep, wait for I/O, or acquire locks. | No, sleeping or blocking is forbidden. |
Memory Access | Can access process-specific memory and user-space memory. | No guarantee of process-specific memory being valid. |
Execution Time | Can take longer (within reason) since it’s scheduled. | Must be very fast to avoid delaying the system. |
Safe Kernel Functions | Most kernel functions are safe to call. | Many functions, especially those that sleep, cannot be used. |
Why is Interrupt Context Important
- Ensures Timely Response to Hardware Events: Interrupts are essential for real-time responsiveness, ensuring that hardware events are handled without unnecessary delays.
- Prevents System Instability: Understanding interrupt context helps developers avoid deadlocks, race conditions, or system crashes by ensuring that interrupt handlers are properly designed.
- Optimizes Performance: Since interrupt handlers must execute quickly, they are often designed to defer complex processing to a later time (e.g., using workqueues or soft irqs).
THANK YOU