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 | Key | Does |
|---|---|
P / M | Sort by CPU / by memory |
1 | Show every CPU core separately |
c | Show the full command line |
k | Send a signal to a PID (SIGTERM by default) |
q | Quit |
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).
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.
| Signal | Command | Effect |
|---|---|---|
| SIGTERM (15) | kill 1234 | Please exit. The program can close files and clean up. The default. |
| SIGINT (2) | Ctrl+C | Interrupt the program in the foreground. |
| SIGHUP (1) | kill -HUP 1234 | Many daemons reload their configuration. |
| SIGKILL (9) | kill -9 1234 | Ends 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.