Processes and resources: ps, top, htop and kill

Find what eats CPU or memory, stop a stuck program politely, and keep long jobs out of nohup.

Part 9 of 12 in Linux basics for self-hosters. Last updated

In short: top shows live CPU and memory use (press M to sort by memory, P by CPU, q to quit); ps aux --sort=-%mem | head lists the biggest memory users once. Stop a program with kill PID, which sends SIGTERM and lets it clean up; use kill -9 only when that fails. Anything that should keep running belongs in a systemd service, not in a terminal with nohup or &.

top: the live view

top refreshes a list of processes every few seconds, busiest first. The flags below are for GNU procps top, the version on every mainstream distribution; guides written for macOS or BSD use different flags. Reference: top(1).

top                   # live view, refresh every 3 seconds
top -d 1              # refresh every second
top -u alice          # only alice's processes
top -p 1234           # only PID 1234 (comma-separate several)
top -o %MEM           # sort by memory from the start
top -b -n 1 | head -20   # one plain-text snapshot, for scripts or pasting
Keys inside top
KeyDoes
P / MSort by CPU / by memory
1Show every CPU core separately
cShow the full command line
kSend a signal to a PID (SIGTERM by default)
qQuit

The columns that matter most: %CPU (100 is one full core, so values above 100 are normal on multi-core machines), RES (memory really in use), S (state: R running, S sleeping, D waiting for disk, Z zombie). For something friendlier, install htop or btop.

ps: a snapshot you can filter

ps aux --sort=-%mem | head                 # biggest memory users
ps aux --sort=-%cpu | head                 # biggest CPU users
ps -eo pid,user,%cpu,%mem,etime,comm --sort=-%cpu | head   # pick your own columns
ps -u alice                                # processes of one user
ps -ef --forest                            # who started whom, as a tree
pgrep -a nginx                             # PIDs and command lines by name
pgrep -u alice -a podman                   # by name and user

ps aux (BSD style, no dash) and ps -ef (standard style) both list everything; procps accepts both. etime is how long a process has been running. pgrep saves you the ps aux | grep dance, and it never lists itself. The option list is in ps(1).

Every process has a parent; kill sends a signal to one PID.

A small tree to practise on: a shell that starts two background jobs. The job is a copy of sleep named demo-job, so pgrep and pkill match nothing else on the machine:

$ cp /usr/bin/sleep ./demo-job
$ bash -c './demo-job 300 & ./demo-job 300 & wait' >/dev/null 2>&1 &
$ ps -o pid,ppid,stat,etime,cmd --forest -p $! --ppid $!
    PID    PPID STAT     ELAPSED CMD
3109662 3109661 S          00:00 bash -c ./demo-job 300 & ./demo-job 300 & wait
3109664 3109662 S          00:00  \_ ./demo-job 300
3109665 3109662 S          00:00  \_ ./demo-job 300
$ pgrep -a demo-job
3109664 ./demo-job 300
3109665 ./demo-job 300
$ pkill demo-job; sleep 0.3; pgrep -a demo-job || echo 'no demo-job left'
no demo-job left

Real output on openSUSE Tumbleweed, procps, 2026-09-28; user and host names and addresses redacted.

Why not pkill -f 'sleep 300'? -f matches the whole command line. In a script or a bash -c '...' line, the shell running it has that same text in its own command line, so pkill can kill the shell that called it. Match on the process name, or check with pgrep -af first.

Stopping processes: kill, pkill, systemctl

kill does not kill; it sends a signal. Which one you send decides whether the program gets to save its work.

The signals you will use
SignalCommandEffect
SIGTERM (15)kill 1234Please exit. The program can close files and clean up. The default.
SIGINT (2)Ctrl+CInterrupt the program in the foreground.
SIGHUP (1)kill -HUP 1234Many daemons reload their configuration.
SIGKILL (9)kill -9 1234Ends it immediately, no cleanup. Last resort.
kill 1234                    # ask politely
sleep 5; kill -0 1234 && kill -9 1234   # still there after 5 seconds? then force
pgrep -a -u alice rsync      # check what matches first
pkill -u alice rsync         # then stop them by name and user
sudo systemctl stop caddy    # a service: stop it through systemd, not kill

kill -0 sends no signal; it only answers whether the PID still exists. After a SIGTERM the shell reports the exit status as 128 plus the signal number:

$ sleep 300 & p=$!
$ kill -0 $p && echo "$p is running"
3108520 is running
$ kill $p; sleep 0.2; kill -0 $p; echo "exit=$?"
bash: line 1: kill: (3108520) - No such process
exit=1
$ wait $p; echo "sleep ended with $? (128 + 15 = SIGTERM)"
sleep ended with 143 (128 + 15 = SIGTERM)

Real output on openSUSE Tumbleweed, bash 5.3, 2026-09-28; user and host names and addresses redacted.

A process in state D (waiting for disk or a dead network share) ignores even SIGKILL until the I/O returns; fix the share, not the process. Killing a service's main process makes systemd start it again if the unit has Restart=always, which is why systemctl stop is the right tool there.

