Linux Process Management: Fork, Exec, and Beyond

12 min

Master Linux process management through interactive visualizations. Understand process lifecycle, fork/exec operations, zombies, orphans, and CPU scheduling.

Best viewed on desktop for optimal interactive experience

The Heart of Linux: Process Management

Every program you run on Linux becomes a process - a living entity with its own memory space, resources, and lifecycle. From the moment you boot your system with PID 1 (init/systemd) to the thousands of processes running right now, understanding process management is key to mastering Linux.

Think of processes as actors on a stage. The kernel is the director, the CPU cores are the stages, and the scheduler decides who performs when. Some actors are parents who create children (fork), some transform into entirely different characters (exec), and sometimes actors die but refuse to leave the stage (zombies).

Let's explore this fascinating world where programs come to life, multiply, transform, and eventually die.

Interactive Process Visualization

Watch every step of process creation, execution, and termination - from fork to zombie reaping:

Process Management Deep Dive

Watch processes fork, exec, become zombies, get orphaned, and transition through states - every step visualized.

Fork & Exec: Creating New ProgramsStep 1 of 10

Parent process running

Process PID 1234 is executing. It needs to run a child program (e.g., shell executes "ls" command).

Process Table:
1234
bash
PID: 1234
RUNNING
PC: main+0x42
Memory: code | data | stack
PID: 1234 Name: bash State: RUNNING
Memory: Code (read-only), Data, Stack
Program Counter: main+0x42
About to call fork() to create child
Current working: parsing user command "ls"
1 / 10
10% complete

The Process Tree

Every process on a Linux system is part of a hierarchical tree structure, with PID 1 (systemd/init) at the root:

A Typical Process Tree

A Typical Process TreesystemdPID 1 | PPID 0systemd-journaldPID 100 | PPID 1sshdPID 500 | PPID 1nginxPID 800 | PPID 1sshd: alicePID 1001 | PPID 500sshd: bobPID 1050 | PPID 500nginx workerPID 801nginx workerPID 802bashPID 1010 | PPID 1001zshPID 1060 | PPID 1050vim document.txtPID 1015 | PPID 1010python script.pyPID 1020 | PPID 1010Root (ancestor of all)System daemonsUser sessionsUser shellsUser commands

Key Concepts

PID (Process ID): Unique identifier for each process
PPID (Parent Process ID): PID of the process that created this process
Root: PID 1 (systemd/init) has PPID 0 (the kernel)
Tree Structure: Each process has exactly one parent, but can have many children

Viewing the tree: Use pstree -p to see this hierarchy with PIDs, or ps axjf for a tree-like format.

Process Fundamentals

What is a Process?

A process is more than just a running program. It's a container managed by the kernel that includes:

  • Identity: PID (Process ID), PPID (Parent PID), UID/GID (owner)
  • State: RUNNING, READY, WAITING, STOPPED, ZOMBIE
  • Memory: Code, data, heap, stack (separate virtual address space)
  • Resources: Open files, network connections, signals
  • Scheduling: Priority, nice value, CPU time consumed

The kernel represents each process with a task_struct - a data structure containing all this metadata.

Process vs Thread

Process: Heavy-weight, separate memory space, independent execution. Thread: Light-weight, shared memory space within process, shared resources.

When Firefox runs with 47 threads, they all share memory and file descriptors but have separate stacks and registers.

The Fork-Exec Model

fork(): Cloning Processes

fork() creates a new process by duplicating the calling process:

  1. Kernel creates child: New PID, copy of task_struct
  2. Copy-on-Write (COW): Memory pages shared, not copied
  3. Returns twice: Returns 0 to child, child PID to parent
  4. Identical but separate: Same code, but two different processes

Why Copy-on-Write? Copying gigabytes of memory would be wasteful. Instead, parent and child share read-only pages. Only when either writes to a page does the kernel create an actual copy. Efficient!

exec(): Transformation

exec() replaces the current process image with a new program:

  1. Same PID: Process identity preserved
  2. New program: Code, data, stack replaced from executable file
  3. File descriptors preserved: Open files remain open (unless close-on-exec)
  4. Common pattern: fork() followed by exec() in child

Example: When you type ls in bash, bash calls fork() to create a child copy of itself, then the child calls exec("/bin/ls") to replace itself with the ls program.

Process States

