I began by separating two ideas that I had quietly been treating as one:
program
and:
process
A program is executable code and associated data stored somewhere, perhaps in a file on disk.
A process is a running execution context.
That distinction helped, but it left the process sitting rather statically in my head:
program on disk
│
▼
running process
What I did not yet have was the whole lifecycle.
Where did the process come from?
How did it begin executing a different program?
Why does sleep appear to be doing nothing without having finished?
What exactly happens when I press Ctrl-C?
Why is the command for sending a polite termination request called kill?
And after a process exits, why might it briefly remain visible as a zombie?
The life of a Linux process involves several distinct operations:
creation
│
▼
program execution
│
▼
running and waiting
│
▼
signals and state changes
│
▼
termination
│
▼
parent collects result
That is the journey I want to follow.
These examples assume Bash on Linux. The fork-and-exec diagrams are conceptual simplifications, particularly for multithreaded programs and shells that may use implementation optimisations.
Program, process and execution context
A process is more than a program currently using a CPU.
Among other things, it can have:
a process ID
a parent process ID
a virtual address space
one or more threads
open file descriptors
a current working directory
credentials
an environment
signal dispositions
per-thread signal masks
resource limits
A richer picture is:
PROCESS
│
├── identity
│ ├── PID
│ └── parent PID
│
├── execution
│ ├── program instructions
│ ├── registers
│ └── one or more threads
│
├── memory
│ └── virtual address space
│
├── operating-system resources
│ ├── file descriptors
│ ├── working directory
│ └── credentials
│
└── control state
├── scheduling state
├── signal handling
└── termination status
The executable program is one important part of the process, but it is not the entire process.
Processes have parents
Except for special cases at the root of a process hierarchy, a process is created by another process.
The creator is the parent. The newly created process is its child.
I can inspect those relationships with:
ps -o pid,ppid,stat,cmd
where:
PID process ID
PPID parent process ID
STAT current process state
CMD command
A process tree makes the hierarchy clearer:
ps -eo pid,ppid,pgid,sid,stat,tty,cmd --forest
A simplified fragment might resemble:
PID PPID CMD
1200 1 terminal
└─1250 1200 bash
├─1310 1250 sleep 300
└─1311 1250 ps ...
Here Bash is the parent of the commands it started:
terminal process
│
▼
Bash
│
├── sleep
└── ps
The relationship matters because the parent can wait for a child, inspect how it ended and receive notification when it changes state.
Creating a child and executing a program
A new process must first be created. It can then be made to execute another program.
Those are separate operations.
The traditional Unix model expresses them using:
fork
and:
exec
Very roughly:
parent process
│
│ fork
▼
parent process + child process
│
│ exec
▼
child now executes
another program
Modern libraries and applications may also use interfaces such as posix_spawn(), and shells are free to optimise particular cases. The fork-and-exec model remains an excellent way to understand what the shell is arranging.
fork() creates a child
When a process calls fork(), the system creates a child with its own process ID.
Conceptually, the child begins as a copy of the parent’s process context:
BEFORE FORK
parent
PID 4100
AFTER FORK
parent child
PID 4100 PID 4101
Both continue from the point at which fork() returned. The return value lets the program distinguish them:
parent receives:
child PID
child receives:
0
So one source program can follow two paths:
pid = fork();
if (pid == 0) {
/* child path */
} else {
/* parent path */
}
The child is not the same process. It has a new PID, a logically separate virtual address space and its own process lifecycle.
Linux normally implements the memory side efficiently using copy-on-write. Parent and child can initially share physical memory pages, with a page copied only when one side modifies it, rather than duplicating every page immediately.
There is also an important multithreading qualification. If a multithreaded process calls fork(), only the thread that called fork() exists in the child. The other threads are not copied. This is one reason the path between fork() and exec() requires particular care in multithreaded programs.
The child inherits file descriptors
One inherited resource is especially important for shells and pipelines.
The child receives copies of the parent’s open file descriptors. Suppose the parent has:
fd 0 ──► terminal input
fd 1 ──► terminal output
fd 2 ──► terminal output
After fork(), the child initially has corresponding descriptors:
PARENT
fd 0 ──► terminal
fd 1 ──► terminal
fd 2 ──► terminal
CHILD
fd 0 ──► terminal
fd 1 ──► terminal
fd 2 ──► terminal
The copied descriptors refer to the same underlying open file descriptions. This allows a shell to:
- create a child;
- rearrange the child’s descriptors;
- execute the requested program.
The new program then inherits the prepared input and output connections.
exec replaces the process image
After creating a child, the child can execute another program using an exec-family function.
This is not:
process A starts process B
It is closer to:
process A replaces the program it is executing
Suppose the child initially contains a copy of Bash’s process image:
PID 4101
program image: bash
It then successfully executes:
/bin/sleep
The result is:
PID 4101
program image: sleep
The PID is still 4101. The child did not create another process merely by performing exec; its process image was replaced.
BEFORE EXEC
process 4101
├── Bash program image
├── memory mappings
├── stack
└── instructions
AFTER EXEC
process 4101
├── sleep program image
├── new memory mappings
├── new stack
└── new instructions
A successful execve() does not return to the old program. The old process image is gone.
Not every process attribute disappears. The process keeps its identity, including its PID. Open file descriptors normally remain open unless marked to close during execution.
This allows the shell to prepare:
stdout ──► output.txt
and then execute a completely different program. The new program begins with file descriptor 1 already pointing to the file.
Other attributes have their own rules. For example, caught signal handlers are reset to their default dispositions during exec, while signals that were ignored normally remain ignored.
The useful model is:
exec replaces the program image
but
the process itself continues
What Bash is conceptually arranging
Suppose I type:
grep 'ERROR' application.log > errors.txt
A simplified lifecycle is:
BASH
│
├── parses command
├── recognises output redirection
├── creates a child process
│
▼
CHILD
│
├── opens errors.txt
├── connects fd 1 to errors.txt
└── executes grep
│
▼
GREP PROCESS
│
├── reads application.log
├── writes matches to fd 1
└── exits
│
▼
BASH
└── waits and collects status
The exact implementation may be more sophisticated, but this model explains why:
grepdoes not need to understand Bash’s>syntax;- the program inherits streams prepared by the shell;
- the child gets a distinct PID;
- executing
grepdoes not add another PID after the child already exists; - Bash can later retrieve the child’s completion status.
Running, runnable and waiting
A process can exist without currently using a CPU.
Try:
sleep 300 &
pid=$!
ps -o pid,ppid,stat,cmd -p "$pid"
The state will commonly include:
S
for interruptible sleep.
The process has not terminated. It simply does not need to consume CPU continuously while waiting for its timer.
Process states are snapshots
Tools such as ps and /proc/<pid>/status expose a process’s current state. The value is only a snapshot and may change again before I finish reading it.
Common state letters include:
| State | Broad meaning |
|---|---|
R |
Running or runnable |
S |
Interruptible sleep, waiting for an event |
D |
Uninterruptible sleep, often associated with kernel I/O waits |
T |
Stopped |
Z |
Zombie, terminated but not yet reaped |
These are not permanent identities. A process can move repeatedly between states:
runnable
│
▼
executing
│
▼
waiting
│
▼
runnable again
For example, a process might become runnable, receive CPU time, request data from a file descriptor, sleep while no data is available and become runnable again when data arrives.
Seeing state S does not mean the process is broken. Waiting is normal.
Linux schedules runnable threads
The R state can mean that a thread is currently executing or ready to execute when scheduled.
A machine can have more runnable threads than available CPU cores:
RUN QUEUE
thread A
thread B
thread C
thread D
│
▼
available CPU time
The kernel scheduler chooses among runnable threads. In a single-threaded process, that is effectively the process’s only thread.
A process can therefore be alive, ready and making progress over time without continuously owning a CPU.
Sleeping is efficient
Suppose a server waits for a network request.
A poor design would repeatedly ask:
anything yet?
anything yet?
anything yet?
while consuming CPU.
Instead, a thread can wait for an event:
server calls read
│
▼
no data available
│
▼
thread sleeps
│
▼
network data arrives
│
▼
thread becomes runnable
Sleeping means the execution context is waiting for something before it can continue.
The distinction between:
not running
and:
finished
is essential.
Signals and job control
Signals introduce asynchronous events into a process lifecycle.
A signal can report that:
the user requested interruption
a timer expired
a child changed state
a terminal disconnected
an invalid memory access occurred
a pipe lost all readers
another process requested termination
The event does not have to arrive at a point chosen by the program’s main logic, which is why signal handling requires care.
How signal delivery works
The command:
kill "$pid"
sounds decisive. By default, however, it sends SIGTERM.
More generally, kill is a signal-sending interface:
kill -TERM "$pid"
kill -INT "$pid"
kill -STOP "$pid"
kill -CONT "$pid"
A signal has a disposition that determines its broad treatment. A catchable signal can use its default action, be ignored or invoke a handler.
Default actions include:
terminate
terminate and produce a core dump
ignore
stop
continue
Signal dispositions generally belong to the process. Signal masks, which temporarily block delivery of selected signals, belong to individual threads.
signal disposition
generally process-wide
signal mask
per thread
If a blocked signal is generated, it may remain pending until an eligible thread unblocks it:
signal generated
│
▼
is delivery blocked?
│ │
no yes
│ │
▼ ▼
deliver mark pending
│
▼
deliver when eligible
Blocking is different from ignoring. An ignored signal is discarded according to its disposition. A blocked signal is being held back from delivery.
Familiar signals
| Signal | Typical meaning | Default action | Catchable? |
|---|---|---|---|
SIGTERM |
Conventional termination request | Terminate | Yes |
SIGINT |
Interactive interrupt, commonly Ctrl-C |
Terminate | Yes |
SIGHUP |
Terminal disconnect; sometimes used for reload | Terminate | Yes |
SIGTSTP |
Interactive stop request, commonly Ctrl-Z |
Stop | Yes |
SIGCONT |
Continue a stopped process | Continue | Yes |
SIGCHLD |
Child changed state | Ignore notification | Yes |
SIGPIPE |
A pipe write found no readers | Terminate | Yes |
SIGKILL |
Unconditional termination | Terminate | No |
SIGSTOP |
Unconditional stop | Stop | No |
SIGTERM is normally the right first request for a long-running process. Because it can be caught, a well-designed program may stop accepting work, close listeners, flush required state, remove temporary files and terminate children before exiting.
SIGKILL provides no such opportunity. It cannot be caught, blocked or ignored. The kernel reclaims operating-system resources such as memory and file descriptors, but application consistency may depend on cleanup that the program itself would otherwise perform.
A deliberate shutdown policy is therefore:
send SIGTERM
│
▼
allow graceful shutdown
│
▼
verify whether process exited
│
▼
escalate only if required
SIGCHLD is different again. It notifies a parent that a child changed relevant state. The parent can then use a wait operation to retrieve the details.
Send signals deliberately
Signal names communicate intention more clearly than unexplained numbers:
kill -TERM "$pid"
kill -KILL "$pid"
is clearer than:
kill -15 "$pid"
kill -9 "$pid"
Signal numbers are also not equally portable across every architecture.
The kill interface recognises signal 0 too:
if kill -0 "$pid" 2>/dev/null; then
printf 'process exists and is signalable\n'
fi
No signal is delivered. The operation checks whether the caller can address the process under the relevant existence and permission rules.
That result is only a snapshot. Between checking the PID and acting on it, the original process could exit and the PID could eventually be reused. A PID is an identifier with a lifecycle, not a permanent identity token.
Ctrl-C, process groups and jobs
When I press Ctrl-C, the terminal does not inspect the visible command and select one PID. The terminal driver generates SIGINT for the foreground process group.
This matters because one interactive job may contain several processes:
generate |
transform |
display
Bash can place the pipeline’s processes in one process group:
foreground process group
│
├── generate
├── transform
└── display
A terminal-generated SIGINT can then target the whole foreground operation.
Bash job control adds another abstraction:
job
A job may be one command or a pipeline containing several processes. Bash tracks jobs with:
jobs
fg
bg
If I run:
sleep 300
and press Ctrl-Z, the terminal sends a stop signal to the foreground process group. Bash reports a stopped job.
I can resume it in the background:
bg
or return it to the foreground:
fg
foreground running
│
│ Ctrl-Z / SIGTSTP
▼
stopped
│
├── bg / SIGCONT ──► background running
│
└── fg / SIGCONT ──► foreground running
The process was not restarted. Its execution was suspended and later continued.
Foreground and background are relationships with the controlling terminal. The foreground process group receives terminal-generated signals and is normally the group allowed to read from the terminal. A background job that attempts to read may be stopped with SIGTTIN.
Programs and scripts can handle signals
A long-running program can install handlers for selected signals. In C, the modern interface is sigaction().
A simplified pattern is to let a handler set a flag:
static volatile sig_atomic_t stop_requested = 0;
static void
handle_term(int signal_number)
{
stop_requested = 1;
}
The main program can notice the flag later:
while (!stop_requested) {
do_work();
}
shutdown_cleanly();
The handler should perform only async-signal-safe operations. It should not casually run arbitrary application logic while interrupting normal execution.
Bash scripts can use trap for shell-level handling:
#!/usr/bin/env bash
workspace=$(mktemp -d)
cleanup()
{
rm -rf -- "$workspace"
}
trap cleanup EXIT
trap 'exit 0' TERM
trap 'exit 130' INT
perform_work "$workspace"
This is an illustrative lifecycle sketch, not a complete process supervisor. Supervising children safely introduces additional races and status decisions, and a trapped signal can interrupt Bash’s wait.
The important idea is that a script may end through ordinary completion, SIGTERM, SIGINT or another external event. Reaching the final source-code line is not its only exit path.
For most catchable signals, the receiving process participates in what happens. SIGKILL and SIGSTOP are deliberately exceptional: the process cannot override them.
Termination, waiting and zombies
A process can terminate normally by returning from main() or calling an exit function. It supplies an exit status:
return 0;
A non-zero value can communicate another outcome.
In Bash:
some_command
status=$?
printf 'status=%d\n' "$status"
captures the most recent command’s status before another command replaces $?.
A process can also terminate because a signal’s action was termination, including signals such as:
SIGTERM
SIGINT
SIGSEGV
SIGPIPE
SIGKILL
depending on the circumstances and dispositions.
Bash normally exposes signal termination as:
128 + signal number
This is a shell-level encoding of richer wait information, not a unique or complete description. A program can deliberately exit with the same numeric value.
The parent ordinarily waits
A parent can call a wait-family interface such as:
wait()
waitpid()
waitid()
to learn about child state changes and collect termination information.
In Bash:
sleep 2 &
pid=$!
wait "$pid"
status=$?
printf 'child %s finished with status %s\n' \
"$pid" "$status"
Ordinarily, a parent must perform a wait operation to collect a terminated child’s status. Specialised configurations, including particular SIGCHLD handling or SA_NOCLDWAIT, can cause the kernel to discard that status automatically, but explicit waiting is the normal model.
Termination and reaping are separate
These events do not have to happen at the same instant:
child stops executing
and:
parent collects the child's status
When a child terminates, the kernel releases most of its resources but retains enough information for the parent to learn:
which child terminated
whether it exited normally
its exit status
whether a signal terminated it
some resource-usage information
Until the parent performs an appropriate wait, the terminated child can remain as a zombie.
LIVE CHILD
PID
memory
threads
file descriptors
execution state
...
CHILD TERMINATES
memory released
threads gone
file descriptors closed
ZOMBIE RECORD
PID
termination status
minimal accounting information
A zombie is already dead. It is not using CPU or continuing application work. What remains is a small process-table record containing completion information.
That is why sending SIGKILL to a zombie does not solve anything. There is no execution left to interrupt.
The missing operation is:
parent must wait
not:
send a stronger death signal
Zombies and orphans are different
A zombie is a child that has terminated but has not yet been reaped.
An orphan is a process whose original parent terminated while the child was still alive.
ZOMBIE
child terminated
parent has not waited
ORPHAN
parent terminated
child still running
An orphaned process is reparented within the system, commonly to an init process or an appropriate subreaper, which can later collect its termination status.
A process can be orphaned without being a zombie. A zombie is already terminated.
SIGCHLD helps the parent notice relevant state changes:
child terminates
│
▼
kernel records status
│
▼
parent receives SIGCHLD
│
▼
parent calls wait
│
▼
child is reaped
The signal is notification. The wait operation retrieves the status and completes the ordinary lifecycle.
Operational consequences
This stops being an operating-system curiosity once software runs as a service.
A service manager or orchestrator needs to know:
How is the process started?
Which process should be monitored?
Which signal requests shutdown?
How long should graceful termination be allowed?
Does the process propagate shutdown to children?
When is escalation appropriate?
What exit statuses indicate success or failure?
Will every child be reaped?
A program that starts correctly but cannot stop correctly is not operationally complete.
Process trees complicate termination
Suppose a shell script starts a worker, and that worker starts a helper:
supervisor
│
▼
script
│
▼
worker
│
▼
helper
Sending SIGTERM only to the script does not automatically guarantee that every descendant receives it.
The script may need to forward termination, wait for children, escalate when necessary and collect their statuses. Process groups can help treat a related set as one controllable job.
This is why robust supervision is more complex than:
some_program &
The process hierarchy becomes part of the system architecture.
PID 1 and containers
Inside a container, the application or entrypoint may become process 1 in its PID namespace.
That position brings extra responsibilities. PID 1 has special signal behaviour on Linux: signals whose default action would normally terminate a process may be ignored unless PID 1 has installed a handler. It must also ensure that orphaned descendants are reaped appropriately.
A shell wrapper that starts an application, never uses exec, ignores signals and never waits for children can therefore cause surprising behaviour.
A simple wrapper often benefits from replacing itself with the application:
exec /usr/local/bin/application "$@"
WITHOUT EXEC
container PID 1
│
▼
shell wrapper
│
▼
application
WITH EXEC
container PID 1 / wrapper process
│
│ exec
▼
application in the same process
Using exec removes an unnecessary process layer and lets the application receive signals directly as the container’s PID 1.
It does not solve every PID 1 responsibility. The application must still handle PID 1’s signal behaviour and reap children it creates, or the container should use a minimal init process designed for those jobs.
A small lifecycle experiment
Start a child:
sleep 120 &
pid=$!
printf 'child pid=%s\n' "$pid"
Inspect its relationship and current state:
ps -o pid,ppid,pgid,sid,stat,cmd -p "$pid"
Inspect Linux’s process view:
grep -E '^(Name|State|Pid|PPid):' \
"/proc/$pid/status"
Stop it:
kill -STOP "$pid"
Inspect again:
ps -o pid,ppid,stat,cmd -p "$pid"
Continue it:
kill -CONT "$pid"
Request termination:
kill -TERM "$pid"
Collect the result:
wait "$pid"
status=$?
printf 'wait status=%s\n' "$status"
The experiment follows several distinct operations:
Bash creates child
│
▼
child executes sleep
│
▼
sleep waits
│
▼
SIGSTOP changes state
│
▼
SIGCONT resumes execution
│
▼
SIGTERM terminates process
│
▼
Bash waits and collects status
That is a process lifecycle in miniature.
The mental model I’m keeping
My earlier model was:
program
│
▼
process
│
▼
eventually disappears
The new model is:
PARENT PROCESS
│
│ create child
▼
CHILD PROCESS
│
│ execute program
▼
PROCESS IMAGE
│
┌────────────┼────────────┐
│ │ │
▼ ▼ ▼
runnable running waiting
▲ │ │
└────────────┴────────────┘
│
│ signals may:
│ interrupt
│ stop
│ continue
│ terminate
▼
PROCESS TERMINATES
│
▼
completion information
│
▼
parent performs wait
│
▼
reaped
Several distinctions now feel essential:
program
is not the same thing as
process
creating a process
is not the same thing as
executing a program
not using the CPU
is not the same thing as
terminated
sending a signal
is not always the same thing as
killing a process
termination
is not the same event as
reaping
zombie
is not the same thing as
orphan
A process has a beginning, changing execution states and a completion protocol.
It belongs to a hierarchy. It communicates through file descriptors. The scheduler decides when runnable threads execute. Signals let the environment change the process’s path. The parent ordinarily has the final responsibility of collecting how its child ended.
The life of a program is not simply:
start
work
stop
It is a negotiation between:
the program
the parent
the kernel
the terminal
other processes
the operator
That feels like the first genuinely useful process model I have had.
References and further reading
Core lifecycle
Linux fork(2) manual page
Documents child-process creation, copy-on-write behaviour, multithreaded restrictions, return values and inherited state.
Linux execve(2) manual page
Documents replacement of the calling process’s program image and the process attributes preserved or reset.
Linux signal(7) manual page
Provides the main Linux overview of dispositions, masks, pending signals, delivery and standard signals.
Linux wait(2) and waitpid(2) manual page
Documents waiting for child state changes, retrieving termination information and reaping zombies.
GNU Bash Reference Manual: Job Control Basics
Explains Bash jobs, process groups, foreground and background execution, terminal signals and suspension.
Further detail
GNU C Library: Process Creation Concepts
Provides a conceptual overview of creating children, executing programs and waiting for completion.
GNU C Library: Executing a File
Explains the exec family and replacement of the current process image.
Linux proc_pid_status(5) manual page
Documents /proc/<pid>/status, including identity, state and signal masks.
Linux proc_pid_stat(5) manual page
Documents lower-level process status fields and Linux process-state codes.
Linux ps(1) manual page
Documents process selection and display fields, including PID, PPID, groups, sessions and state.
Linux sigaction(2) manual page
Documents the preferred interface for inspecting and changing signal dispositions.
Linux kill(2) manual page
Documents sending signals to processes and process groups, including signal zero.
Linux setpgid(2) and getpgrp(2) manual page
Documents process groups, sessions and the controlling terminal’s foreground process group.
GNU Bash Reference Manual: Job Control Builtins
Documents jobs, fg, bg, wait, disown and Bash’s kill builtin.
GNU Bash Reference Manual: Signals
Describes how Bash responds to and propagates signals.
GNU C Library: Process Completion
Explains waiting, child termination status and process completion.
GNU Bash Reference Manual: Bourne Shell Builtins
Includes references for Bash’s trap, kill and wait builtins.