Loading learning content...
Every process has a lifecycle—a journey from birth to death. Understanding this lifecycle is essential for:
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 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.
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:
This separation provides remarkable flexibility—code can run between fork() and exec() to set up the child's environment.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h> int main() { printf("Parent PID: %d\n", 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)\n", 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\n", pid); // Wait for child to finish int status; waitpid(pid, &status, 0); if (WIFEXITED(status)) { printf("Child exited with status %d\n", 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:
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().
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:
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:
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.
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:
What a Running Process Can Do:
While running, a process can:
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).
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.
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:
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).
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
}
}
12345678910111213141516171819202122232425262728293031323334353637383940414243
#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\n", WEXITSTATUS(status)); } if (WIFSIGNALED(status)) { printf("Child killed by signal %d\n", WTERMSIG(status)); if (WCOREDUMP(status)) { printf("Core dump generated\n"); } } if (WIFSTOPPED(status)) { printf("Child stopped by signal %d\n", WSTOPSIG(status)); } return 0;}After a process terminates but before its parent collects its exit status, the process enters the Zombie state. This is a brief transitional state that should be short-lived.
What is a Zombie?
A zombie is a terminated process that has released almost all resources but still occupies a slot in the process table. The kernel retains:
This information is preserved so the parent can retrieve it via wait().
Why Zombies Are a Problem:
A few zombies are harmless, but zombie accumulation indicates a bug:
Cause: Parent Not Reaping Children
Zombies accumulate when parents don't call wait(). Common causes:
12345678910111213141516171819202122232425262728293031323334
#include <stdio.h>#include <stdlib.h>#include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { // Child exits immediately printf("Child (PID %d) exiting\n", getpid()); exit(0); } // Parent does NOT call wait() - creating a zombie printf("Parent (PID %d) created child %d\n", getpid(), pid); printf("Sleeping for 60 seconds... check for zombie!\n"); // During this sleep, the child is a zombie sleep(60); // If we wanted to properly reap: // int status; // wait(&status); return 0;} // Terminal 1: Run the program// $ ./zombie_demo // Terminal 2: Observe the zombie// $ ps aux | grep Z// USER PID %CPU %MEM VSZ RSS TTY STAT COMMAND// alice 1235 0.0 0.0 0 0 pts/0 Z+ [zombie_demo] <defunct>On Linux, if a parent sets SIGCHLD to SIG_IGN before children terminate, the kernel automatically reaps zombies without requiring wait(). This is useful for daemons that spawn many children and don't care about exit status.
We have traced the complete journey of a process from creation to destruction. Let's consolidate the key insights:
Module Complete:
You have now completed the Process Definition module. You understand:
This foundation prepares you for deeper study of process management: the Process Control Block, process states in detail, state transitions, process queues, and context switching.
Congratulations! You have mastered the fundamental concept of a process. You now have the conceptual foundation to understand process scheduling, inter-process communication, process synchronization, and all higher-level operating system concepts that build upon the process abstraction.