Process State and the Lifecycle

Imagine a single CPU core and a hundred programs. Each one believes it is running continuously. The operating system maintains this illusion by rapidly switching processes. It switches which program actually occupies the processor. To do this safely, it needs precise knowledge. At every instant, it must know exactly what each program is doing. Is it running, waiting for its turn, or stuck waiting for a disk read? This is the problem that process state solves. It is a small, precise vocabulary describing what a process is doing right now. It also includes a set of rules. These rules govern how a process can move between those situations. The idea matters because of state transitions. Every scheduling decision, every I/O wait, every program launch and exit is really a state transition. When you double-click an application, the OS moves a process from nonexistent to new to ready. When the disk finishes reading a file, the OS moves a waiting process back to ready. When your antivirus software hangs, it is likely stuck in the waiting state. It is blocked on some resource. Without a disciplined state model, the scheduler would have no reliable way to decide who runs next. The system could accidentally run a process that is not actually ready. For example, one still waiting on I/O. This could lead to crashes or wasted CPU cycles. Here is the big picture. Every process, from creation until exit, moves through a small, finite set of states. These states are connected by well-defined transitions. This is a finite state machine (FSM). The OS kernel is the entity that drives every transition. It always responds to a specific triggering event. This could be a scheduling decision, an I/O request, an interrupt, or a completion. Understanding this FSM is a prerequisite for understanding the Process Control Block. The Process Control Block records the current state. It is also a prerequisite for Context Switching. Context Switching is the mechanism that physically performs many of these transitions. This article gives you the canonical five-state process model. It includes a state diagram with every transition labeled by its cause. It also includes a worked trace of several processes competing for one CPU. Finally, it discusses why this simple model is both correct and sufficient. This model is sufficient for building real schedulers.

Key takeaways
  • A process moves through a small set of well-defined states: new, ready, running, waiting (blocked), and terminated.
  • Transitions between states are triggered by specific events: scheduler dispatch, I/O requests, I/O completion, timer interrupts, and exit calls.
  • Only one process per CPU core can be in the running state at any instant; many can be ready or waiting simultaneously.
  • The state model is a strict finite state machine, which is what makes scheduling decisions safe and predictable.
  • Real operating systems refine this five-state model with extra states (e.g., suspended-ready, suspended-blocked, zombie) for memory management and cleanup.
  • The current state of every process is recorded in its Process Control Block, the data structure the OS consults on every scheduling decision and Context Switch.
The core insight

A process's behavior at any instant is fully captured by a single state label, and the OS enforces that state changes only through specific, well-defined transitions — never arbitrarily.

The idea

A process (see The Process Abstraction) is a program in execution, with its own memory image, registers, and execution context. At any given moment, a process is doing exactly one of a small number of things, and the operating system labels this with a process state. The canonical model, used in essentially every textbook and every real kernel in some form, has five states:

  • New: the process is being created — its address space is being set up, but it has not yet been admitted to the pool of processes competing for the CPU.
  • Ready: the process is loaded into main memory, has everything it needs to run, and is simply waiting for the scheduler to assign it a CPU.
  • Running: the process's instructions are actively being executed by a CPU core. On a single-core machine, at most one process is running at any instant; on an nn-core machine, at most nn processes are.
  • Waiting (also called blocked): the process cannot proceed until some event occurs — most commonly the completion of an I/O operation, the availability of a lock, or a signal from another process.
  • Terminated (also called exit or zombie, depending on cleanup stage): the process has finished execution, but the OS may still be holding onto its exit status and resources until a parent process collects them.

The crucial structural fact is that transitions between these states are not arbitrary: they form a directed graph where each edge is labeled by exactly one triggering event. A running process cannot jump straight to ready — it must first be preempted by the scheduler. A waiting process cannot jump straight to running — it must first pass through ready and be dispatched. This restriction is what makes the model tractable: the scheduler only ever needs to reason about processes currently in the ready state, and the I/O subsystem only ever needs to reason about processes currently in the waiting state. Each subsystem owns exactly one state's worth of processes at a time.

How it works

