Back to Blog

Zsh Process Management — Control, Monitor & Signal Processes #15

Sandy LaneSandy Lane

Video: Zsh Process Management — Control, Monitor & Signal Processes #15 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Zsh Lesson 15: Process Management — ps, kill, jobs, signals

ps lists processes. & runs in background; $! is the last bg PID. jobs shows backgrounded jobs in this shell. kill PID (default SIGTERM, 15) or kill -9 PID (SIGKILL). trap catches signals for cleanup.

The shell is also a process supervisor. Knowing how to start, watch, and kill processes is essential.

Process IDs

echo "Script PID: $$"
echo "Parent PID: $PPID"
  • $$ — current shell's PID.
  • $PPID — parent shell's PID.
  • $! — last backgrounded process's PID.

ps: list processes

ps               # processes in this terminal
ps aux           # all processes, all users (BSD style)
ps -ef           # all processes (System V style)
ps -p 1234       # specific PID
ps -u alice      # specific user

ps syntax differs between BSD and Linux; macOS uses BSD. Common BSD flags:

  • a — all users.
  • u — user-oriented format.
  • x — include processes without terminal.
  • j — job format.
ps aux | head
# USER     PID %CPU %MEM     VSZ    RSS TT  STAT START   TIME COMMAND
# alice    432  0.5  1.2  4571084 102844 ??  S    Mon11AM 0:32.12 /usr/bin/some...

Custom output format

ps -p $$ -o pid,ppid,command
ps -A -o pid,user,%cpu,%mem,command

-o selects columns. Combine with sort and head for top-N:

ps -A -o pid,%cpu,command | sort -k2 -rn | head -10
# top 10 by CPU

pgrep: find by name

pgrep sleep              # PIDs of sleep processes
pgrep -l sleep           # with names
pgrep -a sleep           # full command lines
pgrep -u alice node      # node processes owned by alice

Easier than parsing ps output. Use it.

Background jobs: &

sleep 60 &
echo "Started with PID $!"

& at the end runs the command in the background. The shell prints the job number and PID, returns to the prompt immediately. The process continues until it finishes or is killed.

$! holds the last backgrounded PID — capture it if you need to manage the process later.

jobs: list shell jobs

sleep 30 &
sleep 40 &
sleep 50 &

jobs
# [1]    running  sleep 30
# [2]  - running  sleep 40
# [3]  + running  sleep 50

jobs -l            # with PIDs

jobs shows the current shell's backgrounded jobs. The + is the "current" job, - is the previous.

fg / bg / wait

sleep 60 &
fg                # bring to foreground
# (press Ctrl-Z to suspend, returns to prompt)

bg                # resume in background
fg %1             # bring job 1 to foreground

wait              # wait for ALL background jobs
wait $PID         # wait for specific PID
wait %1           # wait for job 1

Suspending a foreground command with Ctrl-Z stops it (without killing). bg resumes it in the background; fg brings it back to the foreground.

wait blocks until backgrounded jobs finish. Useful in scripts that fan out work.

kill: send signals

kill 1234              # SIGTERM (15) — graceful shutdown
kill -9 1234           # SIGKILL — force; can't be caught
kill -HUP 1234         # SIGHUP (1) — terminal closed / reload config
kill -INT 1234         # SIGINT (2) — like Ctrl-C
kill -USR1 1234        # SIGUSR1 — user-defined
kill -l                # list all signals

Default signal is SIGTERM, which asks the process to exit cleanly. Most processes catch it and shut down nicely.

SIGKILL (-9) cannot be caught — the kernel kills the process. Use as a last resort; processes can't clean up.

By job number:

kill %1               # job 1 (in this shell)
kill %sleep           # job whose command starts with "sleep"

killall: by name

killall node           # kill all processes named "node"
killall -9 chrome      # force-kill all Chrome processes

Warning: killall on Linux kills all processes named X. On Solaris (and the BSD origin), it would kill ALL processes (logging out the user). On macOS, it's the Linux-like meaning. But still — verify with pgrep first.

pgrep -l node          # what would I kill?
killall node           # then kill

Common signals

Signal Number Default action
SIGHUP 1 Hangup (terminal closed)
SIGINT 2 Interrupt (Ctrl-C)
SIGQUIT 3 Quit + core dump
SIGKILL 9 Kill (cannot be caught)
SIGTERM 15 Terminate (default for kill)
SIGSTOP 17/19 Pause (cannot be caught)
SIGCONT 18/19 Resume
SIGUSR1 30/10 User-defined
SIGUSR2 31/12 User-defined

