Back to Technology

Part 9: Processes & Program Execution

January 31, 2026 Wasil Zafar 28 min read

Understand how operating systems manage processes—from creation and scheduling to termination—and learn the fork/exec model that powers Unix systems.

Table of Contents

  1. Introduction
  2. Process Concept
  3. Process Control Block
  4. Process Creation
  5. Process Termination
  6. Inter-Process Communication
  7. Conclusion & Next Steps

Introduction

A process is a program in execution—it's the fundamental unit of work in an operating system. Understanding processes is essential for systems programming, performance tuning, and debugging complex applications.

Series Context: This is Part 9 of 24 in the Computer Architecture & Operating Systems Mastery series. Building on kernel architecture, we now explore how the OS manages running programs.

Computer Architecture & OS Mastery

Your 24-step learning path • Currently on Step 9
1
Part 1: Foundations of Computer Systems
System overview, architectures, OS role
2
Digital Logic & CPU Building Blocks
Gates, registers, datapath, microarchitecture
3
Instruction Set Architecture (ISA)
RISC vs CISC, instruction formats, addressing
4
Assembly Language & Machine Code
Registers, stack, calling conventions
5
Assemblers, Linkers & Loaders
Object files, ELF, dynamic linking
6
Compilers & Program Translation
Lexing, parsing, code generation
7
CPU Execution & Pipelining
Fetch-decode-execute, hazards, prediction
8
OS Architecture & Kernel Design
Monolithic, microkernel, system calls
9
Processes & Program Execution
Process lifecycle, PCB, fork/exec
You Are Here
10
Threads & Concurrency
Threading models, pthreads, race conditions
11
CPU Scheduling Algorithms
FCFS, RR, CFS, real-time scheduling
12
Synchronization & Coordination
Locks, semaphores, classic problems
13
Deadlocks & Prevention
Coffman conditions, Banker's algorithm
14
Memory Hierarchy & Cache
L1/L2/L3, cache coherence, NUMA
15
Memory Management Fundamentals
Address spaces, fragmentation, allocation
16
Virtual Memory & Paging
Page tables, TLB, demand paging
17
File Systems & Storage
Inodes, journaling, ext4, NTFS
18
I/O Systems & Device Drivers
Interrupts, DMA, disk scheduling
19
Multiprocessor Systems
SMP, NUMA, cache coherence
20
OS Security & Protection
Privilege levels, ASLR, sandboxing
21
Virtualization & Containers
Hypervisors, namespaces, cgroups
22
Advanced Kernel Internals
Linux subsystems, kernel debugging
23
Case Studies
Linux vs Windows vs macOS
24
Capstone Projects
Shell, thread pool, paging simulator

What is a Process?

A process is a program in execution—a running instance of a program with its own resources, memory space, and execution context. While a program is static code on disk, a process is dynamic and active.

Analogy: A program is like a recipe (instructions written down). A process is like actually cooking that recipe—with ingredients (data), a chef (CPU), pots and pans (resources), and the current step you're on (program counter).

Process Concept

Process vs Program

Program vs Process

Program vs Process:
══════════════════════════════════════════════════════════════

PROGRAM (Static)                    PROCESS (Dynamic)
────────────────────────────────    ────────────────────────────────
• Code stored on disk              • Program loaded in memory
• Passive entity                   • Active entity
• No state                         • Has execution state
• Can exist without executing      • Exists only while running
• One program file                 • Multiple processes possible

Example:
┌─────────────────┐                ┌─────────────────┐
│   /bin/bash     │                │  Process 1234   │
│   (executable)  │  ───────────→  │  bash (user A)  │
│                 │  fork + exec   │                 │
└─────────────────┘                └─────────────────┘
        │
        │                          ┌─────────────────┐
        └───────────────────────→  │  Process 5678   │
                      fork + exec  │  bash (user B)  │
                                   └─────────────────┘

Same program, multiple processes, each with own:
• Memory space (isolated)
• File descriptors
• Environment variables
• Current working directory

Process Memory Layout

