Processes and job control
In this series (15 parts)
- What is Linux and how it differs from other OSes
- Installing Linux and setting up your environment
- The Linux filesystem explained
- Users, groups, and permissions
- Essential command line tools
- Shell scripting fundamentals
- Processes and job control
- Standard I/O, pipes, and redirection
- The Linux networking stack
- Package management and software installation
- Disk management and filesystems
- Logs and system monitoring
- SSH and remote access
- Cron jobs and task scheduling
- Linux security basics for sysadmins
Every program running on your Linux system is a process. When you type ls, a new process is created, it does its job, and then it exits. When you start a web server, a process is created and it stays running until you tell it to stop. Understanding processes is essential for debugging, performance tuning, and security.
Prerequisites
You should be comfortable with shell scripting and command line tools before this article.
The process lifecycle
Every process in Linux follows this lifecycle:
graph TD A[Parent Process] -->|fork| B[Child Process - copy of parent] B -->|exec| C[New Program Loaded] C -->|running| D[Process Running] D -->|exit| E[Zombie - waiting for parent] E -->|wait| F[Process Reaped - gone] D -->|signal| G[Terminated] G --> E style A fill:#64b5f6,stroke:#1976d2,color:#000 style D fill:#81c784,stroke:#388e3c,color:#000 style E fill:#ffb74d,stroke:#f57c00,color:#000
fork and exec
When you run a command, the shell does two things:
-
fork() creates a copy of the current process (the shell). The copy is called the child process. It has a new PID but is otherwise identical to the parent.
-
exec() replaces the child’s program with the new program you asked for. The child’s memory is overwritten with the new program’s code.
This is why environment variables are inherited by child processes. The fork copies everything, and exec keeps the environment.
# See the parent-child relationship
echo "Shell PID: $$"
bash -c 'echo "Child PID: $$ Parent PID: $PPID"'
Output:
Shell PID: 1234
Child PID: 5678 Parent PID: 1234
Process states
ps aux | head -1
ps aux | grep nginx
Output:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1230 0.0 0.0 12345 2345 ? Ss 10:00 0:00 nginx: master
www-data 1234 0.0 0.1 12345 5678 ? S 10:00 0:00 nginx: worker
The STAT column tells you the process state:
| State | Meaning |
|---|---|
| R | Running or runnable |
| S | Sleeping (waiting for an event) |
| D | Uninterruptible sleep (usually I/O) |
| T | Stopped (by a signal or debugger) |
| Z | Zombie (finished but parent hasn’t read exit status) |
Additional characters:
s= session leader+= foreground process groupl= multi-threaded<= high priorityN= low priority
Zombie processes
A zombie is a process that has finished executing but still has an entry in the process table because its parent has not called wait() to read its exit status.
# Find zombies
ps aux | awk '$8 ~ /Z/ {print}'
A few zombies are normal (transient). Many zombies indicate a buggy parent process that is not reaping its children. The fix is to fix or restart the parent.
Foreground vs background jobs
Running in the background
# Start a long-running command in the background
sleep 300 &
Output:
[1] 5678
[1] is the job number, 5678 is the PID.
# List background jobs
jobs
Output:
[1]+ Running sleep 300 &
Switching between foreground and background
# Start something in the foreground
sleep 300
# Press Ctrl+Z to suspend it
# Output: [1]+ Stopped sleep 300
# Resume in the background
bg %1
# Bring it back to the foreground
fg %1
Keeping jobs alive after logout
When you close a terminal, a SIGHUP signal is sent to all child processes, which typically kills them. To prevent this:
# Method 1: nohup
nohup ./long-running-script.sh &
# Output goes to nohup.out
# Method 2: disown
./long-running-script.sh &
disown %1
For production services, use systemd instead (covered below).
Signals
Signals are notifications sent to processes. They are how the system communicates with running programs.
| Signal | Number | Default action | Meaning |
|---|---|---|---|
| SIGHUP | 1 | Terminate | Hangup (terminal closed) |
| SIGINT | 2 | Terminate | Interrupt (Ctrl+C) |
| SIGQUIT | 3 | Core dump | Quit (Ctrl+\) |
| SIGKILL | 9 | Terminate | Force kill (cannot be caught) |
| SIGTERM | 15 | Terminate | Polite termination request |
| SIGSTOP | 19 | Stop | Pause process (cannot be caught) |
| SIGCONT | 18 | Continue | Resume a stopped process |
| SIGUSR1 | 10 | Terminate | User-defined signal 1 |
| SIGUSR2 | 12 | Terminate | User-defined signal 2 |
Sending signals
# Send SIGTERM (default, polite)
kill 5678
# Send SIGKILL (force, last resort)
kill -9 5678
# Send SIGHUP (often used to reload config)
kill -HUP 1230
# Send by name
kill -SIGTERM 5678
Always try SIGTERM first. It gives the process a chance to clean up (close files, save state, release locks). Only use SIGKILL if SIGTERM does not work after a few seconds.
Catching signals in scripts
#!/bin/bash
# Set up a signal handler
cleanup() {
echo "Caught signal, cleaning up..."
rm -f /tmp/my-lockfile
exit 0
}
trap cleanup SIGTERM SIGINT
echo "Running (PID: $$). Press Ctrl+C to stop."
echo $$ > /tmp/my-lockfile
while true; do
sleep 1
done
systemd and init
systemd is the init system on most modern Linux distributions. It is the first process started by the kernel (PID 1) and manages all other services.
Checking service status
# Check if a service is running
systemctl status nginx
Output:
● nginx.service - A high performance web server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled)
Active: active (running) since Mon 2026-05-04 10:00:00 UTC; 3h ago
Process: 1228 ExecStart=/usr/sbin/nginx (code=exited, status=0/SUCCESS)
Main PID: 1230 (nginx)
Tasks: 3 (limit: 4915)
Memory: 12.5M
CPU: 234ms
CGroup: /system.slice/nginx.service
├─1230 "nginx: master process /usr/sbin/nginx"
├─1234 "nginx: worker process"
└─1235 "nginx: worker process"
Managing services
# Start a service
sudo systemctl start nginx
# Stop a service
sudo systemctl stop nginx
# Restart a service
sudo systemctl restart nginx
# Reload config without restart
sudo systemctl reload nginx
# Enable at boot
sudo systemctl enable nginx
# Disable at boot
sudo systemctl disable nginx
Viewing logs for a service
# Recent logs
journalctl -u nginx --since "1 hour ago"
# Follow logs in real time
journalctl -u nginx -f
For more on logs and monitoring, see article 12.
Example 1: Manage a long-running job
Let’s walk through a real workflow of managing background processes.
Start a simulated long-running task:
# Start a fake data processing job
(for i in $(seq 1 100); do echo "Processing record $i"; sleep 1; done) &
Output:
[1] 7890
Processing record 1
Check on it:
jobs -l
Output:
[1]+ 7890 Running ( for i in $(seq 1 100); do echo "Processing record $i"; sleep 1; done ) &
The output is cluttering your terminal. Redirect it:
# Stop the job
kill %1
# Restart with output redirected to a file
(for i in $(seq 1 100); do echo "Processing record $i"; sleep 1; done) > /tmp/processing.log 2>&1 &
Output:
[2] 7891
Monitor progress without interrupting:
# Check how far it got
tail -3 /tmp/processing.log
Output:
Processing record 15
Processing record 16
Processing record 17
# Watch it in real time
tail -f /tmp/processing.log
# Press Ctrl+C to stop watching (the job keeps running)
Now suppose you need to pause the job:
# Send SIGSTOP
kill -STOP 7891
# Verify it stopped
ps -p 7891 -o pid,stat,comm
Output:
PID STAT COMMAND
7891 T bash
The T state means stopped. Resume it:
kill -CONT 7891
# Verify it is running again
ps -p 7891 -o pid,stat,comm
Output:
PID STAT COMMAND
7891 S bash
Example 2: Send signals and observe behavior
Let’s write a script that handles signals and see what happens:
cat > signal-demo.sh << 'SCRIPT'
#!/bin/bash
echo "PID: $$"
echo "Send me signals to see what happens."
echo " kill -TERM $$ (graceful shutdown)"
echo " kill -USR1 $$ (print status)"
echo " kill -USR2 $$ (reset counter)"
echo ""
counter=0
handle_term() {
echo "Received SIGTERM. Shutting down gracefully..."
echo "Final counter value: $counter"
exit 0
}
handle_usr1() {
echo "Status: counter = $counter, uptime = $SECONDS seconds"
}
handle_usr2() {
echo "Resetting counter from $counter to 0"
counter=0
}
trap handle_term SIGTERM
trap handle_usr1 SIGUSR1
trap handle_usr2 SIGUSR2
while true; do
((counter++))
sleep 1
done
SCRIPT
chmod +x signal-demo.sh
Run it in one terminal:
./signal-demo.sh
Output:
PID: 8888
Send me signals to see what happens.
kill -TERM 8888 (graceful shutdown)
kill -USR1 8888 (print status)
kill -USR2 8888 (reset counter)
In another terminal, send signals:
# Check status
kill -USR1 8888
In the first terminal:
Status: counter = 15, uptime = 15 seconds
# Reset counter
kill -USR2 8888
Output:
Resetting counter from 15 to 0
# Graceful shutdown
kill -TERM 8888
Output:
Received SIGTERM. Shutting down gracefully...
Final counter value: 3
Now try with SIGKILL:
./signal-demo.sh &
kill -9 $!
No cleanup message appears. SIGKILL cannot be caught. The process is terminated immediately without running any cleanup code. This is why you should only use -9 as a last resort.
Clean up:
rm -f signal-demo.sh
What comes next
Next up is Standard I/O, pipes, and redirection, which shows how processes communicate through streams and how to chain commands together.
Understanding processes and signals is also critical for Linux security basics, where you will learn how to harden services managed by systemd.