Search…
Linux from Scratch · Part 7

Processes and job control

In this series (15 parts)
  1. What is Linux and how it differs from other OSes
  2. Installing Linux and setting up your environment
  3. The Linux filesystem explained
  4. Users, groups, and permissions
  5. Essential command line tools
  6. Shell scripting fundamentals
  7. Processes and job control
  8. Standard I/O, pipes, and redirection
  9. The Linux networking stack
  10. Package management and software installation
  11. Disk management and filesystems
  12. Logs and system monitoring
  13. SSH and remote access
  14. Cron jobs and task scheduling
  15. 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:

  1. 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.

  2. 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:

StateMeaning
RRunning or runnable
SSleeping (waiting for an event)
DUninterruptible sleep (usually I/O)
TStopped (by a signal or debugger)
ZZombie (finished but parent hasn’t read exit status)

Additional characters:

  • s = session leader
  • + = foreground process group
  • l = multi-threaded
  • < = high priority
  • N = 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.

SignalNumberDefault actionMeaning
SIGHUP1TerminateHangup (terminal closed)
SIGINT2TerminateInterrupt (Ctrl+C)
SIGQUIT3Core dumpQuit (Ctrl+\)
SIGKILL9TerminateForce kill (cannot be caught)
SIGTERM15TerminatePolite termination request
SIGSTOP19StopPause process (cannot be caught)
SIGCONT18ContinueResume a stopped process
SIGUSR110TerminateUser-defined signal 1
SIGUSR212TerminateUser-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.

Start typing to search across all content
navigate Enter open Esc close