Processes: Address Spaces, the PCB, States, fork, exec and wait

Concept

Program vs Process A program is a file. A process is a program in execution: its own virtual address space, one or more threads, open files, credentials, signal handlers and accounting — everything the kernel tracks in its process control block (PCB) (task_struct in Linux).

PID, parent PID, user and group IDs Saved registers, program counter, stack pointer (for context switches) Page tables / memory map

Open file descriptors, current directory State, priority, CPU time used

Linux shows them as R (running/runnable), S (interruptible sleep), D (uninterruptible sleep, usually disk I/O), T (stopped), Z (zombie).

A context switch saves the CPU state of one thread into its PCB and restores another's — pure overhead, typically a few microseconds including cache effects.

Creating Processes on Unix Creates a copy of the calling process; returns 0 in the child and the child's PID in the parent. Memory is shared copy-on-write until one side writes

Replaces the current program with a new one (same PID); returns only on error

Parent waits for a child and collects its exit status Terminates; the process stays a zombie until its parent waits

A zombie is a finished process whose exit status hasn't been collected; an orphan is a running process whose parent died — it is re-parented to init (PID 1), which reaps it. Windows uses a single CreateProcess call instead of fork + exec.

flowchart LR
    NEW["New"] --> READY["Ready"]
    READY -->|"scheduled"| RUN["Running"]
    RUN -->|"preempted"| READY
    RUN -->|"waits for I/O or event"| BLOCK["Blocked / Waiting"]
    BLOCK -->|"event occurs"| READY
    RUN -->|"exit"| ZOMBIE["Terminated (zombie until reaped)"]

Why It Matters

Shells, servers, build systems, test runners and supervisors (systemd) are all built on fork/exec/wait. Zombie leaks, fork bombs and "why does my child modify its copy of a variable but the parent doesn't see it" are classic problems.

Syntax

pid_t pid = fork();
if (pid == 0) { execvp(argv[0], argv); _exit(127); }
waitpid(pid, &status, 0);
if (WIFEXITED(status)) code = WEXITSTATUS(status);

Example

10.1 fork: two processes, two copies of the data 10.2 fork + exec + wait: a tiny shell

10.3 Observing process states, including a zombie

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int counter = 100;

