Operating Systems · Processes and Threads

Process States and the Transitions the Kernel Actually Tracks

Why the kernel keeps a bitmask rather than a state enum, how runnable differs from running, and what the letter printed by ps leaves out.

Run ps on a quiet machine and every process carries a single letter: R, S, D, T, Z. The letter looks like an enumeration, one value out of a small closed set, and it is tempting to treat it as the kernel’s model of what a process is doing. It is a rendering of a bitmask, and the rendering throws away the parts that matter for reasoning about scheduling, signals, and exit.

One Field, Several Meanings

A task descriptor holds a single state field rather than a state enum. In current kernels the field is an integer named __state; before the 5.14 kernel it was a long named state, and much documentation still uses the older name. The values are constants defined in the scheduler header, not members of a C enumeration, and they are laid out as single bits in a mask.

The base values are few. TASK_RUNNING means the task is on a run queue and eligible for a processor, or is currently occupying one. TASK_INTERRUPTIBLE means it is sleeping and may be woken by a signal. TASK_UNINTERRUPTIBLE means it is sleeping and will not respond to signals until the condition it waits on resolves. __TASK_STOPPED covers job control, entered on SIGSTOP, SIGTSTP, SIGTTIN, or SIGTTOU. __TASK_TRACED covers a task halted at a debugger or ptrace event. EXIT_ZOMBIE and EXIT_DEAD are kept in a separate field, exit_state, and cover the interval between thread exit and reaping.

What the bit layout is used for is the wakeup test. When a task blocks it writes a value into __state, and a waker matches that value against its own mask with wake_up_state, which wakes a task only if any bit of the mask is set in the stored value. That is how TASK_KILLABLE gets its behaviour: it is TASK_UNINTERRUPTIBLE with the TASK_WAKEKILL bit added, so an ordinary wakeup ignores the task and only the signal path, which also passes the kill bit in its mask, can rouse it. TASK_IDLE, used by kernel threads that park deliberately, is TASK_UNINTERRUPTIBLE with TASK_NOLOAD, so a parked thread does not count toward the load average. The current value of __state is one of these constants; the mask behaviour belongs to the wakeup predicates, not to a set of simultaneously held states.

TASK_RUNNING being a single value is the design decision with the most consequences. There is no separate “ready” state, so the letters R and S in a process listing do not map cleanly onto “running” and “runnable”: R covers both. A task that has been woken and placed on a run queue but has not yet been given a processor is in exactly the same state as one executing on another core. The kernel does not need to distinguish them, because the scheduler’s own data structures already do: run-queue membership answers “can this task make progress if given a CPU”, and the current-task pointer answers “is it on a CPU right now”. Duplicating that information in the state field would create a second source of truth that could disagree with the run queue after a migration.

This has a visible effect on observation. A process reported as R may be running on any of the machine’s processors or may be waiting for one, and by the time the observation is printed the answer may have changed twice. Tools that sample /proc read the state at one instant and cannot resolve the ambiguity.

The Sleep That Cannot Be Interrupted

The distinction between interruptible and uninterruptible sleep is not cosmetic. When a task sleeps, it registers a wait-queue entry and calls into the scheduler with a state value. The state determines what the wakeup path may do to it. An interruptible sleep is woken by any signal whose disposition is not “ignore” and which is not blocked; the sleeping call then returns an error indicating interruption, conventionally EINTR, and the program must decide whether to retry. An uninterruptible sleep ignores signals entirely and cannot be terminated by SIGKILL while it lasts.

Uninterruptible sleep exists because some waits cannot be abandoned safely. A task partway through a page fault that has already committed to reading a block from a device cannot unwind: the buffer it is filling is referenced by structures other than its own stack, and returning to user space mid-operation would leave those structures describing a transfer that never happened. The same applies to a process holding a filesystem lock during a metadata update. The kernel marks such waits uninterruptible and accepts that the task becomes unresponsive for the duration.

The cost is the D state visible in ps and its effect on the load average, which counts uninterruptible tasks alongside runnable ones because from the scheduler’s perspective both represent demand that cannot currently be satisfied. The counting is done in the scheduler’s periodic accounting, not by a separate observer, and it is done from the task state: a task whose state carries TASK_NOLOAD is left out of the runnable and uninterruptible totals, which is why TASK_IDLE exists. A process stuck in D on a failing network filesystem is unkillable until the mount times out or the transport recovers; SIGKILL is recorded as pending and delivered only after the task returns to a state where signals are checked.

