MODELDRIFT.WATCH

Question 14 · Sep 12 · Day 3

Gemini on Question 14, Sep 12

I'm writing a bash script to purge orphaned children after the parent dies. What's the idiomatic way?

The most idiomatic and standard way to clean up orphaned child processes in Bash is to use the trap command to catch the EXIT signal, and then use jobs -p to kill any background processes.

Here are the best approaches, from the standard idiomatic way to more advanced solutions for complex process trees.

1. The Idiomatic Way (Direct Children)

If your script spawns standard background jobs (using &), the cleanest way is to trap EXIT. The EXIT pseudo-signal triggers when the script ends, whether it finishes naturally or is killed (e.g., by Ctrl+C or SIGTERM).

#!/bin/bash

# Define the cleanup function
cleanup() {
    # Get PIDs of all running background jobs (-p gets PIDs, -r gets only running)
    local child_pids=$(jobs -pr)
    if [ -n "$child_pids" ]; then
        echo "Purging orphaned children: $child_pids"
        kill $child_pids 2>/dev/null
    fi
}

# Trap the EXIT signal
trap cleanup EXIT

# --- Example Usage ---
sleep 100 &
sleep 200 &

echo "Parent doing work... Press Ctrl+C to kill parent."
sleep 5

Why this is good:

  • •trap ... EXIT catches both normal script termination and most signals (like SIGINT/SIGTERM).
  • •jobs -pr ensures you only try to kill jobs that belong to the current shell and are still running.
  • •2>/dev/null suppresses errors in case a child process dies naturally milliseconds before the kill command is issued (a common race condition).

2. The Process Group Way (Nested Children)

The method above only kills direct children. If your background process spawned its own children (grandchildren of your script), those grandchildren will survive and become orphans.

To kill the entire process tree under your script, you need to send a signal to the Process Group.

By default, Bash scripts usually share a process group with their parent terminal. To safely kill all children, you should start the script in its own process group using setsid, or enable Job Control (set -m).

Here is the Job Control method:

#!/bin/bash

# Enable monitor mode (Job Control). 
# This forces every background process into its own process group.
set -m 

cleanup() {
    # Send SIGTERM to the process group (indicated by the negative sign) of every job
    for pid in $(jobs -pr); do
        kill -TERM "-$pid" 2>/dev/null
    done
}

trap cleanup EXIT

# --- Example Usage ---
# This wrapper script will spawn its own children. 
# Without `set -m` and `kill -$pid`, its children would survive.
bash -c 'sleep 100 & sleep 200 & wait' &

echo "Parent doing work... Press Ctrl+C."
sleep 5

3. The Linux Kernel Way (Bulletproof, Linux only)

Both methods above fail if the parent script is killed with SIGKILL (kill -9), because SIGKILL cannot be trapped.

If you absolutely must guarantee that a child dies when the parent dies (even on kill -9), you must rely on the Linux kernel using PR_SET_PDEATHSIG. Because pure Bash cannot make direct syscalls, you use the standard utility prlimit or bwrap if available, but a more common workaround in bash is wrapping the child in tail --pid:

#!/bin/bash

# Child process wrapper
# It waits for its parent ($$) to die, then kills the actual workload
run_child() {
    local parent_pid=$1
    # Do the actual work in the background
    sleep 100 &
    local worker_pid=$!
    
    # Wait for the parent to die, then kill the worker
    tail --pid=$parent_pid -f /dev/null
    kill $worker_pid 2>/dev/null
}

# Pass the parent's PID ($$) to the child function
run_child $$ &

echo "Even if you 'kill -9 $$', the child will die."
sleep 60

Summary of Best Practices

  • 1.Use trap '...' EXIT: It is much cleaner than trapping SIGINT, SIGTERM, etc., individually.
  • 2.Use jobs -pr: It prevents your script from accidentally trying to kill system PIDs.
  • 3.Never use kill 0 blindly: You will see tutorials suggesting kill 0 to kill the current process group. If your script wasn't launched with its own process group, kill 0 will kill your terminal session or SSH connection!

sha256 434c4e3ef8d9… · 24 tokens in · 2,702 out · 23.9 s · via Google · stopped on its own

Sep 12, the whole dayQuestion 14, every day

