Viewing Processes (ps, top, htop)

Every running program on a Linux system is a process, and the kernel tracks dozens or hundreds of them at any moment — your shell, your text editor, background services like cron and nginx, and the kernel’s own housekeeping threads. Before you can manage processes (stop them, prioritize them, debug a slow server), you need to be able to see them. This lesson covers the three standard tools for that: ps for point-in-time snapshots, top for a live, auto-refreshing view, and htop for a friendlier interactive version of the same idea.

Overview / How it works

A process is a running instance of a program, identified by a unique PID (process ID). Every process except the very first one has a parent, recorded as its PPID (parent PID), which is how Linux processes form a tree. On modern Debian/Ubuntu systems, PID 1 is systemd (historically init); it is the ancestor of every other process, either directly or through a chain of parents. When a process’s parent exits before it does, the process is “re-parented” to PID 1 or to a nearby subreaper so it is never orphaned in the tree.

The kernel exposes live information about every process through a virtual filesystem mounted at /proc. For a process with PID 2044, the kernel maintains a directory /proc/2044/ containing files like status (human-readable state and memory info), stat (compact machine-readable stats used for CPU accounting), cmdline (the exact command and arguments it was started with), and fd/ (its open file descriptors). Nothing is actually stored on disk here — the kernel generates these files on demand when you read them. This is the entire trick behind ps, top, and htop: they are just programs that read /proc and format what they find. ps reads it once and prints a snapshot; top and htop re-read it every couple of seconds, compute the deltas (how much CPU time each process consumed since the last read), and redraw the screen.

Process states

Every process is in one of a small number of states at any instant, shown in the STAT column of ps or the S column of top:

Code Meaning
R Running or runnable (on the CPU or waiting for a CPU slot)
S Interruptible sleep — waiting for an event (e.g. input, a timer); the vast majority of idle processes are here
D Uninterruptible sleep — usually waiting on disk or network I/O; cannot be interrupted by a signal, not even kill -9
T Stopped, typically by a job-control signal (Ctrl+Z) or by SIGSTOP
Z Zombie — the process has finished and exited, but its parent has not yet called wait() to collect its exit status, so the kernel keeps a small record around

A zombie is not “still running” in any real sense — it holds no memory or CPU, just a PID and exit code waiting to be read. A large number of long-lived zombies usually points to a parent process with a bug in its child-reaping logic.

Syntax

The general forms are shown below (these are illustrative patterns, not literal commands to run):

ps [options]
top [options]

ps has an unusual history: it accepts both “UNIX-style” options with a dash (ps -ef) and “BSD-style” options without one (ps aux), and mixing the two styles changes the meaning of letters. Stick to one style per invocation. The most useful options:

Option Meaning
a (BSD style) show processes for all users attached to a terminal
u (BSD style) show the user/owner and detailed CPU/memory columns
x (BSD style) also include processes with no controlling terminal (daemons, services)
-e (UNIX style) show every process on the system
-f (UNIX style) full-format listing, including PPID and the full command line
-o <fmt> custom output columns, e.g. -o pid,ppid,%cpu,cmd
--sort=<key> sort output by a column; prefix with - for descending, e.g. --sort=-%cpu
--forest draw an ASCII tree showing parent/child relationships
-p <pid> show only the given PID(s)

top takes far fewer command-line options since it’s an interactive program; most control happens through keypresses once it’s running (covered below). htop is a third-party (but near-universal) enhancement of top with color, mouse support, and a built-in process tree; on Debian/Ubuntu install it with sudo apt install htop, and on Fedora/RHEL with sudo dnf install htop.

Examples

1. List every process (BSD style)

ps aux

Output:

USER         PID  %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root           1   0.0  0.1 168400 11288 ?        Ss   08:02   0:03 /sbin/init
root         512   0.0  0.0  21532  3924 ?        Ss   08:02   0:00 /usr/sbin/cron -f
www-data    1188   0.2  1.3 412300 52288 ?        Sl   08:03   0:41 /usr/sbin/nginx -g daemon on;
deploy      2044   0.1  0.8 815200 33012 ?        Sl   09:15   0:12 node /srv/app/server.js
deploy      2201   0.0  0.0  21344  3392 pts/0    Ss   10:41   0:00 bash
deploy      2350   0.0  0.0  19100  2440 pts/0    R+   10:52   0:00 ps aux

a shows other users’ processes on a terminal, u adds the detailed columns (%CPU, %MEM, VSZ virtual memory size, RSS resident set size — actual RAM in use), and x includes processes with no terminal at all, like cron and nginx. Note the ps aux command itself shows up in its own output (PID 2350, state R+ for “running in the foreground”) — it captured a snapshot that included itself.

2. Find the top CPU consumers

ps -eo pid,ppid,user,%cpu,%mem,stat,comm --sort=-%cpu | head -n 6

Output:

  PID  PPID USER     %CPU %MEM STAT COMMAND
 2044     1 deploy    18.3  0.8 Sl   node
 1188     1 www-data   4.1  1.3 Sl   nginx
  512     1 root       0.3  0.0 Ss   cron
    1     0 root       0.0  0.1 Ss   init
 2201  2044 deploy     0.0  0.0 Ss   bash

-o lets you pick exactly the columns you care about instead of a fixed layout, and --sort=-%cpu sorts descending by CPU usage. Piping to head -n 6 keeps just the header plus the top 5 rows — useful when a box feels slow and you want the culprit fast.

3. Watch processes live with top

top

Output (one refresh):