The kernel maintains, for each process, a Process Control Block (PCB) — a data structure holding the process's state label, saved registers, program counter, memory pointers, and scheduling metadata (this structure is developed in full elsewhere, but its role as "the box that stores the state" is essential here). Processes in the same state are typically threaded together on a queue: a single ready queue, and typically one or more wait queues (often one per device or resource type). The five states and their governing transitions are: 1. new → ready ("admit"): the OS finishes creating the process (allocating a PCB, setting up its address space) and places it on the ready queue. 2. ready → running ("dispatch"): the scheduler selects this process (by whatever scheduling policy — FCFS, round robin, priority, etc.) and the dispatcher performs a context switch onto it. 3. running → ready ("preempt" or "timeout"): the process is still runnable but the OS takes the CPU away from it — typically because a timer interrupt fires (time-sharing) or a higher-priority process becomes ready. 4. running → waiting ("block"): the process issues a blocking request (e.g., a `read()` system call to disk, waiting on a semaphore) that cannot complete immediately. 5. waiting → ready ("wake up"): the event the process was waiting for occurs (I/O completes, lock is released), and the OS moves it back onto the ready queue — not directly to running. 6. running → terminated ("exit"): the process calls `exit()`, returns from `main`, or is killed by a signal. Every legal transition is one of these six edges. There is deliberately no edge directly from waiting to running, and no edge directly from ready to waiting — a process must always become the running process before it can decide to block, and must always pass through ready before it can run again after waking up. This is not an arbitrary restriction; it reflects the physical reality that only the currently-executing process can issue the system call that blocks it, and only the scheduler (acting on ready processes) decides who gets a CPU. ```pseudocode // Global data structures maintained by the kernel ReadyQueue : queue of PCB // FIFO or priority queue, policy-dependent WaitQueue[dev] : queue of PCB // one queue per device/resource, simplified here AllProcesses : map from PID to PCB // --- Transition 1: new -> ready --- function CREATE_PROCESS(program): pcb = new PCB() pcb.pid = fresh_pid() pcb.state = NEW allocate_address_space(pcb, program) load_initial_registers(pcb, program.entry_point) pcb.state = READY ReadyQueue.enqueue(pcb) return pcb.pid // --- Transition 2: ready -> running --- function DISPATCH(): if ReadyQueue.is_empty(): run_idle_process() return next = ReadyQueue.dequeue() // policy decides which PCB (FCFS, priority, ...) next.state = RUNNING context_switch_to(next) // save old registers, load next's registers // --- Transition 3: running -> ready (timer preemption) --- function ON_TIMER_INTERRUPT(current): current.state = READY ReadyQueue.enqueue(current) DISPATCH() // --- Transition 4: running -> waiting (blocking syscall) --- function ON_BLOCKING_SYSCALL(current, device): current.state = WAITING WaitQueue[device].enqueue(current) issue_io_request(device, current) DISPATCH() // CPU must go to someone else // --- Transition 5: waiting -> ready (I/O completion interrupt) --- function ON_IO_COMPLETE(device): pcb = WaitQueue[device].dequeue() pcb.state = READY ReadyQueue.enqueue(pcb) // Note: pcb does NOT become RUNNING here directly. // It waits its turn like any other ready process. // --- Transition 6: running -> terminated --- function ON_EXIT(current, exit_code): current.state = TERMINATED current.exit_code = exit_code release_resources(current) // free memory, close file descriptors notify_parent(current) // parent may wait() to collect status DISPATCH() ``` Two invariants are worth naming explicitly, because they are what a correctness argument will lean on:

  • Single-occupancy invariant: on a machine with nn CPU cores, at most nn PCBs have `state = RUNNING` at any instant.
  • Queue-membership invariant: a PCB is on exactly one queue (ready queue, or exactly one wait queue) whenever its state is ready or waiting, and on no queue when it is running, new, or terminated.
Figure
The five-state process lifecycle: nodes are states, edges are triggering events
new creating PCB ready on ready queue running holds a CPU waiting blocked on I/O terminated exit collected admit dispatch timeout / preempt I/O or event wait I/O completion exit()

Complexity & correctness

