systems-programming Lesson 1 22 min read

What Are System Calls and How Do Processes Fork?

fork(), execlp(), wait() — creating child processes and understanding how every program is launched

Reading: Linux Text: Ch. 10 §1–6 (pp. 131–144, Processes and System Calls)

After this lesson, you will be able to:

  • Explain what fork() does and how the return value distinguishes parent from child
  • Write the standard fork/if-else pattern for different parent and child behavior
  • Use execlp to replace a child process’s image with a new program
  • Call wait() to collect exit status and prevent zombie processes
  • Describe fork-exec-wait as the mechanism the shell uses for every command

Full Circle

Back in Lesson 2.2, you learned that every command you type creates a process: bash creates a child, the child replaces itself with the new program, bash waits for it to finish. You saw this from the outside — ps, jobs, kill.

Now you’re going to write C code that does this from the inside. You’ll call the actual system calls that the shell uses. This is the capstone of the course — C talking directly to the operating system.


How fork() Works

Creating a Copy of Your Process

fork() creates a new process by duplicating the calling process:

Before you read further: look at the code below and predict what it outputs. How many times does the printf after fork() run? Which process prints which line? Then read the explanation.

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

int main(void)
{
    printf("Before fork: PID = %d\n", getpid());

    pid_t pid = fork();

    if (pid < 0)
    {
        perror("fork failed");
        return 1;
    }
    else if (pid == 0)
    {
        // CHILD process
        printf("Child:  PID = %d, Parent PID = %d\n", getpid(), getppid());
    }
    else
    {
        // PARENT process
        printf("Parent: PID = %d, Child PID = %d\n", getpid(), pid);
        wait(NULL);    // Wait for child to finish
    }

    return 0;
}
Before fork: PID = 1234
Parent: PID = 1234, Child PID = 1235
Child:  PID = 1235, Parent PID = 1234

Key Insight: After fork(), there are two processes running the same code. The return value distinguishes them: fork() returns 0 in the child and the child’s PID in the parent. This is why the if/else pattern is standard — it separates parent and child behavior.

Check Your Understanding
After a successful fork(), how many processes are executing the code that follows?
A Two — the original parent and the new child, both starting from the line after fork()
B One — the parent pauses while the child runs
C Two — but the child starts from the beginning of main
D One — the child replaces the parent
Answer: A. fork() creates a copy of the current process. Both processes continue from the point immediately after the fork() call. Option B is wrong — both run concurrently (or the OS schedules them). Option C is wrong — the child doesn't restart from the beginning. Option D describes exec, not fork.

The fork() Return Value

Return Value You Are
< 0 Error (fork failed)
== 0 The child process
> 0 The parent process (value is child’s PID)

Replacing with a New Program

The child is a copy of the parent — same code, same variables. To run a different program, use the execlp family:

if (pid == 0)
{
    // Child: replace yourself with "ls -l"
    execlp("ls", "ls", "-l", NULL);
    // If we get here, the replacement failed
    perror("failed to launch program");
    _exit(1);
}

execlp replaces the current process image with a new program. If it succeeds, it never returns — the old code is gone, replaced by ls. If it fails, it returns and you handle the error.

Waiting for the Child

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

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

wait() blocks until a child finishes. Without it, the parent might exit first, leaving an orphan process.

Check Your Understanding
What happens if execlp("ls", "ls", "-l", NULL) succeeds?
A execlp returns 0 and the next line of your code runs
B execlp never returns — the current process's code is completely replaced by ls
C execlp runs ls in a new child process and returns immediately
D execlp returns the PID of the new process
Answer: B. execlp replaces the entire process image — your code, variables, stack, everything — with the new program. If it succeeds, there's nothing to return to. Any code after execlp only runs if the call fails (bad path, missing program, no permission). This is why the perror after execlp is an error handler, not normal flow.
Why does this matter?

The fork-exec pattern is how every Unix program is launched. When you type gcc in the terminal, the shell forks a child and execs gcc into it. Understanding this means understanding how your operating system actually runs programs — and it’s the foundation for OS courses.

The Complete Pattern

This is how the shell runs every command you type:

pid_t pid = fork();
if (pid == 0)
{
    // Child: run the command
    execlp(command, command, arg1, arg2, NULL);
    perror("launch failed");
    _exit(1);
}
else
{
    // Parent: wait for the command to finish
    wait(NULL);
}
Check Your Understanding
A parent calls fork() but never calls wait(). The child finishes and exits. What is the child process now called?
A An orphan process
B A daemon process
C A zombie process
D A background process
Answer: C. A zombie is a process that has exited but whose exit status hasn't been collected by its parent via wait(). It still occupies a slot in the process table. An orphan is different — that's a child whose parent has exited (adopted by init). Daemons and background processes are still running.

From Java: Java’s ProcessBuilder and Runtime.getRuntime() do the same thing — create a child process and run a program. The difference: Java wraps it in an object-oriented API with exception handling. C gives you the raw system calls. Understanding the C version means understanding what Java’s process API actually does underneath.

Big Picture: fork() and the exec family are the foundation of Unix. Every program you’ve ever run — every ls, gcc, grep — was launched this way. The shell is just a loop: read a command, fork, replace with the new program, wait, repeat.

Quick Check: What does fork() return to the child process?

Zero. The parent receives the child’s PID (a positive number). This is how the code tells the two processes apart — the child gets 0, the parent gets the child’s PID.

Quick Check: Why does execlp never return on success?

Because it replaces the entire process image with a new program. The calling code is gone — overwritten by the new program’s code. Only if the replacement fails (bad path, no permission) does it return, at which point you handle the error.

Quick Check: What happens if the parent doesn't call wait()?

The child becomes a zombie when it exits — it’s done running but its exit status isn’t collected. Zombies consume a process table entry. If the parent exits first, the child becomes an orphan, adopted by the init process.


What’s Next

You can create processes and launch programs. But how do processes talk to each other? In the next lesson, you’ll learn pipe() and signal() — building the shell’s | operator in C, and handling signals like SIGINT programmatically.