Process Address Space

Process Memory Layout (Linux/x86-64):
══════════════════════════════════════════════════════════════
High Addresses
┌─────────────────────────────────────────┐ 0x7FFFFFFFFFFF
│              Kernel Space               │ (inaccessible to user)
│         (mapped but protected)          │
├─────────────────────────────────────────┤ 
│                                         │
│              Stack ↓                    │ Grows downward
│         (local variables, return        │ 
│          addresses, function params)    │
│                                         │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤ ← Stack limit
│                                         │
│         (unmapped - guard region)       │
│                                         │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│                                         │
│      Memory-Mapped Region               │
│    (shared libraries, mmap files)       │
│                                         │
├─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤
│                                         │
│              Heap ↑                     │ Grows upward
│    (dynamic allocation: malloc/new)     │
│                                         │
├─────────────────────────────────────────┤ ← Program break (brk)
│              BSS                        │ Uninitialized globals
│    (zero-initialized by OS)             │ int global_array[1000];
├─────────────────────────────────────────┤
│             Data                        │ Initialized globals
│    (initialized global variables)       │ int count = 42;
├─────────────────────────────────────────┤
│             Text (Code)                 │ Machine instructions
│    (read-only, executable)              │ main(), printf(), etc.
└─────────────────────────────────────────┘ 0x400000
Low Addresses
# Examine a process's memory map on Linux
$ cat /proc/self/maps
00400000-00401000 r--p 00000000 fd:00 123456  /usr/bin/cat     # Text (code)
00401000-00408000 r-xp 00001000 fd:00 123456  /usr/bin/cat     # Text (code)
00608000-00609000 r--p 00008000 fd:00 123456  /usr/bin/cat     # Read-only data
00609000-0060a000 rw-p 00009000 fd:00 123456  /usr/bin/cat     # Data (r/w)
0060a000-0062b000 rw-p 00000000 00:00 0       [heap]           # Heap
7f9a00000000-7f9a00021000 rw-p 00000000 00:00 0                # mmap region
7ffc12345000-7ffc12366000 rw-p 00000000 00:00 0  [stack]       # Stack

Process States

Process State Diagram

Process State Transitions:
══════════════════════════════════════════════════════════════

                    ┌─────────────┐
                    │    New      │
                    │  (created)  │
                    └──────┬──────┘
                           │ admitted
                           ▼
           ┌───────────────────────────────┐
           │                               │
           │    ┌─────────────────┐        │
interrupt  │    │      Ready      │◄───────┘
    │      │    │ (waiting for    │    I/O or event
    │      │    │  CPU)           │◄────completion
    │      │    └────────┬────────┘        │
    │      │             │                  │
    │      │   scheduler │ dispatch         │
    │      │             ▼                  │
    │      └──►┌─────────────────┐          │
    │          │    Running      │          │
    └──────────│ (executing on   │──────────┘
               │  CPU)           │   I/O or event wait
               └────────┬────────┘
                        │ exit
                        ▼
               ┌─────────────────┐
               │   Terminated    │
               │   (zombie)      │
               └─────────────────┘

Linux Process States (ps output):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
State   Code    Description
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
R       Running Currently executing or runnable (in run queue)
S       Sleep   Interruptible sleep (waiting for event)
D       Disk    Uninterruptible sleep (usually I/O)
T       Stopped Stopped by signal (e.g., SIGSTOP)
Z       Zombie  Terminated but parent hasn't read exit status
I       Idle    Kernel thread, idle

Process Control Block

The Process Control Block (PCB) is a data structure the kernel maintains for each process. It contains all information needed to manage and schedule the process.

PCB Contents

PCB Structure

Process Control Block (task_struct in Linux):
══════════════════════════════════════════════════════════════