Load average and priorities

uptime                         # load average: 0.52, 0.61, 0.70
nproc                          # number of CPU cores to compare with
nice -n 10 tar -caf backup.tar.zst /srv/data   # start with a lower priority
renice -n 10 -p 1234           # lower the priority of a running process
ionice -c 3 -p 1234            # disk access only when nothing else needs it

The three load numbers are 1-, 5- and 15-minute averages of processes that want CPU or wait for disk. Below the core count is fine; far above it means work is queueing. Nice values run from -20 (most favoured) to 19 (least); normal users may only make things nicer. For a service, set Nice= and IOSchedulingClass=idle in its unit.

uptime reads its numbers from /proc/loadavg. On this 8-core laptop a load around 2 means about a quarter busy. The fourth field is running and total processes, the fifth the last PID handed out:

$ nproc; cat /proc/loadavg
8
1.96 2.49 2.18 2/3545 3108541

Real output on openSUSE Tumbleweed, 2026-09-28; user and host names and addresses redacted.

Why services belong in systemd

nohup ./server & keeps a program alive after you log out, but nothing restarts it after a crash or a reboot, its output goes to a stray nohup.out, and nobody remembers it is there. A systemd service fixes all of that: it starts at boot, restarts on failure, logs to the journal and can be stopped by name.

systemd-run --user --unit=reindex ./reindex.sh   # one-off job as a transient user service
systemctl --user status reindex                   # watch it
journalctl --user -u reindex                      # read its output

A real run with a job that prints a line and fails on purpose (exit code 3). systemd records the failure, and the output is in the journal instead of a lost terminal:

$ systemd-run --user --unit=demo-hello --quiet sh -c 'echo hello from a transient service; exit 3'
$ systemctl --user status demo-hello --no-pager | head -8
× demo-hello.service - [systemd-run] /usr/bin/sh -c "echo hello from a transient service; exit 3"
     Loaded: loaded (/run/user/1000/systemd/transient/demo-hello.service; transient)
  Transient: yes
     Active: failed (Result: exit-code) since Mon 2026-09-28 06:35:46 CEST; 996ms ago
   Duration: 26ms
 Invocation: 81e8981caafc4273a4568b85058c3a3d
    Process: 3110022 ExecStart=/usr/bin/sh -c echo hello from a transient service; exit 3 (code=exited, status=3)
   Main PID: 3110022 (code=exited, status=3)

$ journalctl --user -u demo-hello --no-pager -o short-iso | tail -4
2026-09-28T06:35:46+02:00 laptop systemd[1774]: Started [systemd-run] /usr/bin/sh -c "echo hello from a transient service; exit 3".
2026-09-28T06:35:46+02:00 laptop sh[3110022]: hello from a transient service
2026-09-28T06:35:46+02:00 laptop systemd[1774]: demo-hello.service: Main process exited, code=exited, status=3/NOTIMPLEMENTED
2026-09-28T06:35:46+02:00 laptop systemd[1774]: demo-hello.service: Failed with result 'exit-code'.

Real output on openSUSE Tumbleweed, systemd 261, 2026-09-28; user and host names and addresses redacted.

For anything permanent, write a unit file or, for containers, a quadlet. The systemd unit explainer explains every line, and systemctl and journalctl for beginners covers starting, stopping and reading logs.

Common mistakes

Reaching for kill -9 first

SIGKILL gives the program no chance to finish writing. A database killed this way may need a recovery on the next start, and temporary files stay behind. Send SIGTERM, wait a few seconds, and only then use -9.

Copying top flags from a macOS guide

macOS and BSD top take flags such as -o cpu or -s that mean something else, or nothing, in procps top. On Linux, -d is the delay, -o takes a column name such as %CPU, and -b -n 1 prints once. Paste an unfamiliar command into the shell command explainer first.

Killing a service that comes straight back

If the PID changes every time you kill it, systemd is restarting it as designed. Find the unit with systemctl status 1234 (it accepts a PID) and stop or disable the unit.

Frequently asked questions

How do I kill a process in Linux?

Find its PID with pgrep -a name, then run kill PID. That sends SIGTERM, which asks the program to exit cleanly. If it is still there after a few seconds, kill -9 PID (SIGKILL) ends it without cleanup. For a systemd service, use sudo systemctl stop name instead.

What does load average mean in top?

The three numbers are the average number of processes running or waiting for CPU or disk over 1, 5 and 15 minutes. Compare them with the number of CPU cores from nproc: a load of 4 on a 4-core machine means fully busy, a load of 12 means work is queueing.

What is a zombie process?

A process that has exited but whose parent has not yet collected its exit status. It uses no CPU or memory, only a slot in the process table, and shows state Z. kill does nothing to it; it disappears when the parent collects it or exits.

Is htop better than top?

htop shows the same information with colours, mouse support, a tree view (F5) and easy kill and renice keys, so it is friendlier. btop adds graphs for disks and network. top is installed everywhere, which makes it worth knowing for servers.