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
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
- Always reap child processes - Prevent zombies
- Check return values - fork() and exec() can fail
- Use appropriate signals - SIGTERM before SIGKILL
- Set resource limits - Prevent resource exhaustion
- Handle SIGCHLD - Automatic zombie reaping
- Use process groups - Manage related processes together
- Monitor process health - Regular health checks
- 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