Time complexity of a single transition. Each transition function above does O(1)O(1) work aside from the queue operation itself: updating `pcb.state` is a single field write, and enqueue/dequeue on a doubly linked list (the standard PCB queue implementation) is O(1)O(1). If the ready queue is implemented as a priority queue (for priority scheduling), dispatch becomes O(log⁡n)O(\log n) for nn ready processes, since it requires a heap extract-min. The context switch itself (saving/restoring registers, switching page tables) has a small constant cost independent of nn, though the resulting cache and TLB misses have real, machine-dependent overhead — this is discussed fully under Context Switching. So the state-machine bookkeeping is cheap; the expense of scheduling lives in the policy (which process to pick), not in the mechanism of moving a PCB between states.

Space complexity. Each process requires one PCB, O(1)O(1) space per process, so O(n)O(n) total for nn live processes. Queue pointers add no asymptotic overhead beyond the PCBs themselves.

Correctness argument. We want to show the model never allows an inconsistent or unsafe execution — specifically, that the single-occupancy invariant and the queue-membership invariant, stated above, are maintained by every transition function. Proof sketch by induction on the sequence of transition calls.

  • Base case: at system start, no processes exist; both invariants hold vacuously.
  • Inductive step: assume both invariants hold before a transition call. We check each of the six functions:
  • `CREATE_PROCESS` sets state to `READY` and enqueues onto `ReadyQueue` exactly once — queue-membership holds for the new PCB; no existing PCB's state or queue is touched, so single-occupancy is untouched.
  • `DISPATCH` dequeues a PCB from `ReadyQueue` (removing it from a queue) and sets its state to `RUNNING`. Since it was dequeued, it is now on no queue — consistent with running processes being queue-free. Because the scheduler is only ever invoked on a core that just became free (via preemption, blocking, or exit — each of which vacates the running label for that core first), single-occupancy is preserved: the core acquires at most one new running process, and it had none before.
  • `ON_TIMER_INTERRUPT` moves the current PCB from running (queue-free) to ready (enqueued once) — the running-slot is vacated before `DISPATCH` is called, so single-occupancy is momentarily "0 running on this core," never "2 running."
  • `ON_BLOCKING_SYSCALL` similarly vacates the running slot before enqueuing onto exactly one wait queue.
  • `ON_IO_COMPLETE` dequeues from exactly one wait queue and enqueues onto the ready queue — a PCB is never on two queues at once because dequeue and enqueue are performed atomically within the interrupt handler (interrupts are typically disabled or the operation is otherwise made atomic during this critical section).
  • `ON_EXIT` vacates the running slot and never re-enqueues the PCB onto `ReadyQueue` or any wait queue — it is marked terminated and removed from active bookkeeping.

Since every transition function preserves both invariants given that they held beforehand, by induction they hold after any finite sequence of transitions. ■\blacksquare

Honest tradeoffs. This five-state model is a simplification. Real kernels add refinements:

  • A suspended or swapped-out dimension (orthogonal to ready/waiting) exists in systems with virtual memory pressure, giving effectively seven or more states (ready-suspended, blocked-suspended) — the OS may swap an entire process's memory out to disk to reclaim RAM, and such a process cannot run even if its I/O completes.
  • POSIX systems distinguish a zombie state (process has exited but its exit code has not yet been read via `wait()`) from full removal, because the OS must retain minimal bookkeeping until the parent collects it — a `terminated` process is not always instantly reclaimed.
  • The model assumes a single wait "reason" abstraction; real kernels track why a process is waiting (which device, which lock, which signal) via separate wait queues, exactly as sketched above, so that a wake-up event only unblocks the relevant processes rather than all waiting ones.

None of these refinements change the core argument: they simply subdivide waiting or ready further while preserving the same transition discipline — a process still can't skip from a blocked variant straight into running.

Worked example

Consider a single-CPU machine running three processes, P1P_1, P2P_2, P3P_3, admitted in that order at time 0, using round-robin scheduling with a time quantum of 4 ms. P1P_1 needs 6 ms of CPU and then does 3 ms of I/O before finishing with 2 more ms of CPU. P2P_2 needs 3 ms of CPU only. P3P_3 needs 8 ms of CPU only. We trace states over time.

