Understanding Processes

Every program you run on Linux — from a simple ls to a web server handling thousands of requests — becomes a process the moment the kernel loads it into memory and starts executing it. Understanding processes is the foundation for everything else in job control: backgrounding tasks, killing runaway programs, and monitoring system load. This lesson explains what a process actually is under the hood, how to inspect running processes, and how to read the information tools like ps and top give you.

Overview: What a Process Actually Is

A process is a running instance of a program. The distinction matters: a program is a static file sitting on disk (like /usr/bin/firefox), while a process is that program loaded into memory, given its own address space, and actively being scheduled to run on the CPU by the kernel. You can run the same program twice and get two completely separate processes, each with its own memory, its own state, and its own identity.

Every process the kernel creates gets a unique PID (Process ID), a positive integer assigned in increasing order (and reused once the number space wraps around, which takes a long time on modern systems). The very first process started at boot is PID 1 — historically init, and on most modern distributions systemd. Every other process on the system is a descendant of PID 1, directly or indirectly.

Processes are created by forking. When a running process calls fork(), the kernel duplicates it: a new process (the child) is created as a near-exact copy of the calling process (the parent), with its own PID but a copy of the parent’s memory, open file descriptors, and environment. The child usually then calls exec(), which replaces its memory image with a new program — this is how your shell starts every command you type: it forks itself, and the child immediately execs into ls, grep, or whatever you asked for. This is why every process (except PID 1) has a PPID (Parent Process ID) — the PID of the process that forked it.

Each process also carries an owning user ID (UID) and group ID (GID), which the kernel uses for permission checks — a process can generally only send signals to, or otherwise interfere with, processes owned by the same user (root is the exception). This is the same ownership model you see with files: three sets of permissions, enforced consistently across the system.

Process States

At any moment, a process is in one of a small number of states tracked by the kernel scheduler:

Code State Meaning
R Running or runnable Actively executing on a CPU, or ready and waiting for its turn
S Interruptible sleep Waiting for an event (input, a timer, a lock) — can be woken by a signal
D Uninterruptible sleep Usually waiting on I/O (disk, network filesystem); cannot be interrupted, not even by kill -9
T Stopped Paused, typically by a job-control signal like SIGSTOP or Ctrl+Z
Z Zombie Finished executing but still has an entry in the process table because its parent hasn’t collected its exit status yet

A zombie deserves a special mention because the name sounds alarming but the reality is mundane: when a process exits, the kernel keeps a small record (its PID and exit status) until the parent calls wait() to read that status. Until then, the dead process shows up in ps as a zombie. It uses no memory or CPU — it’s just an entry waiting to be reaped. A large number of long-lived zombies usually means the parent program has a bug where it never calls wait().

Every process, when it exits, returns an exit status — an integer from 0 to 255. By convention, 0 means success and any non-zero value means some kind of failure, but the kernel itself does not enforce this meaning; it is purely a convention that well-behaved programs follow. The shell stores the most recently finished command’s exit status in the special variable $?.

Syntax

The two essential tools for inspecting processes are ps (a one-shot snapshot) and top (a live, continuously updating view).

ps [options]
top

Common ps option styles (both are equally valid, but mixing them incorrectly causes confusion — see Common Mistakes):

Form Style Shows
ps aux BSD All processes on the system, in a wide, human-friendly format, including %CPU and %MEM
ps -ef UNIX/System V All processes with full command lines, including PPID, in a slightly different column layout
ps -u <user> UNIX Only processes owned by the given user

Key columns you’ll see in ps aux output:

  • USER — the owner of the process
  • PID — the process ID
  • %CPU / %MEM — current CPU and memory usage
  • STAT — the process state code from the table above (may include modifiers like + for foreground process group, s for session leader)
  • START — when the process began
  • COMMAND — the command line that launched it

Examples

Example 1: Listing every process on the system

ps aux

Output:

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1 168400 11232 ?        Ss   09:02   0:03 /sbin/init
root       842  0.0  0.0      0     0 ?        S    09:02   0:00 [kworker/0:1]
www-data  2210  0.3  1.2 412500 98304 ?        Sl   09:05   1:14 nginx: worker process
ava       3391  0.0  0.4  21568  9024 pts/0    Ss   10:11   0:00 -bash
ava       4502  1.1  0.6  34012 12480 pts/0    R+   10:44   0:02 top

Each row is one process. Notice init at PID 1 owned by root, a kernel worker thread in brackets, an nginx worker process, and the interactive bash shell (ava‘s login shell) that in turn is running top in the foreground (the R+ state — running, foreground process group).

Example 2: Finding a specific process

ps -ef | grep sshd

Output:

root       912     1  0 09:02 ?        00:00:00 /usr/sbin/sshd -D
ava       5120  3391  0 11:02 pts/0    00:00:00 grep --color=auto sshd

The first line is the real SSH daemon, forked directly from PID 1 at boot. The second line is grep itself, which briefly shows up in the results because its own command line contains the text “sshd” — a classic quirk of piping ps into grep that catches beginners off guard.

Example 3: Watching a process’s state live

top

Output (a snapshot of the constantly refreshing screen):

