Skip to main content
RunBook Academy

LinuxVI · ProcessesProcess lifecycle

Process lifecycle — fork, exec, exit, and wait

Intermediate⏱ ~12 minbashpstopstrace

What you'll learn

  • Trace the lifecycle of a process from fork to wait
  • Distinguish fork() from exec() and clone()
  • Explain what happens when a process receives SIGTERM, SIGKILL, or exits normally
  • Diagnose stuck processes using the lifecycle state

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

The process lifecycle is the foundation of every program that runs. Every daemon, every shell pipeline, every container process follows the same pattern: fork, exec, run, exit, wait.

The four phases

sequenceDiagram
  participant P as Parent
  participant K as Kernel
  participant C as Child
  P->>K: fork()
  K-->>P: return child PID (in parent)
  K-->>C: return 0 (in child)
  Note over C: New process is a copy<br/>(or clone with shared state)
  C->>K: execve(new-program)
  K->>K: replace memory<br/>with new program
  Note over C: Same PID, new code
  C->>K: _exit(status)
  K-->>P: SIGCHLD
  P->>K: wait(&status)
  K-->>P: child\'s exit status

1. fork() — create a copy

fork() (or clone() for threads and namespaces) creates a new process that is a near-copy of the parent. The two processes share most state at the moment of fork; subsequent writes diverge.

Read-only / Safestrace fork/exec
$ strace -e trace=fork,clone,execve ls /tmp 2>&1 | head -5
execve(/bin/ls, [ls, /tmp], 0x7ffd...) = 0
clone(...) = 1234
wait4(-1, ...) = 1234
...

Illustrative output

2. execve() — replace the program

The new process is initially a copy of the parent. execve(path, argv, envp) replaces the process’s memory with a new program. The PID stays the same; only the program and memory change.

Read-only / Safe/proc/self/exe
$ cat /proc/self/exe
/usr/bin/cat

Illustrative output

3. Exit — terminate normally

A process exits by calling exit(status) (C library) or _exit(status) (kernel). The kernel:

  1. Closes file descriptors (releasing locks and resources).
  2. Sends SIGCHLD to the parent.
  3. Releases most process state, but keeps the PID, exit status, and resource usage for the parent’s wait().
  4. The process is now a zombie (state Z).

4. Wait — parent reaps the child

The parent calls wait() or waitpid(). The kernel releases the remaining zombie state and returns the exit status to the parent. If the parent never calls wait(), the zombie persists until the parent itself exits (at which point PID 1 reaps the orphan).

What happens on signals

Processes rarely exit cleanly on their own. Most production exits are triggered by a signal.

SignalDefault effectUseful for
SIGTERM (15)Terminate after cleanupGraceful shutdown — services receive this from systemctl stop
SIGINT (2)TerminateCtrl-C in a terminal
SIGKILL (9)Terminate immediatelyUnresponsive processes; SIGTERM timeout
SIGHUP (1)Terminate (or reload for daemons)“Hang up” — many daemons reload config
SIGQUIT (3)Core dumpDebugging
SIGSTOP (19)SuspendJob control
SIGCONT (18)ResumeAfter SIGSTOP
SIGCHLD (17)Notify parentChild exited
Service impact possiblethe escalation systemd already implements
$ systemctl stop myapp.service

Illustrative output

For a process that no unit owns, escalate by hand — but never with a bare sleep between the two signals:

pid=1234
kill -TERM "$pid" || exit 0             # already gone: nothing to do

for _ in $(seq 30); do
    kill -0 "$pid" 2>/dev/null || { echo "exited cleanly"; exit 0; }
    sleep 1
done

# Re-check immediately before escalating, not 30 seconds ago.
kill -0 "$pid" 2>/dev/null && kill -KILL "$pid"

Production shutdown order

A well-designed systemd dependency graph ensures shutdown order reverses start order:

A Requires=B  →  no ordering at all; B is pulled in, but both may
                 start and stop simultaneously
A After=B     →  A starts AFTER B, and A stops BEFORE B
A Before=B    →  A starts BEFORE B, and A stops AFTER B

Two directives, two separate jobs. Requires= answers “must B be running for A to make sense?”. After=/Before= answer “in which order?”. Neither implies the other, and a unit that declares only Requires= gets no ordering at all.

Shutdown is derived, not declared. systemd inverts the start order to produce the stop order, so After=B means A is torn down before B. You never write a separate shutdown dependency.

A web application with Requires=postgresql.service plus After=postgresql.service therefore starts after postgres and is stopped before it — which is exactly what you want, because the application must drain its in-flight transactions before the database goes away. The same logic chains upward: a load balancer with After= on the web application is stopped first, so it drains traffic before the application it fronts disappears.

Knowledge check

Knowledge check · 4 questions

  1. Q1. After a child process exits, what reaps its zombie state?

  2. Q2. After a successful execve(), the process keeps the same PID and the same open file descriptors.

  3. Q3. Which of the following are correct signals-related practices? Select all that apply.

  4. Q4. myapp.service has Requires=postgresql.service and After=postgresql.service. During a host reboot, which is torn down first, and why does it matter?

Passing score: 75%. Answers are checked in this browser.