Time (ms)EventP1P_1 stateP2P_2 stateP3P_3 stateCPU owner
0all three admittedreadyreadyready—
0dispatch P1P_1runningreadyreadyP1P_1
4quantum expires (P1P_1 used 4 of 6 ms CPU)readyreadyready(switching)
4dispatch P2P_2readyrunningreadyP2P_2
7P2P_2 finishes (only needed 3 ms)readyterminatedready(switching)
7dispatch P3P_3readyterminatedrunningP3P_3
11quantum expires (P3P_3 used 4 of 8 ms)readyterminatedready(switching)
11dispatch P1P_1 (2 ms of CPU burst remain, then blocks)runningterminatedreadyP1P_1
13P1P_1 issues blocking I/O requestwaitingterminatedready(switching)
13dispatch P3P_3 (remaining ready process)waitingterminatedrunningP3P_3
16I/O completes for P1P_1readyterminatedrunningP3P_3 (unaffected)
17quantum expires for P3P_3 (4 more ms used, 4 ms remain)readyterminatedready(switching)
17dispatch P1P_1 (final 2 ms burst)runningterminatedreadyP1P_1
19P1P_1 finishesterminatedterminatedready(switching)
19dispatch P3P_3 (final 4 ms)terminatedterminatedrunningP3P_3
23P3P_3 finishesterminatedterminatedterminated—

Notice the two subtle points this trace is designed to surface: 1. At t=16t=16, P1P_1's I/O completes while P3P_3 is running. P1P_1 moves waiting → ready, not waiting → running — it must wait its turn even though its event has already occurred, because P3P_3 currently owns the single CPU and the single-occupancy invariant forbids two running processes. 2. P1P_1 passes through running twice (once before blocking, once after waking) and ready three times, illustrating that a process's full lifecycle is not a simple straight line through the five states — it can loop through ready/running/waiting arbitrarily many times before reaching terminated.

Common pitfalls & traps

PitfallWhy it's wrongFix
Assuming waiting → running is a direct transitionViolates single-occupancy: the CPU might be busy with another process when the I/O completesAlways route wakeups through ready; let the scheduler dispatch normally
Forgetting to remove a PCB from its old queue before changing stateLeaves a process "ghost" on two queues, corrupting scheduler bookkeepingDequeue before mutating state, or make dequeue+state-update one atomic step
Treating "terminated" as instant deallocationParent may not have called `wait()` yet; freeing PCB early loses the exit code (POSIX zombie problem)Keep a minimal PCB (zombie) until parent collects status, then reap
Ignoring that multiple wait queues exist per device/resourceA wakeup on the wrong queue can leave an unrelated process starved foreverTag each waiting PCB with its wait reason/resource and only wake the matching queue
Allowing a race between timer interrupt and blocking syscallTwo competing transition requests arriving "simultaneously" can double-enqueue the running processDisable interrupts (or otherwise use a lock) during the critical section that changes state and queue membership

Pioneers & history

The five-state process model crystallized as computing moved from single-program batch systems to multiprogrammed, time-shared systems, where many programs genuinely coexist in memory and compete for one CPU. Formalizing "state" as an explicit, OS-tracked property was necessary the moment more than one program could be resident at once — a batch system running one job at a time never needed to distinguish ready from running.

Origins · 1 minEdsger W. Dijkstra and how this was discovered
EW
Edsger W. Dijkstra
1930–2002 · Multiprogramming systems and concurrent process theory, THE, 1968

Dijkstra's work on the THE multiprogramming system made explicit the idea that a running system is best understood as a collection of sequential processes, each occupying a well-defined state, synchronized by clear rules — laying conceptual groundwork for state-based process models and semaphore-based synchronization used to implement wait queues.

History
1954
— early batch-processing systems run one program at a time; no notion of "process state" is needed.
1965
— MULTICS and contemporaneous timesharing projects introduce multiprogramming, requiring an explicit ready/running distinction.
1968
— Dijkstra's THE operating system formalizes cooperating sequential processes and synchronization primitives.
1973
— UNIX popularizes the fork/exec/wait/exit process lifecycle model still visible in POSIX systems today.
1980
s–1990s — textbook formalizations (five/seven-state diagrams) standardize the ready/running/waiting/new/terminated vocabulary taught universally today.

Where this matters