Numbers vary by OS — use names to be portable.

nohup: survive terminal close

nohup ./long_running.sh &

nohup ignores SIGHUP, so closing the terminal doesn't kill the process. Output redirects to nohup.out by default.

nohup ./script.sh > /tmp/script.log 2>&1 &

Capture output explicitly so it doesn't fill nohup.out.

For long-running services, prefer systemd (Linux) or launchd (macOS) over nohup hacks. Lesson 24 covers cron and launchd.

Disowning a job

sleep 1000 &
disown            # remove from shell's job table
exit              # parent shell can exit; sleep keeps running

disown is similar to nohup but for already-started jobs.

trap: handle signals

cleanup() {
  echo "Cleaning up..."
  rm -f /tmp/demo_$$
  echo "Done."
}

trap cleanup EXIT
trap 'echo "Caught SIGINT"' INT
trap 'echo "Got SIGHUP"' HUP

# Create temp file
echo "data" > /tmp/demo_$$

# Long work
sleep 60

trap CMD SIGNAL runs CMD when SIGNAL is received. Common uses:

  • EXIT — runs whenever the script exits (success, failure, signal). Cleanup goes here.
  • INT — Ctrl-C handler.
  • HUP — terminal-closed handler.
  • TERM — graceful shutdown.

EXIT is the most useful — guaranteed cleanup:

trap 'rm -f /tmp/lock_$$' EXIT

Signal a temp file or lock for cleanup; trust it'll be removed even on errors or Ctrl-C.

Multiple traps

trap 'rm -f /tmp/data_$$' EXIT       # cleanup
trap 'echo "interrupted"; exit 130' INT

Multiple trap commands set different handlers for different signals. The EXIT handler runs after the INT handler.

Removing a trap

trap - EXIT       # remove EXIT handler
trap - INT TERM   # remove for multiple signals

trap - resets to default behavior.

A wrapper script with cleanup

#!/usr/bin/env zsh
set -euo pipefail

WORK_DIR=$(mktemp -d)
trap 'rm -rf "$WORK_DIR"' EXIT

echo "Working in $WORK_DIR"

cd "$WORK_DIR"
# do work that creates files, lock, etc.

# On exit (success or failure), the directory is cleaned up

Using mktemp -d for a unique temp dir, paired with EXIT trap for cleanup. Standard pattern.

Process trees

ps -ef | head
pstree              # tree visualization (install with brew on macOS)

pstree shows the parent-child hierarchy. Useful for debugging "where did this process come from?"

CPU / memory monitoring

top              # interactive CPU/memory view
htop             # nicer top (install via brew)
ps aux --sort=-%mem | head     # top-mem (Linux)
ps aux | sort -k4 -rn | head   # macOS BSD: column 4 is %MEM

For one-shot scripts, ps aux + sort is enough. For watching live, top or htop.

Common stumbles

Forgetting &. sleep 60 blocks the prompt. Add & to background.

kill -9 first. Try kill PID first (SIGTERM) — gives the process a chance to clean up. -9 if it doesn't respond.

Killing the wrong PID. With many similar processes, double-check with ps -p PID. Or use pgrep for name-based filtering.

Job numbers vs PIDs. kill %1 is the job number; kill 12345 is the PID. Different scopes — job numbers are per-shell.

trap with single quotes. trap 'echo $$' EXIT$$ is captured at expansion time (when trap fires), not when set. To freeze, use double quotes: trap "echo $$" EXIT.

set -e and trap order. With set -e, an error before the trap is set means cleanup doesn't run. Set traps first, before any failable code.

Background jobs in scripts and MONITOR. Job control is off by default in scripts. setopt MONITOR to enable, or just don't rely on fg/bg in scripts.

nohup not actually surviving. If you Ctrl-C from the terminal that started it, that's a SIGINT not a SIGHUP — nohup doesn't help. Use & + close the terminal.

What's next

Lesson 16: pipes and redirection. |, >, <, 2>&1, /dev/null.

Recap

$$ current PID, $! last backgrounded PID, $PPID parent. ps aux lists processes; pgrep name for name search. cmd & runs in background, jobs lists, fg %1 foregrounds, wait blocks. kill PID for SIGTERM (15); kill -9 for SIGKILL (force). trap CMD EXIT for cleanup, runs on any exit. nohup cmd & survives terminal close. Use mktemp -d + trap for guaranteed temp-dir cleanup.

Next lesson: pipes and redirection.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.