int main(void)
{
    printf("parent: pid %d, counter %d\n", getpid(), counter);
    fflush(stdout);                         /* don't duplicate buffered output in the child */

    pid_t pid = fork();                     /* returns twice: 0 in the child, child's pid in the parent */
    if (pid < 0) { perror("fork"); return 1; }

    if (pid == 0) {
        counter += 1;                       /* modifies the CHILD's copy only (copy-on-write) */
        printf("child : pid %d, parent %d, counter %d at %p\n", getpid(), getppid(), counter, (void *)&counter);
        exit(3);                            /* exit status for the parent */
    }

    int status;
    waitpid(pid, &status, 0);               /* reap the child: no zombie left behind */
    printf("parent: child %s with status %d, my counter still %d at %p\n",
           WIFEXITED(status) ? "exited" : "died", WEXITSTATUS(status), counter, (void *)&counter);
    return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

/* A tiny shell: fork + exec + wait, the way every shell runs a command */
static int run(char *const argv[])
{
    pid_t pid = fork();
    if (pid == 0) {
        execvp(argv[0], argv);                /* replaces the child's program; returns only on error */
        perror("execvp");
        _exit(127);
    }
    int status;
    waitpid(pid, &status, 0);
    return WEXITSTATUS(status);
}

int main(void)
{
    char *echo[] = {"echo", "hello from a new program", NULL};
    char *count[] = {"sh", "-c", "exit 5", NULL};
    char *bad[] = {"no-such-program", NULL};
    printf("echo returned %d\n", run(echo));
    printf("sh -c 'exit 5' returned %d\n", run(count));
    fflush(stdout);
    printf("missing program returned %d\n", run(bad));
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

static void show_state(pid_t pid, const char *label)
{
    char path[64], line[128];
    snprintf(path, sizeof path, "/proc/%d/status", pid);
    FILE *f = fopen(path, "r");
    while (f && fgets(line, sizeof line, f))
        if (strncmp(line, "State:", 6) == 0) printf("%-28s %s", label, line);
    if (f) fclose(f);
}

int main(void)
{
    pid_t sleeper = fork();
    if (sleeper == 0) { sleep(5); _exit(0); }
    pid_t quick = fork();
    if (quick == 0) _exit(0);                   /* exits immediately */

    usleep(200000);
    show_state(getpid(), "parent (running this code):");
    show_state(sleeper, "child sleeping in sleep():");
    show_state(quick, "child that exited, unreaped:");
    waitpid(quick, NULL, 0);                    /* reaping removes the zombie */
    char path[64];
    snprintf(path, sizeof path, "/proc/%d", quick);
    printf("after waitpid, /proc/%d exists: %s\n", quick, access(path, F_OK) == 0 ? "yes" : "no");
    kill(sleeper, 9);
    waitpid(sleeper, NULL, 0);
    return 0;
}

Expected Result

The child changed counter to 101 but the parent still sees 100 — at the same virtual address. After fork the two processes have separate address spaces; copy-on-write gave the child its own physical page when it wrote. (PIDs and addresses differ on every run.)

A missing program made execvp fail; the child printed the error and exited with 127, the shell convention for "command not found". (Its error message went to stderr and appears last because stderr and stdout were captured separately.)

The exited but unreaped child was a zombie (Z); waitpid removed it completely.

parent: pid 50, counter 100
child : pid 51, parent 50, counter 101 at 0x56e2ee70f010
parent: child exited with status 3, my counter still 100 at 0x56e2ee70f010
hello from a new program
echo returned 0
sh -c 'exit 5' returned 5
missing program returned 127
execvp: No such file or directory
parent (running this code):  State:	R (running)
child sleeping in sleep():   State:	S (sleeping)
child that exited, unreaped: State:	Z (zombie)
after waitpid, /proc/82 exists: no

Common Mistakes

Forgetting to wait for children → zombies accumulate.

Calling exit() instead of _exit() in a child after a failed exec (flushes the parent's duplicated stdio buffers twice).

Not flushing stdout before fork → buffered output appears twice.

Assuming the child and parent share variables after fork.

Calling fork in a loop without checking the return value (fork bomb).

Best Practices

Always check fork/exec return values and reap children (or ignore SIGCHLD deliberately).

Use posix_spawn when you only need "run a program" — it can avoid copying the parent's page tables.

Close file descriptors the child doesn't need (O_CLOEXEC).

Interview Relevance

Very high: "process vs program?", "what does fork return?", "what is a zombie/orphan?", "what is in the PCB?", "what is a context switch?", "how does a shell run a command?", "what is copy-on-write?".

Practice Questions

How many processes does fork(); fork(); fork(); create in total? Verify with a program.

Write a program that creates an orphan and shows its new parent PID.

Extend the tiny shell to support cmd > file redirection with dup2.

Measure how long fork takes for a process with a 1 GB heap vs a small one.

Quick Quiz

Q1. (Multiple choice) What does fork() return in the child? A) The child's PID B) The parent's PID C) 0 D) −1 Answer: C.

Q2. (Predict the output) After the child increments counter from 100 to 101, what does the parent print?

Answer: 100 (verified) — separate address spaces.

Q3. (True/False) A zombie process still uses CPU time.

Answer: False. It has finished; only its PCB entry (PID, exit status) remains until reaped.

Q4. (Find the bug) printf("start "); fork(); printf("end\n"); prints start twice.

Answer: The unflushed printf buffer is duplicated by fork; call fflush(stdout) before forking.

Coding Challenge

Write a program that forks three children; each child exits with a different status (1, 2, 3); the parent reaps all of them and prints each child's status.

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    pid_t kids[3];
    for (int i = 0; i < 3; i++) {
        kids[i] = fork();
        if (kids[i] == 0) _exit(i + 1);
    }
    for (int i = 0; i < 3; i++) {
        int status;
        waitpid(kids[i], &status, 0);
        printf("child %d exited with status %d\n", i, WEXITSTATUS(status));
    }
    return 0;
}