Every general-purpose operating system kernel — Linux, Windows, macOS's XNU, the BSDs — implements some variant of this state machine as the backbone of its process (and thread) scheduler. In Linux, `/proc/[pid]/status` literally reports a process's state using letters (`R` running, `S` interruptible sleep, `D` uninterruptible sleep, `Z` zombie, `T` stopped) that map almost directly onto the states described here. Tools like `top`, `ps`, and Windows Task Manager are, at their core, dashboards for this state machine — showing you, in real time, which processes are ready, running, or blocked. Container orchestration systems (Kubernetes, Docker) and process supervisors (systemd) build higher-level lifecycles — "pending," "running," "crashed," "restarting" — that are directly analogous extensions of the same idea to a coarser granularity (whole containers instead of processes). Real-time operating systems used in embedded and automotive systems rely on precise, provable state-transition timing guarantees built on exactly this same finite-state discipline, since a car's braking-control process must never be left starving in ready for too long. Anywhere multiple independent computations share a single processor, this five-state lifecycle — or a close descendant of it — is the invisible scaffolding making concurrency manageable.

Ask the tutor: Why can't the OS just let a woken-up process resume running immediately instead of going back to ready?

Why isn't "ready" the same as "running"? Both mean the process could execute.

The distinction is about possession of the CPU, not merely eligibility. A ready process has everything it needs — memory, valid registers, no pending blocking condition — but is not currently being executed by any core; it is waiting its turn. A running process is the one whose instructions the CPU is actively fetching and executing right now. This distinction matters because with nn ready processes and one core, only one can run at a time; the rest must queue. Conflating the two would make it impossible to reason about scheduling fairness or starvation, since you'd have no way to describe "a process that deserves the CPU but hasn't gotten it yet."

What exactly puts a process into the waiting state?

A process enters waiting when it issues a request that cannot be satisfied immediately by the CPU alone — most commonly a system call like `read()` on a disk file or socket, waiting on a semaphore or mutex held by another process, waiting for a child process via `wait()`, or waiting for a timer. In each case the kernel recognizes the request cannot complete synchronously, updates the PCB's state to waiting, places it on the appropriate wait queue, and immediately invokes the scheduler to give the CPU to someone else — since a waiting process gains nothing from holding the CPU idle.

Does every process go through all five states in order?

No — new and terminated are visited exactly once, but ready, running, and waiting can be visited many times, in any order permitted by the transition graph, before termination. A CPU-bound process that never performs I/O might oscillate purely between ready and running dozens of times (due to preemption) without ever entering waiting. An I/O-heavy process might spend most of its lifetime cycling through ready → running → waiting repeatedly. The five-state diagram describes the possible transitions, not a fixed sequence every process must follow.

How does this relate to the Process Control Block?

The PCB is the data structure that physically stores a process's current state label, along with its saved CPU registers, program counter, memory management information, and scheduling metadata. Whenever a transition function changes a process's state (as in the pseudocode above), it is literally updating a field inside that process's PCB and moving the PCB itself between queues. The state machine described here is the behavior; the PCB is the record the kernel consults and mutates to enact that behavior. A deeper treatment of the PCB's exact fields and layout belongs in its own dedicated discussion.

What is a "zombie" process and how does it fit this model?

A zombie is a process that has finished executing (it has run `exit()` and released its memory and resources) but whose exit status has not yet been retrieved by its parent process via a `wait()`-family system call. It occupies a middle ground between running and fully terminated: the kernel keeps a minimal residual PCB — essentially just the PID and exit code — until the parent collects it, after which the entry is fully reclaimed. If a parent never calls `wait()`, zombies can accumulate and exhaust the kernel's process table, a real and historically common bug class.

Why do some textbooks show seven states instead of five?

Systems with virtual memory sometimes need to swap an entire process's memory out to disk under memory pressure, even if that process is only ready or waiting, not running. This adds a "suspended" dimension orthogonal to the ready/waiting distinction, producing states like ready-suspended and blocked-suspended. These extended models don't change the core transition logic described here — they just refine where in memory a ready or waiting process currently lives, which matters for a scheduler that must also account for memory availability, not just CPU availability.

Can two processes be in the waiting state on the same resource?

Yes, commonly. Many processes can block on the same wait queue — for instance, multiple processes reading from the same socket, or multiple threads blocked on the same mutex. When the resource becomes available, the OS's wakeup policy (e.g., wake one, wake all) determines how many waiting processes move to ready. This is exactly why wait queues are modeled as queues (or sets) rather than single-slot variables — the waiting state can have arbitrarily high occupancy, unlike running, which is capped by the number of CPU cores.

Before this

Where this leads