student@ubuntu:~$
systems-programming Lesson 16 10 min read

Processes: fork & exec

How Unix creates new processes — and why every program you run starts with fork

Reading: Supplemental (not in textbook)

Quick check before you start: When you type ls in the terminal, what actually happens? If you are not sure, read on. If you can describe fork/exec, skip to wait() and Zombie Prevention.

Practice this topic: Fork & Exec skill drill

After this lesson, you will be able to:

  • Explain what fork() returns in the parent vs. the child
  • Use execlp to replace a process with a new program
  • Use wait() to collect a child’s exit status
  • Describe how zombie processes happen and how to prevent them

System Calls vs Library Functions

Throughout this course, you have used C library functions like printf and calloc. Under the hood, these call system calls — the actual interface to the kernel. Library functions add convenience; system calls do the real work.

Library function Underlying system call Purpose
fopen open Open a file
printf write Write to a file descriptor
calloc sbrk / mmap Allocate memory
fclose close Close a file descriptor
exit _exit Terminate the process

You can look up either layer in the manual. Man page sections tell you which layer you are reading:

  • Section 2 — system calls (man 2 open)
  • Section 3 — library functions (man 3 printf)

If man printf shows the shell builtin instead of the C function, use man 3 printf to get the right page. From this lesson forward, you will work directly with system calls.


How Unix Creates Processes

Every process in Unix is created the same way: an existing process calls fork(). The kernel copies the current process, creating a parent-child pair. Then the child typically calls exec to replace itself with a different program.

This is how your shell runs every command. When you type ls:

  1. The shell calls fork() — now there are two copies of the shell
  2. The child calls execlp("ls", "ls", NULL) — the child becomes ls
  3. The parent calls wait() — it pauses until the child finishes
  4. The shell prints a new prompt

fork() Return Value

fork() returns different values depending on which process you are in:

Process Return value
Parent The child’s PID (a positive integer)
Child 0
Error -1 (fork failed)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return 1;
    } else if (pid == 0) {
        // CHILD process
        printf("I am the child, PID = %d\n", getpid());
    } else {
        // PARENT process
        printf("I am the parent, child PID = %d\n", pid);
        wait(NULL);
    }
    return 0;
}

After fork(), both processes execute the same code. The return value is how you tell them apart.

execlp: Replacing the Process

fork() gives you a copy. execlp replaces that copy with a different program:

if (pid == 0) {
    execlp("ls", "ls", "-l", NULL);
    // If we get here, exec failed
    perror("execlp");
    exit(EXIT_FAILURE);
}

The arguments to execlp:

  1. The program name (searched in PATH)
  2. argv[0] (by convention, the program name again)
  3. Additional arguments
  4. NULL to terminate the list

After a successful exec, the child process is gone — it has been replaced by ls. The code after execlp only runs if exec fails.

wait() and Zombie Prevention

When a child exits, it becomes a zombie — a dead process that still has an entry in the process table. The parent must call wait() to collect the child’s exit status and remove the zombie.

int status;
pid_t child = wait(&status);

if (WIFEXITED(status)) {
    printf("Child %d exited with status %d\n",
           child, WEXITSTATUS(status));
}

If the parent never calls wait(), zombies accumulate. In a long-running server, this eventually fills the process table and prevents new processes from being created.

The Complete Pattern

pid_t pid = fork();
if (pid < 0) {
    perror("fork");
    exit(EXIT_FAILURE);
} else if (pid == 0) {
    // Child: run a different program
    execlp("ls", "ls", "-la", NULL);
    perror("execlp");
    exit(EXIT_FAILURE);
} else {
    // Parent: wait for child
    int status;
    wait(&status);
    printf("Child finished\n");
}

This is the fundamental pattern for process creation in Unix. Every shell, every server, every process manager uses some variation of fork + exec + wait.


Check Your Understanding
After a successful fork(), what does the return value look like in the parent and child?
AParent gets 0, child gets the parent's PID
BParent gets the child's PID, child gets 0
CBoth get the same PID
DParent gets 1, child gets -1
Answer: B. The parent receives the child's PID (a positive integer) so it can track the child. The child receives 0, which is how the child knows it is the child. This asymmetry is the mechanism for branching logic after fork.

What Comes Next

You can create processes. Next, you will learn how processes communicate — using pipes to connect one program’s output to another’s input, and signals to interrupt running processes.

Next: Pipes & Signals