What fork() does
On Linux, every program runs inside a process: a running instance with its own memory, open files and process ID (PID). The fork() system call creates a new process by duplicating the one that calls it. The caller is the parent and the new process is the child.
The strange part is that fork() is called once but returns twice, once in each process:
- In the parent, it returns the PID of the new child.
- In the child, it returns
0. - On failure, it returns
-1in the parent, no child is created, anderrnois set.
Your code uses that return value to decide which process it is and what each one should do. This is how a shell starts every command you type.
Your first fork
The program below creates a child. The child prints its PID and exits with status 7. The parent waits for it and reads that status.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) {
/* child */
printf("child: pid=%d parent=%d
", getpid(), getppid());
exit(7);
}
/* parent */
printf("parent: pid=%d created child %d
", getpid(), pid);
int status;
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
return EXIT_FAILURE;
}
if (WIFEXITED(status))
printf("parent: child exited with status %d
", WEXITSTATUS(status));
return EXIT_SUCCESS;
}
gcc -Wall -Wextra -o fork_demo fork_demo.c ./fork_demo
child: pid=4243 parent=4242
parent: child exited with status 7
Your PIDs will differ. The order of the first two lines can also change between runs, because the kernel does not guarantee whether the parent or the child runs first. Never write code that depends on it. If you need an order, use wait or another form of synchronization.
Separate memory, copy-on-write
The child gets a copy of the parent’s memory: variables, the heap and the stack. After the fork the two processes live in separate address spaces, so a change in one is never seen by the other.
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
int counter = 100;
pid_t pid = fork();
if (pid == 0) {
counter = 200;
printf("child: counter=%d
", counter);
return 0;
}
wait(NULL);
printf("parent: counter=%d
", counter);
return 0;
}
parent: counter=100
Copying all memory on every fork would be slow, so Linux uses copy-on-write. At first, parent and child share the same physical pages, marked read-only. The kernel makes a private copy of a page only when one of the processes writes to it. According to the Linux man page, the real cost of fork() is therefore just the time and memory needed to duplicate the parent’s page tables and create a new task structure for the child.
What the child inherits
The child is an almost exact duplicate of the parent. It inherits things like your variables, environment, current directory and open file descriptors. A few details matter in practice:
- Different IDs. The child has its own unique PID, and its parent PID is the PID of the process that forked it.
- Shared open files. Inherited file descriptors point to the same open files as the parent’s, so they share the file offset. If both write to the same file, their output interleaves instead of overwriting.
- One thread only. The child has a single thread: the one that called
fork(), even if the parent had many. - Things not inherited. Pending signals, timers, memory locks and resource usage counters start fresh in the child.
- Exit signal. When the child terminates, the parent receives
SIGCHLD.
Read the manualRun man 2 fork for the full list of what is and isn’t inherited. It also lists the possible errors, such as EAGAIN when a process or memory limit is reached.
fork and exec
A copy of the same program is rarely what you want. Usually you want to run a different program. So the child calls one of the exec functions, which replaces its memory with a new program. The PID stays the same. This fork-then-exec pair is how shells launch commands.
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return EXIT_FAILURE;
}
if (pid == 0) {
execlp("ls", "ls", "-l", (char *)NULL);
perror("execlp"); /* only reached if exec failed */
_exit(127);
}
int status;
waitpid(pid, &status, 0);
return WIFEXITED(status) ? WEXITSTATUS(status) : EXIT_FAILURE;
}
If exec succeeds it never returns, so any code after it runs only on failure. In that case the child should call _exit, not exit, so it doesn’t flush buffered data that belongs to the parent.
wait, zombies and orphans
When a child ends, the kernel keeps a small record of it, including its exit status, until the parent asks for it with wait or waitpid. Two special states come from this:
- Zombie. A child that has exited but hasn’t been waited for. It uses almost no memory, but it still holds its PID. Many zombies can use up the process table. The fix is for the parent to call
wait, or to exit itself. - Orphan. A child whose parent exited first. The kernel gives it a new parent, normally
init(PID 1) or a designated subreaper, which then cleans it up.
You can see a zombie yourself. Write a parent that forks, then sleeps for 30 seconds without calling wait while the child exits right away. In another terminal, run:
ps -o pid,ppid,stat,cmd --ppid <parent-pid>
5121 5120 Z+ [zombie] <defunct>
The Z in the STAT column means zombie. Once the parent exits or reaps the child, the entry disappears.
Common mistakes
Forgetting to check for -1
If fork() fails and you don’t check, -1 is non-zero, so your code treats it like a parent. Always test for failure first.
Duplicated buffered output
The stdio buffer is part of the process memory, so it gets copied too. Look at this program:
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("hello
");
fork();
return 0;
}
On a terminal, stdout is line-buffered, so hello prints once. When the output goes to a pipe or file, it is fully buffered. The text is still waiting in the buffer when you fork, and both processes flush it on exit:
hello
$ ./buf | cat
hello
hello
Call fflush(stdout) before fork() to avoid this.
Forking in a loop without control
A loop that forks, where the children also keep looping, grows exponentially and can freeze a machine. Make sure only the parent continues the loop and each child exits or calls exec. Practice in a virtual machine.
Forking in a multithreaded program
Only the calling thread exists in the child, but it inherits locks held by other threads at that moment. Until the child calls exec, keep it to simple, async-signal-safe operations.
Where to go next
You now know the full life cycle: fork creates the child, exec optionally replaces its program, and wait collects its exit status. Good next steps:
- Connect parent and child with
pipe()and send data between them - Handle
SIGCHLDto reap children without blocking - Read about
vfork()andclone(), which Linux uses underneathfork() - Build a tiny shell that reads a line, forks, and runs it