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,sfor 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
- You type
topand press Enter. Your interactive bash process (say, PID 3391) callsfork(), creating a near-identical child process with a new PID. - The child immediately calls
exec(), replacing its memory image with thetopbinary. The PID stays the same, but the program running under it is nowtop, not bash. - Bash, the parent, calls
wait()and pauses — this is why your prompt doesn’t come back untiltopexits. The new process runs in the foreground, meaning it owns the terminal and receives keyboard signals like Ctrl+C directly. - If you had instead run
sleep 300 &, bash would skip thewait()step and immediately print its prompt again — the child runs in the background, and bash tracks it as a numbered job you can inspect withjobs. - When
topexits, it terminates with some exit status. The kernel keeps that exit status available until bash (the parent) collects it viawait(), 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 scanningtopoutput by eye. - Prefer plain
kill(SIGTERM) beforekill -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
psis a snapshot; usetop(orhtopif 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
htopwithsudo apt install htopfor a friendlier, colorized alternative totop; on Fedora/RHEL, usesudo dnf install htop.
Practice Exercises
- Run
ps auxand identify your own login shell’s PID and its parent (PPID) by cross-referencing withps -ef. What process is its parent? - Start
sleep 120 &, then useps auxorpgrep sleepto find its PID, and confirm its state in/proc/<PID>/statusmatches whatpsreports. - Open
top, locate the process using the most memory (%MEM), and note its PID, state, and command — then quit withqwithout killing anything.
Summary
- A process is a running instance of a program, identified by a unique PID and created via
fork()followed byexec(). - 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
STATcolumn ofps. ps auxandps -efgive one-time snapshots;topgives 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) overkill -9(SIGKILL) so processes can shut down gracefully.