Every process is always in one of these states:

  1. NEW: Being created (fork in progress)
  2. READY: Runnable, waiting in scheduler queue for CPU
  3. RUNNING: Actively executing on a CPU core
  4. WAITING: Blocked on I/O or event (sleep, disk read, network)
  5. STOPPED: Suspended by signal (SIGSTOP, Ctrl+Z)
  6. ZOMBIE: Terminated but exit status not yet collected by parent

State Transitions

  • READY → RUNNING: Scheduler assigns CPU
  • RUNNING → READY: Time slice expired (preempted)
  • RUNNING → WAITING: Process blocks (I/O, sleep, lock)
  • WAITING → READY: Event completes (I/O done, wake up)
  • RUNNING → STOPPED: Receives SIGSTOP or SIGTSTP
  • STOPPED → READY: Receives SIGCONT
  • RUNNING → ZOMBIE: Calls exit() or killed

Zombies & Orphans

Zombie Processes

When a process terminates, it becomes a zombie - dead but not fully gone:

  • Why exist? Must preserve exit status for parent to retrieve
  • What's left? Only task_struct (process descriptor), no memory
  • How to see? ps aux | grep defunct or state shows 'Z'
  • How to kill? You can't! Already dead. Parent must call wait()
  • Problem: If parent never calls wait(), zombie persists forever

Orphan Processes

When a parent dies before its children:

  • Kernel re-parents: Orphans automatically adopted by init (PID 1)
  • init reaps: init periodically calls wait() to clean up orphans
  • No zombie accumulation: init ensures orphans don't become permanent zombies

This is why PID 1 (init/systemd) is special - it's the ultimate parent that never dies and always cleans up its adopted children.

Orphan Process Adoption

Orphan Process AdoptionBefore Parent Diessystemd (PID 1)PPID: 0Parent (PID 1000)PPID: 1Child (PID 1001)PPID: 1000Parent exits(crashes, killed, or normal exit)After Parent Diessystemd (PID 1)PPID: 0Parent (PID 1000)GONERe-parented!Child (PID 1001)PPID: 1Why Reparenting Happens1. Process exits: Parent process terminates (crash, kill signal, or normal exit)2. Kernel detects orphans: Any children with PPID pointing to dead process3. Automatic adoption: Kernel changes children's PPID to 1 (systemd)4. Life continues: Child processes keep running, now with systemd as parent

Why This Matters

Orphan adoption ensures every process always has a parent. This is critical because when a process exits, it becomes a zombie until its parent calls wait(). If orphans weren't adopted, they'd stay as zombies forever. Systemd automatically reaps (cleans up) zombies from processes it adopts.

Real-World Example

When you close a terminal window that's running background processes, those processes become orphans and are immediately adopted by init/systemd. They continue running happily under their new parent.

$ nohup long_running_job &
# Close terminal - process becomes orphan
# systemd adopts it, process keeps running!

Process Creation Workflow

Step-by-step: Shell executes "ls" command

  1. User types: ls -la
  2. Shell (bash) calls: fork()
  3. Kernel creates child: New process, same code as bash
  4. fork() returns: Parent gets child PID (e.g., 1235), child gets 0
  5. Parent (bash): Calls wait() to wait for child
  6. Child (bash clone): Calls exec("/bin/ls", "-la")
  7. exec replaces child: bash code replaced with ls code, same PID
  8. Child (now ls): Executes, prints directory listing
  9. Child exits: Calls exit(0), becomes zombie
  10. Parent reaps: wait() retrieves exit status, zombie cleaned up
  11. Shell ready: Shows prompt again

Process Inspection

# View all processes ps aux # Process tree showing relationships pstree -p # Detailed process info ps -eo pid,ppid,state,comm,cmd # Watch processes in real-time top htop # Check specific process ps -p 1234 -o pid,ppid,state,comm,%cpu,%mem # View process memory map cat /proc/1234/maps # View process status cat /proc/1234/status # View process file descriptors ls -l /proc/1234/fd/

Process Control

# Create background process command & # List jobs jobs # Bring to foreground fg %1 # Send to background bg %1 # Suspend process (Ctrl+Z sends SIGTSTP) # Resume with: fg or bg # Send signals kill -SIGTERM 1234 # Request termination (15) kill -SIGKILL 1234 # Force kill (9, cannot be caught) kill -SIGSTOP 1234 # Suspend (19) kill -SIGCONT 1234 # Resume (18) # Kill all processes by name pkill firefox killall chromium