┌─────────────────────────────────────────────────────────────┐
│                    Process Control Block                     │
├─────────────────────────────────────────────────────────────┤
│ IDENTIFICATION                                              │
│   • PID (Process ID): 1234                                 │
│   • PPID (Parent PID): 1                                   │
│   • UID/GID (User/Group): 1000/1000                        │
│   • Process name: "firefox"                                │
├─────────────────────────────────────────────────────────────┤
│ STATE INFORMATION                                           │
│   • Current state: Running / Ready / Blocked               │
│   • Priority: 20 (nice value)                              │
│   • CPU time used: 45.23 seconds                           │
│   • Scheduling class: SCHED_NORMAL                         │
├─────────────────────────────────────────────────────────────┤
│ CPU CONTEXT (saved on context switch)                       │
│   • Program Counter (RIP): 0x401234                        │
│   • Stack Pointer (RSP): 0x7ffc12345678                    │
│   • General registers: RAX, RBX, RCX, ...                  │
│   • Flags register (RFLAGS)                                │
│   • Floating-point state (FPU/SSE/AVX)                     │
├─────────────────────────────────────────────────────────────┤
│ MEMORY MANAGEMENT                                           │
│   • Page table pointer (CR3): 0x12345000                   │
│   • Memory limits: stack, heap boundaries                  │
│   • Memory map (VMAs): code, data, stack, mmap            │
├─────────────────────────────────────────────────────────────┤
│ FILE DESCRIPTORS                                            │
│   • Open file table pointer                                │
│   • stdin (0), stdout (1), stderr (2)                      │
│   • Network sockets, pipes, etc.                           │
├─────────────────────────────────────────────────────────────┤
│ SIGNAL HANDLING                                             │
│   • Pending signals bitmap                                 │
│   • Signal handlers: signal → handler function             │
│   • Blocked signals mask                                   │
├─────────────────────────────────────────────────────────────┤
│ ACCOUNTING                                                  │
│   • Start time: 2024-01-15 10:30:45                        │
│   • CPU time (user/system): 12.5s / 3.2s                   │
│   • I/O statistics: bytes read/written                     │
└─────────────────────────────────────────────────────────────┘

Context Switching

A context switch saves one process's state and restores another's, allowing the CPU to run multiple processes "simultaneously."

Context Switch Sequence:
══════════════════════════════════════════════════════════════

Process A running                   Process B waiting
     │                                    │
     ▼                                    │
┌─────────────────┐                       │
│ Timer interrupt │                       │
│ or syscall      │                       │
└────────┬────────┘                       │
         │                                │
         ▼                                │
┌─────────────────────────────────────────┼─────────────────┐
│                    KERNEL                │                 │
│                                          │                 │
│  1. Save Process A's context:           │                 │
│     • Push registers to A's kernel stack│                 │
│     • Save stack pointer in A's PCB     │                 │
│                                          │                 │
│  2. Scheduler selects Process B         │                 │
│                                          │                 │
│  3. Restore Process B's context:        │                 │
│     • Load B's page table (CR3)         │                 │
│     • Load B's stack pointer            │                 │
│     • Pop registers from B's kernel stack                 │
│                                          │                 │
└──────────────────────────────────────────┴─────────────────┘
                                                    │
                                                    ▼
                              ┌─────────────────────────────┐
                              │     Process B now running   │
                              │     (A is now "Ready")      │
                              └─────────────────────────────┘

Context Switch Cost:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Operation                              Time
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Save/restore registers                 ~1-2 μs
TLB flush (address space switch)       ~0-10 μs (ASID helps)
Cache pollution                        10-1000+ μs (indirect)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Direct cost: ~2-5 μs
Indirect cost (cache misses): Much higher!

Process Creation

On Unix/Linux, processes are created using the fork-exec model: fork() creates a copy of the parent, then exec() replaces it with a new program.

fork() System Call

fork() Creates a Clone

