What Are System Calls and How Do Processes Fork?
fork(), execlp(), wait() — creating child processes and understanding how every program is launched
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-elsepattern for different parent and child behavior - Use
execlpto 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
printfafterfork()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 theif/elsepattern is standard — it separates parent and child behavior.
fork(), how many processes are executing the code that follows?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.
execlp("ls", "ls", "-l", NULL) succeeds?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);
}
fork() but never calls wait(). The child finishes and exits. What is the child process now called?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
ProcessBuilderandRuntime.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 — everyls,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.