Linux Process Management and Job Control
Master Linux process management: forking, signals, background jobs, wait, kill, ps, and /proc. Lesson 7 of the Linux & Bash Scripting course.

Every command you run is a process. Understanding how the kernel creates and manages processes — through forking, signals, file descriptor inheritance, and process groups — is essential when writing scripts that launch parallel tasks, manage long-running daemons, or need to clean up after themselves reliably.
Previous: Lesson 6 — Error Handling, Traps, and Defensive Scripting
Process Concepts
PIDs, PPIDs, and the Process Tree
Every process has a PID (process ID) and a PPID (parent process ID). When a process forks, the child inherits file descriptors, environment variables, and signal dispositions from the parent.
# Your current shell's PID
echo "Shell PID: $$"
# The PID of the last background command
sleep 10 &
echo "Background job PID: $!"
# View the full process tree
pstree -p $$
# Full process list with hierarchy
ps auxfInspecting Processes
# List all processes (BSD-style)
ps aux
# Find a specific process
ps aux | grep nginx
# More targeted: use pgrep
pgrep -la nginx
# Detailed information for a specific PID
ps -p 1234 -o pid,ppid,pcpu,pmem,cmd
# Real-time monitoring
top # basic
htop # enhanced (install separately)/proc — The Process File System
Every running process has a directory under /proc/<PID>/ exposing its state:
cat /proc/$$/status # Current shell status
cat /proc/$$/cmdline | tr '\0' ' ' # Full command line
ls -la /proc/$$/fd # Open file descriptors
cat /proc/$$/environ | tr '\0' '\n' | grep PATH # Environment
cat /proc/meminfo # System memory
cat /proc/cpuinfo # CPU detailsBackground Jobs and Job Control
# Run a job in the background
long_task.sh &
# List background jobs in the current shell
jobs -l
# Bring the most recent background job to the foreground
fg
# Bring a specific job (job number 2) to the foreground
fg %2
# Send a foreground job to the background: Ctrl+Z, then:
bg
# Detach a job so it survives shell exit (immune to SIGHUP)
nohup long_task.sh > /var/log/task.log 2>&1 &
# disown removes the job from the shell's job table
long_task.sh &
disown $!Running Parallel Tasks and Waiting
#!/usr/bin/env bash
set -euo pipefail
# Launch several tasks in parallel and wait for all
pids=()
for server in web01 web02 web03; do
ssh "$server" 'systemctl restart nginx' &
pids+=($!)
done
# Wait for all background jobs and check exit codes
failed=0
for pid in "${pids[@]}"; do
if ! wait "$pid"; then
echo "Task (PID $pid) failed" >&2
(( failed++ )) || true
fi
done
if (( failed > 0 )); then
echo "ERROR: ${failed} task(s) failed" >&2
exit 1
fi
echo "All restarts completed successfully"Parallel Execution with xargs
# Run a command on 4 servers in parallel
echo -e "web01\nweb02\nweb03\nweb04" \
| xargs -P 4 -I {} ssh {} 'df -h /'
# Process a list of files in parallel (4 at a time)
find /data -name "*.csv" \
| xargs -P 4 -I {} python3 process.py {}Signals
Signals are asynchronous notifications sent to a process. The kernel, terminal, or another process can send them.
Common Signals
| Signal | Number | Default Action | Typical Use |
|---|---|---|---|
| SIGTERM | 15 | Terminate | Graceful shutdown request |
| SIGINT | 2 | Terminate | Ctrl+C from terminal |
| SIGKILL | 9 | Terminate (uncatchable) | Force kill |
| SIGHUP | 1 | Terminate | Terminal closed; reload config |
| SIGSTOP | 19 | Stop (uncatchable) | Pause process |
| SIGCONT | 18 | Continue | Resume paused process |
| SIGUSR1/2 | 10/12 | Terminate | Custom application signals |
Sending Signals
# Send SIGTERM (graceful shutdown) to a PID
kill 1234
# Send a specific signal by name or number
kill -SIGTERM 1234
kill -15 1234
# Force kill (cannot be caught or ignored)
kill -9 1234
kill -SIGKILL 1234
# Send a signal to all processes matching a name
pkill -SIGTERM nginx
# Send SIGHUP to reload nginx config (common pattern)
pkill -HUP nginx
# or
nginx -s reload
# Send SIGUSR1 to Apache to rotate logs
pkill -USR1 apache2Sending Signals to Process Groups
# A negative PID targets the entire process group
# Kill all processes in the process group of PID 1234
kill -TERM -1234
# Kill your own process group (all children)
kill -TERM 0Managing Long-Running Processes
Check if a Process is Running
is_running() {
local process_name="$1"
pgrep -x "$process_name" &>/dev/null
}
if is_running nginx; then
echo "nginx is up"
else
echo "nginx is down — restarting..."
systemctl start nginx
fiWait for a Process to Start
wait_for_process() {
local name="$1"
local timeout="${2:-30}"
local elapsed=0
echo "Waiting for ${name} to start..."
while ! pgrep -x "$name" &>/dev/null; do
sleep 1
(( elapsed++ ))
if (( elapsed >= timeout )); then
echo "ERROR: ${name} did not start within ${timeout}s" >&2
return 1
fi
done
echo "${name} started (PID: $(pgrep -x "$name"))"
}Process Substitution
Process substitution <(command) connects a command's output to a file descriptor, allowing it to be used where a filename is expected:
# Diff the sorted output of two commands without temp files
diff <(sort file1.txt) <(sort file2.txt)
# Compare the package list on two servers
diff <(ssh web01 'dpkg -l') <(ssh web02 'dpkg -l')
# Read two sorted streams simultaneously
while IFS= read -r line; do
echo "Line: $line"
done < <(find /var/log -name "*.log" -newer /tmp/last_check)Monitoring System Resources
# CPU and memory snapshot
top -bn1 | grep "Cpu(s)"
free -h
# Disk I/O statistics
iostat -xz 1 3
# Network connections
ss -tuln # listening TCP/UDP ports
ss -tunp # with process info (requires sudo)
netstat -an # classic alternative
# Per-process resource usage (one snapshot)
ps aux --sort=-%cpu | head -10 # top CPU consumers
ps aux --sort=-%mem | head -10 # top memory consumers
# Open files per process
lsof -p 1234
lsof -i :80 # what's listening on port 80