top - 10:53:12 up 2 days,  4:31,  1 user,  load average: 0.42, 0.38, 0.31
Tasks: 118 total,   1 running, 116 sleeping,   0 stopped,   1 zombie
%Cpu(s):  8.3 us,  2.1 sy,  0.0 ni, 89.0 id,  0.4 wa,  0.0 hi,  0.2 si,  0.0 st
MiB Mem :   7935.0 total,   1204.6 free,   3120.8 used,   3609.6 buff/cache
MiB Swap:   2048.0 total,   2048.0 free,      0.0 used.   4260.1 avail Mem

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
   2044 deploy    20   0  815200  33012  12288 S  18.3   0.8   4:12.90 node
   1188 www-data  20   0  412300  52288   9800 S   4.1   1.3   1:01.22 nginx
    512 root      20   0   21532   3924   2200 S   0.3   0.0   0:00.44 cron

Unlike ps, top stays open and redraws every 3 seconds by default. The header shows system-wide load average, a task count broken down by state, and CPU/memory summaries; the table below sorts by %CPU by default. Useful keys while it’s running: P sorts by CPU, M sorts by memory, k prompts for a PID to send a signal to, r renices a process’s priority, and q quits. top never exits on its own — you must press q or Ctrl+C.

4. A friendlier alternative: htop

sudo apt install htop
htop

htop shows the same underlying /proc data as top but adds color-coded CPU/memory meter bars per core, a toggleable process tree (F5), mouse clicks on column headers to sort, and the ability to select and signal multiple processes at once with the spacebar and F9. It isn’t installed by default on most distributions, which is why top remains the one guaranteed to exist on any Linux box you SSH into.

How it works step by step

  1. You run ps, top, or htop.
  2. The tool lists the numeric subdirectories of /proc — each one is a currently-running PID.
  3. For each PID, it reads files like /proc/<pid>/stat (CPU ticks, state, PPID), /proc/<pid>/status (memory in kB, UID), and /proc/<pid>/cmdline (the command and arguments).
  4. ps formats this once and exits. top/htop keep the previous read in memory, sleep for the refresh interval, read again, and compute the CPU-time delta divided by the elapsed wall-clock time to get a percentage — which is why the very first screen top shows right after starting is often less accurate than the second.
  5. The parent/child relationships come from chaining each process’s PPID back through the tree until you reach PID 1. ps -ef --forest or pstree draws this visually:
ps -ef --forest | grep -A2 nginx
www-data    1188       1  0 08:03 ?        00:00:41 /usr/sbin/nginx -g daemon on;
www-data    1190    1188  0 08:03 ?        00:00:12  \_ nginx: worker process
www-data    1191    1188  0 08:03 ?        00:00:11  \_ nginx: worker process

This confirms nginx’s master process (PID 1188) was started directly under PID 1 (systemd), and it in turn spawned two worker processes as its children — exactly the tree systemd would show you with systemctl status nginx.

Common Mistakes

Grepping ps output matches the grep command itself

A very common pitfall: searching for a process by name with grep also matches the grep command’s own argument.

ps aux | grep nginx
www-data    1188  0.2  1.3 412300 52288 ?  Sl 08:03 0:41 /usr/sbin/nginx -g daemon on;
deploy      2401  0.0  0.0  19100  2440 pts/0 S+ 11:04 0:00 grep nginx

The second line is grep matching its own command line, not an actual nginx process. Fix it by wrapping one letter of the pattern in a bracket expression so it no longer matches literally, or better, use pgrep which is built for exactly this:

ps aux | grep '[n]ginx'
pgrep -a nginx

Forgetting to quote a variable in the search pattern

If the process name you’re searching for is built from a variable with spaces, an unquoted expansion word-splits and breaks the intended match:

PROC="node server"
ps aux | grep $PROC

Unquoted, $PROC expands to two separate words (grep node server), and grep treats server as a filename to search, not part of the pattern — it will likely fail with “No such file or directory”. Always quote it:

PROC="node server"
ps aux | grep "$PROC"

Assuming %CPU is capped at 100%

top and ps report %CPU per process relative to a single core, not the whole machine. On a 4-core box, a process using all four cores fully shows roughly 400.0 in the %CPU column, not 100.0. Don’t assume a value above 100 is a bug in the tool — check nproc to see how many cores are available before judging whether a number is high.

Best Practices

  • Use ps -ef --forest or pstree -p when you need to understand which process spawned which, not just a flat list.
  • In scripts, prefer pgrep/pkill over parsing ps output with grep — they match by process name or attributes directly and avoid the self-match trap.
  • In top, use the P (CPU) and M (memory) keys to re-sort live instead of restarting the command with different flags.
  • For scripted or logged output, use ps -o with an explicit column list — the default aux/-ef layouts are meant for humans and can vary slightly between distributions.
  • Always double-check the PID and command column before sending a signal with kill; PIDs are reused after a process exits, so a PID you saw five minutes ago may belong to something else now.
  • Install htop on machines you work on interactively often — the tree view and mouse sorting save real time over plain top.

Practice Exercises

  • Start a background command with sleep 300 & in your terminal, then use ps to find its PID and confirm its STAT column shows a sleeping state. Watch it in top and confirm it disappears from the list once 300 seconds pass.
  • Using ps -eo, build a command that prints only PID, user, and resident memory (rss), sorted by memory usage descending, and shows the 5 largest consumers on your machine.
  • Install htop, open it, press F5 to toggle the tree view, and identify which process is the parent of your current shell.

Summary

  • A process is identified by a PID and belongs to a parent (PPID); together they form a tree rooted at PID 1 (systemd).
  • ps, top, and htop all read the same source of truth: the /proc virtual filesystem.
  • ps takes a one-time snapshot; top and htop refresh continuously and compute CPU deltas between reads.
  • Process states (R, S, D, T, Z) tell you what a process is actually doing right now.
  • %CPU is per-core, so values above 100% are normal on multi-core systems.
  • Prefer pgrep/pkill over grepping ps output in scripts to avoid the self-match and quoting pitfalls.