top - 11:15:02 up  2:13,  1 user,  load average: 0.08, 0.12, 0.09
Tasks: 118 total,   1 running, 116 sleeping,   0 stopped,   1 zombie
%Cpu(s):  2.3 us,  1.1 sy,  0.0 ni, 96.4 id,  0.1 wa,  0.0 hi,  0.1 si,  0.0 st
MiB Mem :   7854.0 total,   3021.4 free,   1892.5 used,   2940.1 buff/cache

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 2210 www-data  20   0  412500  98304  15200 S   0.3   1.2   1:14.02 nginx
 3391 ava       20   0   21568   9024   7680 S   0.0   0.4   0:00.10 bash

top refreshes on an interval (3 seconds by default) and shows load average, memory, and per-process resource usage all at once. Press q to quit, or k to kill a process by PID directly from within top.

Example 4: Inspecting a single process through /proc

Every running process also has a live directory under /proc/<PID>/ maintained directly by the kernel — this is in fact where ps and top get their data.

cat /proc/912/status | head -n 5

Output:

Name:	sshd
Umask:	0022
State:	S (sleeping)
Tgid:	912
Ngid:	0

This confirms directly, from the kernel’s own bookkeeping, that PID 912 is the sshd process and is currently in interruptible sleep, waiting for an incoming connection.

How It Works, Step by Step

  1. You type top and press Enter. Your interactive bash process (say, PID 3391) calls fork(), creating a near-identical child process with a new PID.
  2. The child immediately calls exec(), replacing its memory image with the top binary. The PID stays the same, but the program running under it is now top, not bash.
  3. Bash, the parent, calls wait() and pauses — this is why your prompt doesn’t come back until top exits. The new process runs in the foreground, meaning it owns the terminal and receives keyboard signals like Ctrl+C directly.
  4. If you had instead run sleep 300 &, bash would skip the wait() step and immediately print its prompt again — the child runs in the background, and bash tracks it as a numbered job you can inspect with jobs.
  5. When top exits, it terminates with some exit status. The kernel keeps that exit status available until bash (the parent) collects it via wait(), at which point the process table entry is freed and $? is updated in your shell.
sleep 300 &
jobs

Output:

[1] 5188
[1]+  Running                 sleep 300 &

Bash reports the job number ([1]) and the PID (5188) it assigned to the backgrounded sleep process, which is now a child of your shell running independently of your terminal input.

Common Mistakes

Mistake 1: Mixing BSD and UNIX-style ps flags

ps aux (no dash, BSD style) and ps -aux (with a dash) look nearly identical but are not the same command. Adding the dash makes ps interpret a, u, and x as separate UNIX-style options, and -u expects a username argument — so ps -aux is parsed as “show processes for user x“, which usually doesn’t exist.

ps -aux

Output:

error: user list argument 'x' cannot be found in /etc/passwd
usage: ps [options]

Corrected — drop the dash to use the BSD-style combined flags as intended:

ps aux

Mistake 2: Reaching for kill -9 by default

kill without options sends SIGTERM (signal 15) — a polite request asking the process to shut down, which well-behaved programs catch to save state and clean up before exiting. kill -9 sends SIGKILL, which the kernel enforces immediately and which the target process cannot catch, block, or ignore. Defaulting straight to -9 skips any cleanup the program would otherwise perform (like flushing a database write or removing a lock file).

# wrong: skips graceful shutdown for a process that could exit cleanly
kill -9 4502

Corrected — try a plain SIGTERM first, and only escalate if the process ignores it:

kill 4502
sleep 2
kill -0 4502 2>/dev/null && kill -9 4502

The last line uses kill -0, which sends no signal at all and just checks whether the PID still exists — if it does, the process ignored SIGTERM and it’s escalated to SIGKILL.

Best Practices

  • Use ps aux | grep <name> or, better, pgrep <name> to find a PID instead of scanning top output by eye.
  • Prefer plain kill (SIGTERM) before kill -9 (SIGKILL) so processes get a chance to clean up.
  • Check $? immediately after a command if you need its exit status — the next command you run will overwrite it.
  • Remember that ps is a snapshot; use top (or htop if installed) when you need to watch resource usage change over time.
  • A single zombie or two is normal and harmless; a growing pile of them signals a bug in the parent process that never calls wait() — investigate the parent, not the zombie.
  • On Debian/Ubuntu, install htop with sudo apt install htop for a friendlier, colorized alternative to top; on Fedora/RHEL, use sudo dnf install htop.

Practice Exercises

  • Run ps aux and identify your own login shell’s PID and its parent (PPID) by cross-referencing with ps -ef. What process is its parent?
  • Start sleep 120 &, then use ps aux or pgrep sleep to find its PID, and confirm its state in /proc/<PID>/status matches what ps reports.
  • Open top, locate the process using the most memory (%MEM), and note its PID, state, and command — then quit with q without killing anything.

Summary

  • A process is a running instance of a program, identified by a unique PID and created via fork() followed by exec().
  • Every process (except PID 1) has a parent (PPID); the kernel tracks ownership (UID/GID) for permission checks.
  • Processes move through states — Running, Sleeping, Uninterruptible sleep, Stopped, Zombie — visible in the STAT column of ps.
  • ps aux and ps -ef give one-time snapshots; top gives a live, refreshing view; /proc/<PID> is the kernel’s own live source of truth.
  • Exit status ($?) is a convention — 0 means success, non-zero means failure — not something the kernel enforces.
  • Prefer kill (SIGTERM) over kill -9 (SIGKILL) so processes can shut down gracefully.