Process Definition
Process Lifecycle
Read aloud is unavailable in this browser.
The Journey of a Process
Every process has a lifecycle—a journey from birth to death. Understanding this lifecycle is essential for:
- Systems programming: Knowing what happens at each stage of process execution
- Debugging: Understanding why a process is stuck, crashed, or not starting
- Resource management: Ensuring proper cleanup when processes terminate
- Concurrency: Understanding parent-child relationships and synchronization
A process doesn't simply 'run' and 'stop.' It transitions through well-defined states, influenced by system events, signals, and its own behavior. This page traces the complete journey from creation to destruction.
By the end of this page, you will understand how processes are created (fork, exec, spawn), the states a process transitions through during execution, what triggers each state transition, how processes terminate (normally and abnormally), and what happens to process resources after termination.
The Complete Lifecycle Model
The process lifecycle can be visualized as a state machine with well-defined states and transitions. While operating systems may have additional internal states, the conceptual model includes these fundamental stages:
| State | Description | Can Execute? | Consumes CPU? |
|---|---|---|---|
| New | Being created; resources being allocated | No | No |
| Ready | Waiting to be assigned to a CPU | Not yet | No |
| Running | Instructions being executed | Yes | Yes |
| Waiting (Blocked) | Waiting for I/O or event | No | No |
| Stopped | Execution suspended by signal | No | No |
| Terminated | Finished execution | No | No |
| Zombie | Terminated but not yet reaped | No | No |
Different operating systems and textbooks use varying terminology. Linux uses TASK_RUNNING (both running and ready), TASK_INTERRUPTIBLE, TASK_UNINTERRUPTIBLE, TASK_STOPPED, etc. The conceptual model remains consistent even if state names differ.
Process Creation: Birth of a Process
Processes don't appear spontaneously—they are explicitly created through system calls. The creation mechanism differs significantly between Unix/Linux and Windows.
Unix/Linux: fork() + exec()
The Unix model separates process creation into two steps:
- fork(): Create a copy of the current process
- exec(): Replace the copy's program with a new one
This separation provides remarkable flexibility—code can run between fork() and exec() to set up the child's environment.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> int main() { printf("Parent PID: %d", getpid()); pid_t pid = fork(); if (pid < 0) { // fork() failed perror("fork failed"); exit(1); } if (pid == 0) { // CHILD PROCESS printf("Child PID: %d (Parent: %d)", getpid(), getppid()); // Child can do setup before exec() chdir("/tmp"); // Change directory close(STDERR_FILENO); // Close stderr // Replace this process with 'ls' char *args[] = {"ls", "-la", NULL}; execvp("ls", args); // exec() only returns on error perror("exec failed"); exit(1); } // PARENT PROCESS printf("Parent created child %d", pid); // Wait for child to finish int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { printf("Child exited with status %d", WEXITSTATUS(status)); } return 0;} /* Output:Parent PID: 1000Parent created child 1001Child PID: 1001 (Parent: 1000)total 48drwxrwxrwt 12 root root 4096 Jan 15 10:00 ....Child exited with status 0*/What fork() Duplicates:
- •Address space (via COW)
- •Open file descriptors
- •Environment variables
- •Current working directory
- •Signal handlers (reset on exec)
- •User/group IDs
- •Process group and session
- •Resource limits
- •Nice value
- •Controlling terminal
- •Process ID (new PID)
- •Parent PID (set to forking process)
- •Pending signals (cleared)
- •File locks (not inherited)
- •CPU time counters (reset)
- •Pending alarms (cleared)
- •Process-specific timers
- •Asynchronous I/O contexts
The fork() + exec() pattern is powerful but can be heavy. posix_spawn() combines both into a single call, reducing overhead for simple cases. Some systems also offer vfork() which avoids copying the address space entirely—the child must immediately call exec() or _exit().
The Ready State: Waiting for CPU
After creation, a process enters the Ready state. In this state, the process is fully prepared to execute—it has all necessary resources—but is waiting for the CPU scheduler to assign it a processor.
Characteristics of the Ready State:
- •All resources allocated — Memory, file descriptors, and other resources are available
- •CPU not assigned — The process is queued, waiting for its turn
- •Could run immediately — Nothing is blocking execution except CPU availability
- •Managed in ready queue — The scheduler maintains a data structure of ready processes
- •Priority-ordered — Processes may be ordered by priority within the queue
The Ready Queue:
The operating system maintains a ready queue—a collection of all processes waiting to run. This isn't necessarily a simple FIFO queue; modern schedulers use:
- Priority queues (higher priority runs first)
- Multiple queues (background vs foreground)
- Red-black trees (Linux CFS scheduler)
- Run queues per CPU (for SMP systems)
12345678910111213141516171819202122232425262728
# View processes in running/runnable state (R)$ ps aux | awk '$8 ~ /R/ {print $0}'USER PID %CPU %MEM VSZ RSS TTY STAT COMMANDalice 1234 95.0 0.1 10000 5000 pts/0 R+ ./compute_intensivealice 1235 0.1 0.0 5000 1000 pts/0 R+ ps aux # The R state includes both:# - Actively running on a CPU# - In the ready queue waiting for CPU # View run queue length (number of runnable processes)$ cat /proc/loadavg0.75 0.60 0.55 2/150 12345# ^# running/total processes # More detailed view$ vmstat 1 3procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 0 500000 50000 200000 0 0 0 0 500 800 10 5 85 0 0# ^# r = runnable (ready queue length) # Real-time view of scheduler activity$ sudo perf sched record -- sleep 5$ sudo perf sched latency# Shows scheduling latency for each processWhen the run queue is consistently longer than the number of CPUs, the system is CPU-saturated. Processes spend significant time waiting to run, causing high latency. A load average greater than the CPU count indicates potential CPU saturation.
The Running State: Active Execution
When the scheduler selects a process from the ready queue and assigns it to a CPU, the process enters the Running state. This is when instructions are actually being executed.
Transition to Running (Dispatch):
The act of switching from one process to another is called a context switch. The dispatcher must:
- •Save current process state — Store CPU registers, program counter, stack pointer to the outgoing process's PCB
- •Switch page tables — Change the MMU mappings to the incoming process's address space
- •Restore new process state — Load CPU registers from the incoming process's PCB
- •Jump to new location — Set program counter to where the new process should resume
- •Flush caches (if needed) — On some architectures, clear CPU caches
What a Running Process Can Do:
While running, a process can:
- Execute user-mode instructions (computations, function calls)
- Make system calls (transition to kernel mode temporarily)
- Access memory within its address space
- Be interrupted by hardware interrupts or signals
- Voluntarily yield the CPU
Leaving the Running State:
A process leaves the running state for several reasons:
| Trigger | Next State | Example |
|---|---|---|
| Timer interrupt (quantum expired) | Ready | Process preempted after 4ms time slice |
| Higher priority process becomes ready | Ready | Real-time process wakes up |
| I/O request or blocking call | Waiting | read() on slow disk, sleep() |
| SIGSTOP signal received | Stopped | Debugger stops process |
| exit() called or fatal signal | Terminated | Process finishes or crashes |
| Voluntary yield | Ready | sched_yield() called |
1234567891011121314151617181920212223242526272829
// Simplified context switch pseudocode (kernel code) struct context { uint64_t rsp; // Stack pointer uint64_t rbp; // Frame pointer uint64_t rip; // Instruction pointer (return address) uint64_t rbx, r12, r13, r14, r15; // Callee-saved registers}; void context_switch(struct task_struct *prev, struct task_struct *next) { // 1. Save current CPU state to prev's context save_context(&prev->context); // 2. Switch page tables (address space) load_cr3(next->mm->pgd); // x86: Load new page directory // 3. Switch kernel stack set_kernel_stack(next->kernel_stack); // 4. Restore next's CPU state restore_context(&next->context); // 5. Return "into" the new process // The restored context contains the return address // so we effectively return to where 'next' was saved} // Context switch overhead: typically 1-10 microseconds// Includes TLB flush, cache effects, pipeline flushContext switches are expensive—1-10 microseconds of direct cost, plus indirect costs from cache/TLB pollution. Excessive context switching hurts performance. The scheduler balances responsiveness (frequent switches) against throughput (fewer switches).
The Waiting State: Blocked on Events
A process enters the Waiting (or Blocked) state when it cannot proceed until some event occurs. Unlike the ready state (where the process could run if given CPU), a waiting process genuinely cannot make progress.
Common Reasons for Waiting:
| Blocking Event | Example System Call | What Process Waits For |
|---|---|---|
| Disk I/O | read(), write() | Data transfer to/from storage |
| Network I/O | recv(), accept() | Network data or connection |
| User input | read() on stdin | Keyboard or mouse input |
| Sleep/Timer | sleep(), nanosleep() | Specified time to elapse |
| Lock acquisition | pthread_mutex_lock() | Lock to become available |
| Wait for child | wait(), waitpid() | Child process to terminate |
| Semaphore | sem_wait() | Semaphore count to increase |
| Message queue | msgrcv() | Message to arrive |
| Memory | mmap(), brk() | Memory to be available or paged in |
Interruptible vs Uninterruptible Sleep:
Linux distinguishes between two types of waiting:
-
Interruptible (S): Process can be woken by signals. This is the common case—waiting for user input, network data, etc. Signal handlers can run.
-
Uninterruptible (D): Process cannot be interrupted, even by SIGKILL. Used during critical I/O operations where interruption would cause corruption. Process must wait for I/O to complete.
The 'D' state is notorious when things go wrong—if an NFS server is unreachable, processes waiting on NFS mounts enter uninterruptible sleep and cannot be killed.
123456789101112131415161718192021222324252627282930
# View sleeping processes$ ps aux | awk '$8 ~ /S/ {print}'USER PID %CPU %MEM VSZ RSS TTY STAT COMMANDalice 1234 0.0 0.1 10000 5000 pts/0 S+ vim file.txtalice 1235 0.0 0.0 5000 1000 ? S sleep 60 # S = interruptible sleep# S+ = foreground process in interruptible sleep # View uninterruptible (D state) processes$ ps aux | awk '$8 ~ /D/ {print}'USER PID %CPU %MEM VSZ RSS TTY STAT COMMANDroot 5678 0.0 0.0 8000 2000 ? D mount.nfs server:/share # See what a blocked process is waiting for (wait channel)$ cat /proc/1234/wchanpoll_schedule_timeout # Waiting in poll() $ ps -eo pid,stat,wchan,comm | head -10 PID STAT WCHAN COMMAND 1 Ss ep_poll systemd 2 S kthreadd kthreadd 500 Sl futex_wait sshd 1234 S poll_schedu vim # View blocked processes and their wait reasons$ sudo cat /proc/1234/stack[<0>] __x64_sys_poll+0x123/0x456[<0>] do_syscall_64+0x78/0x90[<0>] entry_SYSCALL_64_after_hwframe+0x44/0xa9High I/O wait (shown in top as '%wa') means many processes are blocked waiting for I/O. While the CPU might appear idle, the system can still feel sluggish because processes can't make progress. This indicates an I/O bottleneck, not idle capacity.
The Stopped State: Suspended Execution
A process enters the Stopped state when its execution is suspended—not waiting for an event, but deliberately paused. The process can be resumed later from exactly where it stopped.
How Processes Enter Stopped State:
- •SIGSTOP signal — Unconditionally stops the process; cannot be caught or ignored
- •SIGTSTP signal — Terminal stop (Ctrl+Z); can be caught but usually stops the process
- •SIGTTIN signal — Background process tried to read from terminal
- •SIGTTOU signal — Background process tried to write to terminal (with stty tostop)
- •Debugger attachment — Debuggers stop processes to examine state (uses SIGSTOP)
Job Control and Stopped Processes:
Shells use the stopped state for job control, allowing users to suspend foreground tasks:
$ long_running_task
^Z # Send SIGTSTP (Ctrl+Z)
[1]+ Stopped long_running_task
$ bg # Resume in background (send SIGCONT)
[1]+ long_running_task &
$ fg # Bring back to foreground
long_running_task
This is invaluable for interactive work—pause a task, do something else, resume later.
12345678910111213141516171819202122232425262728293031323334
# Start a process and stop it$ sleep 1000 &[1] 12345$ kill -STOP 12345 # Stop the process $ ps aux | grep 12345USER PID %CPU %MEM VSZ RSS TTY STAT COMMANDalice 12345 0.0 0.0 5000 1000 pts/0 T sleep 1000# ^# T = stopped # Resume the process$ kill -CONT 12345 $ ps aux | grep 12345USER PID %CPU %MEM VSZ RSS TTY STAT COMMANDalice 12345 0.0 0.0 5000 1000 pts/0 S sleep 1000# ^# S = sleeping (resumed) # Interactive job control$ vim largefile.txt# Press Ctrl+Z[1]+ Stopped vim largefile.txt $ jobs[1]+ Stopped vim largefile.txt $ fg %1 # Resume vim in foreground # Debugger stopping$ gdb -p 12345 # Attach to running process (stops it)(gdb) continue # Resume execution(gdb) detach # Detach and let it continueSIGSTOP pauses a process; SIGKILL terminates it. Both cannot be caught or ignored. SIGSTOP is useful when you want to freeze a process temporarily—for debugging, reducing load, or before migrating to another system (with CRIU).
Process Termination: End of Execution
Every process eventually terminates. Understanding the termination process is crucial for proper resource management and error handling.
Ways a Process Can Terminate:
Normal (Voluntary) Termination:
-
Return from main() — The program reaches the end of main() and returns. The return value becomes the exit status.
-
Call to exit() — Any function can call exit(status) to terminate the process. Cleanup handlers are run.
-
Call to _exit() — Immediately terminates without running cleanup handlers. Used after fork() in error cases.
int main() {
// Normal return
return 0; // Exit status 0 = success
}
void some_function() {
if (fatal_error) {
exit(1); // Exit status 1 = failure
}
}
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>#include <signal.h> int main() { pid_t pid = fork(); if (pid == 0) { // Child - terminate in various ways // Normal exit: exit(42); // Segfault: *(int*)0 = 0; // Signal: raise(SIGTERM); exit(42); // Exit with status 42 } // Parent - collect termination status int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { printf("Child exited normally with status %d", WEXITSTATUS(status)); } if (WIFSIGNALED(status)) { printf("Child killed by signal %d", WTERMSIG(status)); if (WCOREDUMP(status)) { printf("Core dump generated"); } } if (WIFSTOPPED(status)) { printf("Child stopped by signal %d", WSTOPSIG(status)); } return 0;}KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.