Linux Process Management: Fork, Exec, and Beyond

20 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

Explore the complete process lifecycle, fork/exec operations, and scheduling in action:

Process State Transitions

New
Ready
Running
Waiting
Terminated
Zombie

Current Step: New

Process is being created (fork)

Process Fundamentals

What is a Process?

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

struct task_struct { // Identity pid_t pid; // Process ID pid_t tgid; // Thread group ID pid_t ppid; // Parent process ID // State volatile long state; // RUNNING, SLEEPING, STOPPED, ZOMBIE int exit_code; // Exit status // Memory struct mm_struct *mm; // Memory descriptor unsigned long stack; // Kernel stack // Scheduling int prio; // Priority int nice; // Nice value struct sched_entity se; // Scheduling entity // Resources struct files_struct *files; // Open files struct signal_struct *signal; // Signals struct rlimit rlim[RLIM_NLIMITS]; // Resource limits };

Process vs Thread

# Process: Separate memory space # Thread: Shared memory space within process # View threads ps -eLf | grep firefox # PID PPID LWP C NLWP # 12345 1 12345 0 47 # Firefox with 47 threads # Threads share: # - Memory space # - File descriptors # - Signal handlers # - Current directory # Threads have separate: # - Stack # - Registers # - Thread ID (TID) # - Signal mask

Process Lifecycle

1. Birth: Fork System Call

The fork() system call is how processes reproduce in Linux:

#include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid < 0) { // Fork failed perror("fork failed"); return 1; } else if (pid == 0) { // Child process printf("I'm the child, PID: %d\n", getpid()); printf("My parent is: %d\n", getppid()); } else { // Parent process printf("I'm the parent, PID: %d\n", getpid()); printf("My child is: %d\n", pid); } return 0; }

Copy-on-Write (COW) Magic

Fork doesn't immediately copy all memory. It uses COW:

# Before write: Parent and child share pages Parent [Page1|Page2|Page3] <-- Shared --> [Page1|Page2|Page3] Child # After child writes to Page2: Parent [Page1|Page2|Page3] [Page1|Page2'|Page3] Child ↑ ↑ Original New copy

2. Transformation: Exec System Call

Exec replaces the current process image with a new program:

#include <unistd.h> int main() { printf("Before exec - PID: %d\n", getpid()); // This line replaces the entire process execl("/bin/ls", "ls", "-la", NULL); // This never executes if exec succeeds printf("This won't print!\n"); return 0; }

The Fork-Exec Pattern

This is how shells launch programs:

// Simplified shell implementation void execute_command(char *cmd) { pid_t pid = fork(); if (pid == 0) { // Child: execute the command execl("/bin/sh", "sh", "-c", cmd, NULL); exit(1); // exec failed } else if (pid > 0) { // Parent: wait for child int status; waitpid(pid, &status, 0); printf("Command exited with status: %d\n", WEXITSTATUS(status)); } }

3. Death and Beyond: Process Termination

Processes can die in several ways:

// Normal termination exit(0); // Clean exit with status 0 return 0; // From main() // Abnormal termination abort(); // Raises SIGABRT raise(SIGKILL); // Send signal to self // Killed by signal kill(pid, SIGTERM); // From another process

Zombie Processes: The Walking Dead

A zombie is a process that has died but still has an entry in the process table:

#include <unistd.h> #include <stdlib.h> int main() { pid_t pid = fork(); if (pid == 0) { // Child dies immediately exit(0); } else { // Parent doesn't call wait() printf("Child is now a zombie!\n"); system("ps aux | grep defunct"); sleep(30); // Zombie exists for 30 seconds // Now reap the zombie wait(NULL); printf("Zombie reaped!\n"); } return 0; }

Preventing Zombies

// Method 1: Signal handler for SIGCHLD void sigchld_handler(int sig) { // Reap all available zombie children while (waitpid(-1, NULL, WNOHANG) > 0); } signal(SIGCHLD, sigchld_handler); // Method 2: Double fork technique pid_t pid = fork(); if (pid == 0) { // First child pid_t pid2 = fork(); if (pid2 == 0) { // Grandchild - adopted by init do_work(); exit(0); } exit(0); // First child dies immediately } wait(NULL); // Reap first child

Orphan Processes: Adopted by Init

When a parent dies before its children:

int main() { pid_t pid = fork(); if (pid == 0) { // Child printf("Original PPID: %d\n", getppid()); sleep(5); // Parent dies during this sleep printf("New PPID: %d (adopted by init)\n", getppid()); // PPID is now 1 (init/systemd) } else { // Parent dies immediately exit(0); } return 0; }

Process States

Linux processes go through several states:

# View process states ps aux | awk '{print $8}' | sort | uniq -c # State codes: # R - Running or runnable # S - Sleeping (interruptible) # D - Sleeping (uninterruptible, usually I/O) # Z - Zombie/defunct # T - Stopped (by signal or trace) # I - Idle kernel thread # Detailed state info cat /proc/<PID>/stat | awk '{print $3}'

State Transitions

fork() ┌─────────┐ │ NEW │ └────┬────┘ ↓ admitted ┌─────────┐ dispatch ┌─────────┐ │ READY │ ←─────────────────→│ RUNNING │ └─────────┘ └────┬────┘ ↑ │ │ I/O or event ↓ │ wait ┌─────────┐ └──────────────────────────│ WAITING │ └─────────┘ exit()↓ ┌─────────┐ │TERMINATED│ └─────────┘

CPU Scheduling

The Completely Fair Scheduler (CFS)

Linux uses CFS for normal processes:

// Simplified CFS concept struct sched_entity { u64 vruntime; // Virtual runtime u64 sum_exec_runtime; // Total CPU time used // Lower vruntime = higher priority for scheduling }; // Nice values affect vruntime progression // Nice -20: vruntime increases slowly (high priority) // Nice 0: vruntime increases normally // Nice +19: vruntime increases quickly (low priority)

Viewing Scheduling Info

# Real-time scheduling info cat /proc/<PID>/sched # Nice value and priority nice -n 10 ./myprogram # Run with nice 10 renice -n 5 -p 1234 # Change nice value # Scheduling policy chrt -f 50 ./realtime_app # FIFO with priority 50 chrt -r 30 ./another_app # Round-robin priority 30 # CPU affinity taskset -c 0,1 ./myapp # Run only on cores 0 and 1 taskset -p 0x3 1234 # Set affinity for running process

Process Groups and Sessions

Processes are organized into groups and sessions:

# Process group - collection of related processes ps -eo pid,ppid,pgid,sid,cmd # Session - collection of process groups # Usually corresponds to a terminal session # Create new session (become session leader) setsid ./myprogram # Send signal to process group kill -TERM -1234 # Negative PID = process group

Resource Limits

Control process resources with limits:

# View limits ulimit -a cat /proc/<PID>/limits # Set limits ulimit -n 4096 # Max open files ulimit -u 1000 # Max processes ulimit -v 1000000 # Max virtual memory (KB) # In C struct rlimit rl; rl.rlim_cur = 1024; // Soft limit rl.rlim_max = 4096; // Hard limit setrlimit(RLIMIT_NOFILE, &rl);

Process Monitoring

Key Tools

# ps - snapshot of processes ps aux # All processes ps -eLf # Include threads ps -p 1234 -o pid,ppid,%cpu,%mem,cmd # Specific PID # top/htop - real-time monitoring top -p 1234 # Monitor specific PID htop -t # Tree view # pstree - process hierarchy pstree -p # Show PIDs pstree -u username # User's processes # /proc filesystem ls /proc/1234/ # Process info cat /proc/1234/status # Detailed status cat /proc/1234/cmdline # Command line cat /proc/1234/environ # Environment variables

Tracing Process Activity

# strace - system call tracing strace -p 1234 # Attach to process strace -f ./program # Follow forks strace -e open,read,write # Filter syscalls # ltrace - library call tracing ltrace -p 1234 ltrace -f ./program # perf - performance analysis perf record -p 1234 perf report

Inter-Process Communication (IPC)

Processes need to communicate:

Signals

// Send signal kill(pid, SIGTERM); // Handle signal void handler(int sig) { printf("Received signal %d\n", sig); } signal(SIGINT, handler); // Common signals // SIGTERM (15) - Polite termination request // SIGKILL (9) - Forceful termination (can't catch) // SIGSTOP (19) - Stop process (can't catch) // SIGCONT (18) - Continue stopped process // SIGCHLD (17) - Child status changed

Pipes

int pipefd[2]; pipe(pipefd); if (fork() == 0) { // Child: write to pipe close(pipefd[0]); // Close read end write(pipefd[1], "Hello", 5); close(pipefd[1]); } else { // Parent: read from pipe char buf[10]; close(pipefd[1]); // Close write end read(pipefd[0], buf, 10); close(pipefd[0]); }

Best Practices

  1. Always reap child processes - Prevent zombies
  2. Check return values - fork() and exec() can fail
  3. Use appropriate signals - SIGTERM before SIGKILL
  4. Set resource limits - Prevent resource exhaustion
  5. Handle SIGCHLD - Automatic zombie reaping
  6. Use process groups - Manage related processes together
  7. Monitor process health - Regular health checks
  8. Log process events - Audit trail for debugging

Common Pitfalls

Fork Bomb

# DON'T RUN THIS! :(){ :|:& };: # What it does: # :() - Define function ':' # { :|:& } - Call itself twice in background # ;: - Call the function # Prevention: ulimit -u 100 # Limit max processes

Double Free After Fork

// Wrong - both parent and child will free char *buf = malloc(100); fork(); free(buf); // Both processes free same memory! // Right - careful with shared resources char *buf = malloc(100); pid_t pid = fork(); if (pid == 0) { // Child doesn't own the buffer _exit(0); // Use _exit to skip cleanup } else { free(buf); // Only parent frees }

Conclusion

Process management is the foundation of Linux system programming. From the simple elegance of fork() to the complexity of the scheduler, understanding processes gives you power over your system. The interactive visualizations showed how processes live, die, and interact in real-time.

Remember: every command you type creates a process, every process has a parent (except init), and every zombie was once a living process whose parent forgot to say goodbye properly.

Master process management, and you master Linux itself.

Next: Memory Management → ← Back to Filesystems

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

Mastodon