Job Control¶
Managing background processes and keeping commands running after terminal close.
Background Jobs¶
Starting Background Jobs¶
The shell returns immediately while the command runs.
Listing Jobs¶
jobs # List all jobs
jobs -l # Include PIDs
jobs -p # PIDs only
jobs -r # Running jobs only
jobs -s # Stopped jobs only
Output:
[N]- Job number+- Current job-- Previous job
Foreground and Background¶
Suspend Current Job¶
Press Ctrl+Z to suspend (stop) the foreground job:
Resume in Background¶
bg # Resume last suspended job in background
bg %1 # Resume job 1 in background
bg %vim # By command name
Bring to Foreground¶
fg # Bring current job to foreground
fg %1 # Bring job 1 to foreground
fg %+ # Current job (same as fg)
fg %- # Previous job
Job Specifiers¶
| Specifier | Meaning |
|---|---|
%N | Job number N |
%+ or %% | Current job |
%- | Previous job |
%string | Job starting with string |
%?string | Job containing string |
Keeping Jobs Running¶
The Problem¶
When you close a terminal or SSH connection, the shell sends SIGHUP to all jobs, which typically terminates them.
nohup¶
Ignore the hangup signal:
Output goes to nohup.out by default:
nohup ./long_task.sh &
# Output in: nohup.out
nohup ./task.sh > output.log 2>&1 &
# Custom output file
disown¶
Remove job from shell's job table:
./server.sh &
disown # Disown last job
disown %1 # Disown job 1
disown -h %1 # Mark to not receive SIGHUP
disown -a # Disown all jobs
Combining nohup and disown¶
When Already Running¶
If you started a job and forgot nohup:
Now safe to close terminal.
Process Substitution for Background Jobs¶
Get PID¶
Wait for Completion¶
Wait for specific job:
Check If Running¶
./command.sh &
pid=$!
# Check if still running
if kill -0 $pid 2>/dev/null; then
echo "Still running"
else
echo "Finished"
fi
Parallel Processing¶
Simple Parallelism¶
Limited Parallelism¶
max_jobs=4
for file in *.txt; do
# Wait if too many jobs running
while (( $(jobs -r -p | wc -l) >= max_jobs )); do
sleep 0.1
done
process "$file" &
done
wait
With xargs¶
With GNU Parallel¶
# Install: brew install parallel
parallel process {} ::: *.txt
parallel -j 4 process {} ::: *.txt # 4 jobs
Screen and tmux¶
For long-running interactive sessions, use terminal multiplexers.
screen¶
screen # Start new session
screen -S name # Named session
screen -ls # List sessions
screen -r # Reattach
screen -r name # Reattach to named
# Inside screen:
# Ctrl+A D - Detach
# Ctrl+A C - New window
# Ctrl+A N - Next window
tmux¶
tmux # Start new session
tmux new -s name # Named session
tmux ls # List sessions
tmux attach # Reattach
tmux attach -t name # Reattach to named
# Inside tmux:
# Ctrl+B D - Detach
# Ctrl+B C - New window
# Ctrl+B N - Next window
# Ctrl+B % - Split horizontal
# Ctrl+B " - Split vertical
Practical Patterns¶
Start Server and Verify¶
./server.sh &
pid=$!
sleep 2
if kill -0 $pid 2>/dev/null; then
echo "Server started successfully (PID: $pid)"
else
echo "Server failed to start"
exit 1
fi
Timeout for Command¶
timeout_cmd() {
local timeout=$1
shift
"$@" &
local pid=$!
(sleep "$timeout"; kill $pid 2>/dev/null) &
local timer_pid=$!
wait $pid 2>/dev/null
local exit_code=$?
kill $timer_pid 2>/dev/null
return $exit_code
}
timeout_cmd 30 ./long_task.sh
Or use the timeout command:
PID File Management¶
#!/usr/bin/env bash
PIDFILE="/var/run/myapp.pid"
start() {
if [[ -f "$PIDFILE" ]]; then
if kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "Already running"
return 1
fi
fi
./myapp &
echo $! > "$PIDFILE"
echo "Started with PID $(cat "$PIDFILE")"
}
stop() {
if [[ -f "$PIDFILE" ]]; then
kill "$(cat "$PIDFILE")" 2>/dev/null
rm -f "$PIDFILE"
echo "Stopped"
else
echo "Not running"
fi
}
case "$1" in
start) start ;;
stop) stop ;;
*) echo "Usage: $0 {start|stop}" ;;
esac
Process Pool¶
#!/usr/bin/env bash
declare -a pids=()
max_workers=4
queue=("${@}")
run_worker() {
local item="$1"
process "$item"
}
# Start workers
for ((i=0; i<max_workers && i<${#queue[@]}; i++)); do
run_worker "${queue[$i]}" &
pids+=($!)
done
# Process remaining with worker reuse
idx=$max_workers
while (( ${#pids[@]} > 0 )); do
wait -n -p finished_pid
# Remove finished PID
pids=("${pids[@]/$finished_pid}")
# Start next item
if (( idx < ${#queue[@]} )); then
run_worker "${queue[$idx]}" &
pids+=($!)
((idx++))
fi
done
Try It¶
-
Basic job control:
-
Multiple jobs:
-
Check background job:
Summary¶
| Task | Command |
|---|---|
| Run in background | command & |
| List jobs | jobs |
| Suspend foreground | Ctrl+Z |
| Resume in background | bg %N |
| Bring to foreground | fg %N |
| Keep after logout | nohup command & |
| Remove from shell | disown %N |
| Get last PID | $! |
| Wait for jobs | wait |
| Wait for specific | wait $pid |
| Kill job | kill %N |
Best practices:
- Use
nohup+disownfor jobs that must survive logout - Always
waitbefore script exits if using background jobs - Use PID files for daemon management
- Consider
tmux/screenfor interactive long-running work - Limit parallelism to avoid overwhelming system