The trade-off is stated in one line: interruptible sleep is responsive but forces every caller to handle partial completion, and uninterruptible sleep is simple for the caller but transfers the cost to the operator. Kernel code chooses per wait site, which is why the same program can be killed instantly during one system call and immovable during another.

How a Task Leaves the Run Queue

Blocking is a two-part operation, and the order of the parts is what prevents lost wakeups. The task first inserts itself into the wait queue of the event it cares about, then rechecks the condition, then sets its state and calls the scheduler. If the check were performed before the insertion, a wakeup arriving in the gap would find no waiter and the task would sleep forever on an event that had already occurred.

The recheck is usually expressed as a loop, so a spurious wakeup costs an iteration rather than a correctness bug. This is why condition variables in user space and wait queues in kernel space share the same shape: the predicate is evaluated under the protection of whatever lock guards the event, and the sleep is entered only when the predicate is false while holding that lock.

Waking reverses the sequence in effect but not in code. The waker marks the task runnable and enqueues it, so a wakeup that arrives between the task’s condition check and its call to the scheduler is not lost: the task is already on a run queue, and the scheduler’s own test of the state value decides whether the blocking call still needs to complete. If the task has a nonzero priority under a preemptive scheduler, the waker may also set a reschedule flag that causes the scheduler to run before returning to user space, which is how a high-priority task preempts a lower-priority one without waiting for a timer tick. What the scheduler does when it acts on that flag is the register and page-table work described in The Anatomy of a Context Switch.

Exit follows a third path. A thread calls the exit system call, releases its address space references, and becomes EXIT_ZOMBIE if any other thread still needs its exit status, or EXIT_DEAD if not. The distinction matters for the reaping rules described in Zombies, Orphans, and the Mechanics of Reaping: a zombie is a task descriptor retained solely so that a parent can read a status word.

Where the Model Is Deliberately Coarse

Several real conditions have no state of their own. A task waiting in a read for input, a task waiting in a write behind a full pipe buffer, and a task in the middle of a page fault that requires disk I/O can all present as S. The kernel’s state field records whether the task can be woken, not why it is asleep. Reason lives in the kernel stack, in the call chain that led to the sleep, which is why stack sampling rather than state polling is the tool that identifies what a blocked process is actually waiting for.

Preemption adds another wrinkle. A task can be marked for rescheduling while it is in the middle of a system call, but it does not leave the processor at that point; it leaves at the next point where the kernel checks the flag, typically on return to user space or at an explicit preemption point inside the kernel. Between the flag being set and the switch occurring, the task is still TASK_RUNNING and still on a processor, and no external observer can see the pending preemption. Real-time kernels reduce this window by making more of the kernel preemptible, at the cost of more lock-protected critical sections and a larger number of places where a task’s assumptions about uninterrupted execution must be re-established.

The transition set is small enough to describe: running to sleeping, sleeping to runnable, runnable to running, running to stopped or traced, and any state to zombie. A stopped or traced task is woken by the job-control or tracing path, which writes TASK_RUNNING and enqueues it, so resumption is an ordinary wakeup rather than a state of its own. Everything else an operator observes is one of those edges plus the granularity of the observation. Reading a process’s state twice and seeing S then R says only that a wakeup occurred in between, and the ordering of the two reads is not a guarantee about which event caused which.

State polling remains useful precisely because the model is coarse: D for a long interval is a strong signal that a device or a remote filesystem is not responding, and a growing count of Z entries indicates a parent that is not reaping. Neither conclusion requires knowing which kernel function the task is parked in, and both are visible without privileges. The finer questions, which lock is held and which wait queue the task sits on, require the kernel stack, and that is a different instrument with a different cost.

References

  1. Linux Kernel Development, 3rd edition — Chapter 3, Process Management — Robert Love; Addison-Wesley
  2. Understanding the Linux Kernel, 3rd edition — Chapter 3, Processes — Bovet and Cesati; O'Reilly
  3. Operating Systems: Three Easy Pieces — The Abstraction: The Process — Arpaci-Dusseau and Arpaci-Dusseau
  4. proc(5) — /proc/[pid]/stat and the state field — Linux man-pages project