Bash Scripting 27 ๐ง Process Management โ Managing job control, background tasks, and PIDs
A shell script that starts a background process must be able to find it again, wait for it, and terminate it when necessary. Bash provides three mechanisms for this: job control (&, jobs, fg, bg, kill %n), the process ID variables ($!, $$, $BASHPID), and the wait builtin. Each mechanism serves a different purpose, and confusing them produces scripts that lose track of their children, kill the wrong process, or exit while background work is still running.
This chapter covers the job control builtins that manage foreground and background execution, the PID variables that identify the shell, the current script, and the most recent background job, and the wait builtin that synchronizes with child processes and collects their exit statuses. It also covers the disown builtin that detaches a job from the shell’s job table, which is what prevents a background job from being terminated when the shell exits.
Key point: $! is the PID of the last background process. $$ is the PID of the shell or script, not the subshell. $BASHPID is the PID of the current Bash instance, which differs from $$ inside a subshell. Use $! to track background jobs and $BASHPID when you need the actual process ID inside a subshell.
Why process management has these distinctions
The job control problem. When a command runs in the foreground, the shell waits for it to finish before returning the prompt. When a command runs with &, the shell returns the prompt immediately and the command continues in the background. The jobs builtin lists the background and stopped jobs, fg brings a job to the foreground, and bg resumes a stopped job in the background . The job number (%1, %2) is a shell-level identifier, not a PID. It refers to a process group, which may contain multiple processes if the job is a pipeline .
The PID variable problem. $$ expands to the PID of the shell or script that is executing, not the PID of a subshell created with parentheses . A script that uses $$ inside ( ... ) to identify itself will get the parent shell’s PID, not the subshell’s PID. Bash 4.0 introduced $BASHPID, which is updated inside subshells to the subshell’s actual PID . The difference matters when a script uses a subshell to run work in parallel and needs to identify that work’s process.
The exit-synchronization problem. A script that starts background jobs and then exits does not automatically wait for them. If the script’s job is to run several tasks in parallel, it must call wait to block until they finish. Without wait, the script exits, the shell terminates, and depending on the shell’s configuration, the background jobs may receive SIGHUP and die . The wait builtin with no arguments waits for all background jobs; wait $pid waits for a specific one and returns its exit status .
a. Job control: &, jobs, fg, bg
The & operator starts a command in the background. The shell prints the job number and PID, and returns the prompt.
# Start a background job
sleep 300 &
# Output: [1] 12345
# [1] is the job number, 12345 is the PID
The jobs command lists the shell’s jobs. The -l flag adds the PID, -r shows only running jobs, and -s shows only stopped jobs .
# List jobs with PIDs
jobs -l
# Output:
# [1]- 12345 Running sleep 300 &
# [2]+ 12346 Running sleep 200 &
# The + marks the current job
# The - marks the previous job
The + and - markers identify the current and previous jobs. fg with no argument resumes the current job in the foreground. bg with no argument resumes the current job in the background. The %n syntax refers to a specific job number .
# Bring job 1 to the foreground
fg %1
# Resume job 2 in the background
bg %2
A foreground job can be stopped with Ctrl+Z, which sends SIGTSTP. The job moves to the stopped state and appears in jobs with the status Stopped. It can then be resumed with fg or bg .
b. PID variables: $!, $$, and $BASHPID
Three variables identify processes, and they are not interchangeable.
$! is the PID of the most recent background command. It is set when a command is started with & and is used to track or wait for that specific job .
sleep 300 &
pid=$!
echo "Background PID: $pid"
# Wait for it specifically
wait "$pid"
$$ is the PID of the shell or script. It does not change inside a subshell created with parentheses. A script that runs ( echo "$$" ) prints the parent script’s PID, not the subshell’s .
echo "Script PID: $$"
( echo "Inside subshell: $$" ) # same as the script PID
$BASHPID is the PID of the current Bash instance. In the main shell, it equals $$. Inside a subshell, it is updated to the subshell’s actual PID .
echo "Main: $$ / $BASHPID" # both the same
( echo "Subshell: $$ / $BASHPID" ) # $$ unchanged, $BASHPID different
The practical rule is to use $! for background jobs, $$ for the script’s own PID (for temporary file names, for example), and $BASHPID when the code runs inside a subshell and needs the subshell’s actual PID.
c. wait and collecting exit statuses
The wait builtin blocks until a background job finishes. With no arguments, it waits for all background jobs. With a PID or job specification, it waits for that specific job .
# Start three jobs
sleep 2 &
pid1=$!
sleep 4 &
pid2=$!
sleep 1 &
pid3=$!
# Wait for each and collect status
wait "$pid3"
echo "Job 3 finished with status $?"
wait "$pid2"
echo "Job 2 finished with status $?"
wait "$pid1"
echo "Job 1 finished with status $?"
The exit status of wait is the exit status of the process it waited for. If the process was terminated by a signal, the status is 128 plus the signal number . This is how a script knows whether a background task succeeded.
A common pattern is to start multiple jobs, store their PIDs in an array, and wait for all of them:
pids=()
for i in 1 2 3; do
( sleep "$i"; exit "$i" ) &
pids+=($!)
done
for pid in "${pids[@]}"; do
wait "$pid"
echo "PID $pid exited with $?"
done
The wait command with no arguments waits for all background jobs but does not report their individual statuses. To collect statuses, the script must wait for each PID individually .
d. disown and the SIGHUP problem
When an interactive shell exits, it sends SIGHUP to all its jobs. The default action of SIGHUP is to terminate the process. A script that starts a background job and then exits will cause that job to receive SIGHUP and die, unless the job is detached from the shell’s job table .
The disown builtin removes a job from the shell’s job table. The job continues running, but the shell no longer tracks it and will not send it SIGHUP on exit. The -h option marks the job so that it is not sent SIGHUP, but it remains in the job table .
# Start a background job and detach it
long_running_task &
disown
# The task continues after the shell exits
In a non-interactive script, the shell does not normally send SIGHUP to background jobs on exit, but the behavior depends on the shell and how it was invoked. Using nohup or disown is the reliable way to start a process that must outlive the script.
Complete Example Session
# ============================================
# PART 1: START A BACKGROUND JOB
# ============================================
sleep 300 &
# Output: [1] 12345
# The job number is 1, the PID is 12345.
# ============================================
# PART 2: LIST JOBS
# ============================================
jobs -l
# [1]+ 12345 Running sleep 300 &
# ============================================
# PART 3: THE PID VARIABLES
# ============================================
echo "Script PID ($$): $$"
sleep 10 &
bg_pid=$!
echo "Background PID (\$!): $bg_pid"
( echo "Subshell: \$\$=$$, \$BASHPID=$BASHPID" )
# $$ is the script PID; $BASHPID is the subshell PID
# ============================================
# PART 4: STOP AND RESUME
# ============================================
# Run a foreground command
sleep 100
# Press Ctrl+Z
# [1]+ Stopped sleep 100
# Resume in the background
bg %1
# Bring back to the foreground
fg %1
# ============================================
# PART 5: WAIT FOR A SPECIFIC PID
# ============================================
sleep 2 &
pid=$!
wait "$pid"
status=$?
echo "Exited with $status"
# ============================================
# PART 6: WAIT FOR MULTIPLE JOBS
# ============================================
pids=()
for i in 1 2 3; do
( sleep "$i"; exit "$i" ) &
pids+=($!)
done
for pid in "${pids[@]}"; do
wait "$pid"
echo "PID $pid exited with $?"
done
# ============================================
# PART 7: JOB SPECIFICATIONS
# ============================================
sleep 300 &
sleep 400 &
# Kill job 1 (sends SIGTERM to the process group)
kill %1
# The job is terminated; jobs shows only job 2
# ============================================
# PART 8: DISOWN
# ============================================
long_task &
disown
# The job is removed from the job table.
# It continues running after the shell exits.
# ============================================
# PART 9: CHECK IF A PROCESS IS RUNNING
# ============================================
pid=$!
if kill -0 "$pid" 2>/dev/null; then
echo "Process $pid is running"
else
echo "Process $pid is not running"
fi
# ============================================
# PART 10: COMPLETE PATTERN
# ============================================
#!/bin/bash
set -euo pipefail
pids=()
cleanup() {
for pid in "${pids[@]}"; do
kill "$pid" 2>/dev/null || true
done
}
trap cleanup EXIT
for i in 1 2 3; do
( sleep "$i"; echo "Job $i done" ) &
pids+=($!)
done
for pid in "${pids[@]}"; do
wait "$pid"
done
The ten parts covered starting background jobs, listing jobs, PID variables, stop and resume, waiting for a specific PID, waiting for multiple jobs, job specifications, disown, checking process liveness, and the complete cleanup pattern.
Quick Reference
Job control builtins
| Command | Effect |
|---|---|
command & | Start in background |
jobs | List jobs |
jobs -l | List with PIDs |
jobs -r | Running jobs only |
jobs -s | Stopped jobs only |
fg %n | Bring job to foreground |
bg %n | Resume job in background |
kill %n | Send signal to job’s process group |
disown | Remove job from job table |
PID variables
| Variable | Value | Changes in subshell |
|---|---|---|
$! | Last background PID | N/A |
$$ | Shell/script PID | No |
$BASHPID | Current Bash PID | Yes |
wait
| Command | Effect |
|---|---|
wait | Wait for all background jobs |
wait $pid | Wait for a specific PID, return its status |
wait %n | Wait for a job specification |
Process liveness
| Command | Effect |
|---|---|
kill -0 $pid | Exit 0 if process exists, non-zero if not |
kill $pid | Send SIGTERM (default) |
kill -9 $pid | Send SIGKILL |
Best Practices
โ Do This:
# Store the background PID immediately
sleep 300 &
pid=$! # โ
# Use wait to synchronize with background jobs
wait "$pid" # โ
# Use $BASHPID inside subshells
( echo "$BASHPID" ) # โ
# Disown long-running jobs that must outlive the script
long_task &
disown # โ
โ Don’t Do This:
# Use $$ inside a subshell expecting the subshell PID
( echo "PID: $$" ) # โ (parent PID)
# Exit without waiting for background jobs
job & # โ (job may be killed)
# Kill by job number in a non-interactive script
kill %1 # โ (job control may be off)
# Assume $! is still valid after a second background job
cmd1 &
cmd2 &
echo "$!" # PID of cmd2, not cmd1 # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Background job dies when script exits | Shell sends SIGHUP to jobs | Use disown or nohup |
$! is wrong PID | Another background job started | Store $! immediately after each & |
| Subshell reports parent PID | $$ does not change in subshells | Use $BASHPID |
wait returns immediately | No background jobs exist | Check that the job was started with & |
kill %1 fails in a script | Job control disabled in non-interactive shells | Use PID or process group ID |
| Exit status lost | wait not called | Call wait "$pid" and capture $? |
Real-World Examples
1. Start a background job
sleep 300 &
2. Store the PID
pid=$!
3. List jobs with PIDs
jobs -l
4. Bring a job to the foreground
fg %1
5. Resume in the background
bg %1
6. Wait for a specific PID
wait "$pid"
7. Wait for all jobs
wait
8. Check if a process is alive
kill -0 "$pid" 2>/dev/null
9. Disown a job
disown
10. Subshell PID
( echo "$BASHPID" )
Visual
Job control states
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ JOB STATES โ
โ โ
โ Foreground โโCtrl+Zโโโบ Stopped โ
โ โ โ โ
โ โ โโโ fg %n โโโบ Foreground โ
โ โ โ โ
โ โ โโโ bg %n โโโบ Running (bg) โ
โ โ โ
โ โผ โ
โ Running (foreground) โ
โ โ
โ command & โโโบ Running (background) โ
โ โ โ
โ โโโ fg %n โโโบ Foreground โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
PID variables
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ PID VARIABLES โ
โ โ
โ $$ โ
โ Script PID โ
โ Does not change in subshell โ
โ โ
โ $BASHPID โ
โ Current Bash PID โ
โ Changes in subshell โ
โ โ
โ $! โ
โ Last background PID โ
โ Set by & โ
โ โ
โ Main shell: $$ = $BASHPID โ
โ Subshell: $$ โ $BASHPID โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
wait synchronization
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ WAIT โ
โ โ
โ Script starts job1 &, job2 &, job3 & โ
โ โ โ
โ โผ โ
โ wait $pid1 โโโบ blocks until job1 finishes โ
โ โ โ
โ โผ โ
โ wait $pid2 โโโบ blocks until job2 finishes โ
โ โ โ
โ โผ โ
โ wait $pid3 โโโบ blocks until job3 finishes โ
โ โ โ
โ โผ โ
โ Script continues โ
โ โ
โ Without wait, the script exits and the jobs may die. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
disown and SIGHUP
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DISOWN โ
โ โ
โ Without disown: โ
โ Script exits โโโบ shell sends SIGHUP to jobs โโโบ jobs die โ
โ โ
โ With disown: โ
โ Script exits โโโบ job not in table โโโบ no SIGHUP โ
โ Job continues running โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Background operator | & |
| Job list | jobs / jobs -l |
| Foreground | fg %n |
| Background resume | bg %n |
| Stop a foreground job | Ctrl+Z (SIGTSTP) |
| Last background PID | $! |
| Script PID | $$ |
| Subshell PID | $BASHPID |
| Wait for job | wait $pid or wait %n |
| Detach from shell | disown |
| Liveness check | kill -0 $pid |
Key takeaways:
- Job control is a shell-level abstraction. The job number (
%1) refers to a process group, not a PID. It works in interactive shells and in scripts that have job control enabled, butkill %1in a non-interactive script may fail if job control is disabled . $!is the only reliable way to track a background job. It is set immediately after&and holds the PID of that specific process. If another background job starts,$!is overwritten. Store it in a variable immediately .$$is the script PID, not the subshell PID. Inside( ... ),$$still refers to the parent. Use$BASHPIDwhen the code runs in a subshell and needs its own PID .waitis how a script synchronizes with its children. Without it, the script exits and the background jobs may receive SIGHUP and terminate.wait "$pid"blocks until that process finishes and returns its exit status .disownis for jobs that must outlive the shell. It removes the job from the shell’s job table, so the shell does not send SIGHUP on exit.nohupachieves a similar result by ignoring SIGHUP in the child .kill -0checks liveness without sending a signal. It returns success if the process exists and failure if it does not. It is the standard way to check whether a PID is still valid before attempting to wait or kill.
Remember: Process management in Bash is three separate concerns. Job control (&, jobs, fg, bg) manages foreground and background execution within a shell session. PID variables ($!, $$, $BASHPID) identify specific processes. wait synchronizes with child processes and collects their exit statuses. The most common mistake is starting a background job, losing its PID, and then having no way to wait for it or kill it. Store $! immediately. The second most common mistake is using $$ inside a subshell and getting the parent’s PID. Use $BASHPID for that. And when a job must survive the script’s exit, disown it.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!