// fork() example - creates child process
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    printf("Before fork: PID = %d\n", getpid());
    
    pid_t pid = fork();  // Creates child process
    
    if (pid < 0) {
        // fork() failed
        perror("fork failed");
        return 1;
    }
    else if (pid == 0) {
        // CHILD process (fork returns 0 to child)
        printf("Child: PID = %d, Parent PID = %d\n", 
               getpid(), getppid());
        // Child does its work...
    }
    else {
        // PARENT process (fork returns child's PID)
        printf("Parent: PID = %d, Child PID = %d\n", 
               getpid(), pid);
        wait(NULL);  // Wait for child to exit
        printf("Parent: Child finished\n");
    }
    
    return 0;
}
Output:
Before fork: PID = 1234
Parent: PID = 1234, Child PID = 1235
Child: PID = 1235, Parent PID = 1234
Parent: Child finished
Copy-on-Write (COW): Modern systems don't actually copy all memory on fork(). Instead, parent and child share memory pages marked read-only. Only when either writes does the kernel copy that specific page. This makes fork() very fast!

exec() System Call

// exec() replaces process image with new program
#include <stdio.h>
#include <unistd.h>

int main() {
    printf("About to exec ls...\n");
    
    // Replace this process with /bin/ls
    // execl(path, arg0, arg1, ..., NULL)
    execl("/bin/ls", "ls", "-la", "/tmp", NULL);
    
    // If exec succeeds, this line NEVER executes!
    // The process is now running /bin/ls
    printf("This won't print unless exec failed\n");
    perror("exec failed");
    return 1;
}

// exec family variants:
// execl  - list of args  : execl("/bin/ls", "ls", "-l", NULL)
// execv  - array of args : execv("/bin/ls", argv)
// execlp - search PATH   : execlp("ls", "ls", "-l", NULL)
// execvp - search PATH   : execvp("ls", argv)
// execle - with env      : execle("/bin/ls", "ls", NULL, envp)
// execve - array + env   : execve("/bin/ls", argv, envp)

Fork-Exec Pattern

How Shells Launch Programs

// Shell-like process creation
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    char *command = "/bin/ls";
    char *args[] = {"ls", "-la", "/tmp", NULL};
    
    pid_t pid = fork();
    
    if (pid == 0) {
        // CHILD: Replace with new program
        execv(command, args);
        perror("exec failed");  // Only reached if exec fails
        _exit(127);
    }
    else if (pid > 0) {
        // PARENT: Wait for child
        int status;
        waitpid(pid, &status, 0);
        
        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n", 
                   WEXITSTATUS(status));
        }
    }
    
    return 0;
}
Fork-Exec Timeline:
══════════════════════════════════════════════════════════════

Shell (bash)                    fork()
PID 1000                          │
    │                             ▼
    │                    ┌────────────────┐
    │                    │ Child (PID 1001)│
    │                    │ Running bash   │
    │                    │ code (copy)    │
    │                    └───────┬────────┘
    │                            │ exec("/bin/ls")
    │                            ▼
    │                    ┌────────────────┐
    │  waitpid()         │ Child (PID 1001)│
    │  (waiting)         │ Now running ls │
    │       │            │ (new program!) │
    │       │            └───────┬────────┘
    │       │                    │ exit(0)
    │       │                    ▼
    │       │            (Child terminated)
    │       │                    │
    │◄──────┴────────────────────┘
    │   wait returns
    ▼
(Shell continues)

Process Termination

Exit Codes

Process Termination:
══════════════════════════════════════════════════════════════

Normal Termination:
• exit(status)   - Terminate with exit code
• return from main() - Same as exit(return_value)
• _exit(status)  - Immediate termination (no cleanup)

Abnormal Termination:
• Killed by signal (SIGKILL, SIGTERM, SIGSEGV, etc.)
• Uncaught exception
• abort() - Sends SIGABRT to self

Exit Code Conventions:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Code    Meaning
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
0       Success
1       General error
2       Misuse of shell command
126     Command found but not executable
127     Command not found
128+N   Killed by signal N (e.g., 137 = 128 + 9 = SIGKILL)

$ ls /nonexistent
ls: cannot access '/nonexistent': No such file or directory
$ echo $?
2

$ sleep 100 &
$ kill -9 $!
$ wait $!
$ echo $?
137   # 128 + 9 (SIGKILL)