Exact recordShowHideThe model id, the run, the verbatim text with line numbers and its full sha256
Model
google/gemini-3.1-pro-preview
pinned google/gemini-3.1-pro-preview-20260219; alias resolved to google/gemini-3.1-pro-preview-20260219 at 2026-09-12 09:00:20 UTC (matches the pin)
Date
2026-09-12 · run 20260912T090021Z-c0c86e · scorer v1
Scored
answered
sha256
434c4e3ef8d943839f11cdeb6ab2d7ea5c5c4b529f5b862900931258d5f65e52
Permalink
https://modeldrift.watch/r/2026-09-12/R5/google/gemini-3.1-pro-preview/
Scored
answered
Received
2026-09-12 09:07:10 UTC
Run
20260912T090021Z-c0c86e
Served by
Google · HTTP 200 · finish stop
Size
4,101 characters · 2,702 tokens out incl. hidden reasoning · 23.9 s
The most idiomatic and standard way to clean up orphaned child processes in Bash is to use the **`trap`** command to catch the `EXIT` signal, and then use **`jobs -p`** to kill any background processes.
Here are the best approaches, from the standard idiomatic way to more advanced solutions for complex process trees.
### 1. The Idiomatic Way (Direct Children)
If your script spawns standard background jobs (using `&`), the cleanest way is to trap `EXIT`. The `EXIT` pseudo-signal triggers when the script ends, whether it finishes naturally or is killed (e.g., by `Ctrl+C` or `SIGTERM`).
```bash
#!/bin/bash
# Define the cleanup function
cleanup() {
# Get PIDs of all running background jobs (-p gets PIDs, -r gets only running)
local child_pids=$(jobs -pr)
if [ -n "$child_pids" ]; then
echo "Purging orphaned children: $child_pids"
kill $child_pids 2>/dev/null
fi
}
# Trap the EXIT signal
trap cleanup EXIT
# --- Example Usage ---
sleep 100 &
sleep 200 &
echo "Parent doing work... Press Ctrl+C to kill parent."
sleep 5
```
**Why this is good:**
* `trap ... EXIT` catches both normal script termination and most signals (like `SIGINT`/`SIGTERM`).
* `jobs -pr` ensures you only try to kill jobs that belong to the current shell and are still running.
* `2>/dev/null` suppresses errors in case a child process dies naturally milliseconds before the `kill` command is issued (a common race condition).
---
### 2. The Process Group Way (Nested Children)
The method above only kills **direct** children. If your background process spawned its own children (grandchildren of your script), those grandchildren will survive and become orphans.
To kill the entire process tree under your script, you need to send a signal to the **Process Group**.
By default, Bash scripts usually share a process group with their parent terminal. To safely kill all children, you should start the script in its own process group using `setsid`, or enable Job Control (`set -m`).
Here is the Job Control method:
```bash
#!/bin/bash
# Enable monitor mode (Job Control).
# This forces every background process into its own process group.
set -m
cleanup() {
# Send SIGTERM to the process group (indicated by the negative sign) of every job
for pid in $(jobs -pr); do
kill -TERM "-$pid" 2>/dev/null
done
}
trap cleanup EXIT
# --- Example Usage ---
# This wrapper script will spawn its own children.
# Without `set -m` and `kill -$pid`, its children would survive.
bash -c 'sleep 100 & sleep 200 & wait' &
echo "Parent doing work... Press Ctrl+C."
sleep 5
```
---
### 3. The Linux Kernel Way (Bulletproof, Linux only)
Both methods above fail if the parent script is killed with `SIGKILL` (`kill -9`), because `SIGKILL` cannot be trapped.
If you absolutely must guarantee that a child dies when the parent dies (even on `kill -9`), you must rely on the Linux kernel using `PR_SET_PDEATHSIG`. Because pure Bash cannot make direct syscalls, you use the standard utility `prlimit` or `bwrap` if available, but a more common workaround in bash is wrapping the child in `tail --pid`:
```bash
#!/bin/bash
# Child process wrapper
# It waits for its parent ($$) to die, then kills the actual workload
run_child() {
local parent_pid=$1
# Do the actual work in the background
sleep 100 &
local worker_pid=$!
# Wait for the parent to die, then kill the worker
tail --pid=$parent_pid -f /dev/null
kill $worker_pid 2>/dev/null
}
# Pass the parent's PID ($$) to the child function
run_child $$ &
echo "Even if you 'kill -9 $$', the child will die."
sleep 60
```
### Summary of Best Practices
1. **Use `trap '...' EXIT`**: It is much cleaner than trapping `SIGINT`, `SIGTERM`, etc., individually.
2. **Use `jobs -pr`**: It prevents your script from accidentally trying to kill system PIDs.
3. **Never use `kill 0` blindly**: You will see tutorials suggesting `kill 0` to kill the current process group. If your script wasn't launched with its own process group, **`kill 0` will kill your terminal session or SSH connection!**

End of response · 106 lines · 4,101 characters · sha256 434c4e3ef8d943839f11cdeb6ab2d7ea5c5c4b529f5b862900931258d5f65e52