Claude on Question 14, Oct 3
I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?Process cleanup is one of those things where the "idiomatic" answer depends on how hard a guarantee you need. Here's the ladder, roughly in order of robustness.
1. Baseline: trap + tracked PIDs
Good enough when your script exits normally or takes SIGTERM/SIGINT.
#!/usr/bin/env bash
set -euo pipefail
pids=()
spawn() { "$@" & pids+=("$!"); }
cleanup() {
trap - EXIT INT TERM HUP # prevent re-entry
(( ${#pids[@]} )) && kill "${pids[@]}" 2>/dev/null
wait 2>/dev/null # reap, avoid zombies
}
trap cleanup EXIT INT TERM HUP
spawn sleep 300
spawn ./worker.sh
waitTwo gaps: it won't catch grandchildren, and traps never run on SIGKILL.
2. Kill by process group
Fixes the grandchildren problem. The trick is set -m, which makes each background job its own process group leader, so $! doubles as a PGID:
set -m
./worker.sh &
worker_pg=$!
set +m
cleanup() {
trap - EXIT INT TERM
kill -TERM -"$worker_pg" 2>/dev/null # leading '-' = whole group
sleep 2
kill -KILL -"$worker_pg" 2>/dev/null
}
trap cleanup EXIT INT TERMYou'll see kill -- -$$ recommended, but it's a trap: it only works if your script actually is the group leader. That's true when launched from an interactive shell, false when launched from another script, where you'd signal your parent's entire group instead. Check first if you go that route:
[[ $$ == "$(ps -o pgid= -p $$ | tr -d ' ')" ]] || echo "not a group leader"
3. PDEATHSIG for the SIGKILL case
If the parent gets kill -9, no trap fires. Linux's PR_SET_PDEATHSIG pushes the responsibility onto the kernel:
setpriv --pdeathsig TERM -- ./worker.sh &
Caveats worth knowing: it triggers when the thread that forked exits (not necessarily the whole parent process), it isn't inherited by grandchildren, and it's cleared across setuid execs. Great as defense-in-depth, not a complete solution.
4. cgroups — the actual guarantee
The only approach that reliably covers an arbitrary tree including double-forked daemons:
# Run the whole script in a transient scope systemd-run --user --scope --collect -- ./myscript.sh
Or manage it yourself on cgroup v2:
cg=/sys/fs/cgroup/myjob.$$ mkdir -p "$cg" echo $$ > "$cg/cgroup.procs" # ... spawn whatever, however deeply nested ... echo 1 > "$cg/cgroup.kill" # atomic, nothing escapes
cgroup.kill is the thing you want if correctness matters. No PID reuse races, no traversal, no escapes.
Things to avoid
- •
pkill -P $$— only one level deep, and racy against PID reuse. - •Parsing
psoutput to walk the tree — a process canfork()between your enumeration and yourkill. - •Forgetting
wait— orphaned-but-unreaped children become zombies holding PID table slots.
Practical recommendation
For most scripts, #1 + #2 is the right amount of effort. Add setpriv --pdeathsig for long-running workers where a parent SIGKILL is plausible. Reach for cgroups when you're writing a job runner or CI executor where leaked processes mean leaked resources.
One more useful tool if you're just bounding runtime: timeout --foreground --kill-after=5s 30s ./worker.sh handles the escalation dance for you.
Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
- Scored
- answered
- Received
- 2026-10-03 09:06:10 UTC
- Run
- 20261003T090008Z-b2d35e
- Served by
- Claude Platform on AWS · HTTP 200 · finish stop
- Size
- 3,255 characters · 3,157 tokens out incl. hidden reasoning · 41.1 s
End of response · 94 lines · 3,255 characters · sha256 0798c294e09535e3e173ef58a12c1a3cb46e320a2e64f09cb4142b24353386e8