Zombies & Orphans

Zombie and Orphan Processes

Zombie Process:
══════════════════════════════════════════════════════════════
A process that has terminated but whose parent hasn't called
wait() yet. The PCB remains so the parent can retrieve the
exit status.

Parent                          Child
   │                              │
   │                              │ exit(0)
   │                              ▼
   │                    ┌─────────────────┐
   │  (doesn't wait)    │ ZOMBIE (Z state)│
   │                    │ PID exists      │
   │                    │ Takes no resources│
   │                    │ Waiting for     │
   │                    │ parent's wait() │
   │                    └─────────────────┘

$ ps aux | grep Z
user   12345  0.0  0.0   0   0 ?  Z  10:30  0:00 [defunct]

Problem: Too many zombies can exhaust PID space.
Solution: Parent should always wait() for children.


Orphan Process:
══════════════════════════════════════════════════════════════
A process whose parent has terminated. The orphan is "adopted"
by init (PID 1) or a subreaper process, which will wait() for it.

Parent (PID 1000)              Child (PID 1001)
   │                              │
   │ exit(0)                      │
   ▼                              │
(Parent terminates)               │
                                  │
init (PID 1)                      │
   │                              │
   │◄───── reparented ────────────┤
   │                              │
   │                              │ exit(0)
   │                              ▼
   │◄─── wait() ────── (Orphan terminates cleanly)

Orphans are NOT zombies - they continue running normally,
just with a new parent (usually init).

Inter-Process Communication

Processes are isolated by design, but sometimes they need to communicate. The OS provides several IPC (Inter-Process Communication) mechanisms:

IPC Mechanisms

MechanismDescriptionUse Case
Pipes Unidirectional byte stream between related processes ls | grep foo
Named Pipes (FIFOs) Pipes with filesystem name, any process can connect Simple client-server
Shared Memory Memory region mapped into multiple processes High-speed data sharing
Message Queues Kernel-managed message passing Structured communication
Semaphores Synchronization primitive (not data transfer) Coordinate access
Sockets Network-style communication (local or remote) Client-server apps
Signals Asynchronous notifications SIGTERM, SIGINT, etc.
// Pipe example - parent writes, child reads
#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
    int pipefd[2];  // pipefd[0] = read end, pipefd[1] = write end
    char buf[100];
    
    pipe(pipefd);   // Create pipe before fork!
    
    if (fork() == 0) {
        // CHILD: Read from pipe
        close(pipefd[1]);  // Close unused write end
        read(pipefd[0], buf, sizeof(buf));
        printf("Child received: %s\n", buf);
        close(pipefd[0]);
    } else {
        // PARENT: Write to pipe
        close(pipefd[0]);  // Close unused read end
        char *msg = "Hello from parent!";
        write(pipefd[1], msg, strlen(msg) + 1);
        close(pipefd[1]);
        wait(NULL);
    }
    return 0;
}
// Output: Child received: Hello from parent!
IPC Choice Matters: Shared memory is fastest (no kernel involvement for data transfer) but requires manual synchronization. Pipes/sockets are simpler but involve kernel copies. Choose based on your needs!

Conclusion & Next Steps

Processes are the fundamental abstraction for running programs. We've covered:

  • Process Concept: Programs in execution with their own memory space and resources
  • Memory Layout: Text, data, BSS, heap, and stack segments
  • Process States: Running, ready, blocked, zombie, and the transitions between them
  • PCB: The kernel's data structure containing all process information
  • Fork-Exec: Unix model for process creation via clone + replace
  • Termination: Exit codes, zombies, and orphan handling
  • IPC: Pipes, shared memory, sockets for inter-process communication
Key Insight: Processes provide isolation—one process crashing doesn't affect others. This isolation comes at a cost (context switch overhead, IPC complexity), which is why threads (next topic) share an address space for faster communication.

Next in the Series

In Part 10: Threads & Concurrency, we'll explore how threads enable parallelism within a single process, threading models, pthreads API, and the challenges of race conditions and synchronization.