Zsh Process Management — Control, Monitor & Signal Processes #15
Video: Zsh Process Management — Control, Monitor & Signal Processes #15 by Taught by Celeste AI - AI Coding Coach
Zsh Lesson 15: Process Management — ps, kill, jobs, signals
pslists processes.&runs in background;$!is the last bg PID.jobsshows backgrounded jobs in this shell.kill PID(default SIGTERM, 15) orkill -9 PID(SIGKILL).trapcatches 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.