Process Priorities

Nice Values

Process priority controlled by "nice" value (-20 to +19):

  • -20: Highest priority (least nice, hogs CPU)
  • 0: Default priority
  • +19: Lowest priority (very nice, yields CPU)
# Start with lower priority nice -n 10 ./cpu-intensive-task # Change priority of running process renice -n 5 -p 1234 # Require root for negative nice (higher priority) sudo renice -n -10 -p 1234

Real-time Priorities

For critical processes needing guaranteed CPU time:

# Set real-time priority (requires root) chrt -f 99 ./realtime-app # SCHED_FIFO priority 99 chrt -r 50 ./realtime-app # SCHED_RR priority 50 # View scheduling policy chrt -p 1234

Sessions and Process Groups

Beyond the parent-child hierarchy, processes are organized into sessions and process groups for job control and terminal management:

Sessions, Process Groups, and Processes

Sessions, Process Groups, and ProcessesSession (SID 1000)Controlling terminal: /dev/pts/0Process Group 1000 (Foreground)PGID: 1000 | Session LeaderbashPID 1000 (leader)PGID 1000vimPID 1010PGID 1000helper processPID 1011PGID 1000Process Group 1020 (Background)PGID: 1020grep (PID 1020)|sort (PID 1021)|uniq (PID 1022)(all in same PGID)Process Group 1030long_running_job (PID 1030)Key Concepts• Session: Related processes• Process Group: Job control← Ctrl+CSIGINT toforegroundgroup only

Session

Collection of process groups, usually one per terminal/login. Session leader creates session with setsid()

Process Group

One or more processes that can receive signals together. Used for job control (Ctrl+C, Ctrl+Z affect whole group)

Process

Individual running program within a process group

Example: Pipeline Command

$ cat file.txt | grep pattern | wc -l &
# Creates 3 processes in 1 process group, running in background
# All 3 have the same PGID
# All belong to the same session (your terminal)

⚠️Signal Delivery

When you press Ctrl+C, the kernel sends SIGINT to all processes in theforeground process group only. Background jobs are unaffected. This is why pipeline commands all terminate together - they're in the same process group!

Understanding the Hierarchy

  • Session: A collection of process groups, typically one per terminal/login
  • Process Group: One or more processes treated as a unit for job control
  • Process: Individual running program

When you press Ctrl+C, SIGINT is sent to all processes in the foreground process group only. This is why piped commands (like cat file | grep pattern | wc) all terminate together - they share the same PGID!

CPU Scheduling

Linux uses the Completely Fair Scheduler (CFS) for normal processes:

  • Red-black tree: Processes sorted by virtual runtime
  • Fairness: Each process gets fair share of CPU proportional to priority
  • Time slices: Dynamic, based on number of runnable processes
  • Real-time classes: SCHED_FIFO and SCHED_RR for real-time tasks

Context Switch: When scheduler switches from one process to another:

  1. Save current process state (registers, PC) to memory
  2. Load new process state from memory
  3. Switch page tables (change memory view)
  4. Jump to new process's program counter

Context switches are expensive (~microseconds) so minimize thrashing!

Common Patterns

Fork-Exec Pattern

# Shell implementing command execution pid = fork() if (pid == 0) { # Child process exec("/bin/ls") } else { # Parent process wait(&status) }

Daemon Process

Long-running background service:

  1. fork() and parent exits (detach from terminal)
  2. setsid() to create new session
  3. Change directory to /
  4. Close stdin/stdout/stderr
  5. Open /dev/null for standard streams
  6. Write PID to /var/run/daemon.pid

Signal Handlers

# Install signal handler signal(SIGINT, handler_function) # Handler receives signal, performs cleanup # Can ignore, catch, or default action

Best Practices

  1. Always wait() for children: Prevent zombie accumulation
  2. Handle signals gracefully: Clean up resources on SIGTERM
  3. Set resource limits: Prevent runaway processes
  4. Use appropriate priorities: Don't starve other processes
  5. Monitor process count: Too many processes = system slowdown
  6. Clean up file descriptors: Close unneeded files in child after fork
  7. Check return values: fork() can fail if resource limits hit

If you found this explanation helpful, consider sharing it with others.

Mastodon