What happens when you fork()?
posted on 20 May 2025Preamble
For a long time I’ve never understood how fork() works. Specifically, I didn’t understand what’s going on
this piece of code:
#include <stdio.h>
#include <unistd.h>
void gofork(void) {
int cpid = fork();
switch (cpid) {
case -1:
perror("fork");
exit(EXIT_FAILURE);
case 0:
puts("something for the child");
break;
default:
puts("all hail the mother earth. hail!");
break;
}
}
My confusion was how come I was seeing the output of the puts() calls – forgetting that fork() creates a
clone process. For instance, I wondered why (or how) the other cases except case -1 both execute instead of
just one of them.
The details
So what actually happens when fork()? Your program is cloned and have some of its properties updated, then
it continues the execution its own version of the calling function. That’s it.
I’m a Linux-based programmer, my explorations are thus targeted to work on Linux-compatible systems.
I think
forkis calledCreateProcessin WinAPI
- The current process is cloned verbatim using the
clonesystem call - Various process parameters are updated to ensure that the new process (child process) is unique,
for example:
- its process ID is set to the lowest available one
- its parent process ID is set to that forking process’s ID
- its pending signals are cleared
- its resource usages are set to 0 etc.
(see here
fork(2))
-
The child process continue to execute the calling function. In this case,
fork()return 0 (SUCCESS), so"something for the child"is printed, the parent gets the returned PID of the child:#include <stdio.h> #include <unistd.h> void gofork(void) { int cpid = fork(); switch (cpid) { case -1: perror("fork"); exit(EXIT_FAILURE); case 0: printf("detached pid=%d, ppid=%d, tid=0x%x\n", getpid(), getppid()); break; default: printf("cpid=%d [pid=%d] ppid=%d, tid=0x%x\n", cpid, getpid(), getppid()); break; } } - The child may be waited upon by the parent with
wait(2)orwaitpid(2)for reaping (clean up).
But what if I don’t want the child to inherit “anything” from the parent process?
That is, I want a “clean” address space for the child. The options are vfork(2), clone(2), clone3(2),
or execve*().
-
vfork(2),clone(2),clone3(2)are similar (tofork(2)) in that they create a new clean process, but accepts at least an pointer to the function to execute in the new process. They’re more involved to used. -
execve*()family of calls execute an executable binary (a program) with arguments or a (text) file starting with a shebang line like#! /path/to/interpreter/for/the/body/of/text/herevia the system’s command runner, which is/bin/sh -con *NIX systems
Uses of fork and examples
If you’ve ever used any program that runs other programs, there’s a fork happening there somewhere.
For a (manufactured) example using ps command:
$ ps --forest | wl-copy
PID TTY TIME COMMAND
2761 tty8 136:07 /usr/bin/kitty
3421 tty8 0:07 \_ /usr/bin/kitten __atexit__
13854 pts/3 0:23 \_ kak /home/saed/.config/kitty/kitty.conf
16901 pts/5 0:01 \_ /usr/bin/nu
17522 pts/5 0:06 | \_ kitten choose-fonts
17537 pts/5 0:02 | \_ /usr/bin/kitty +runpy from kittens.choose_fonts.backend import main; main()
17894 pts/4 0:00 \_ /usr/bin/nu
18471 pts/4 0:03 \_ /home/saed/.cargo/bin/hx
348 pts/18 0:00 nu
2296 pts/18 0:02 \_ zsh
13927 pts/18 0:00 \_ ps
13928 pts/18 0:00 \_ wl-copy
9418 pts/18 0:00 wl-copy
Program to sum numbers in range 0-9 over UNIX pipes
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void) {
int pipefd[2];
pid_t cpid;
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
const int numbers[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
const int isz = sizeof(numbers[0]);
const int n = sizeof(numbers) / isz;
if (cpid == 0) {
// in the child, the write end is unused
// so close it
close(pipefd[1]);
char buf[sizeof(numbers[0])] = {0};
int sum = 0;
while (read(pipefd[0], buf, isz) > 0) {
const int num = *(int *)buf;
dprintf(STDOUT_FILENO, "[child] received %d\n", num);
sum += num;
}
dprintf(STDOUT_FILENO, "[child] sum = %d\n", sum);
// we're done close our read end
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else {
// in the parent, the read end is unused
// so close it
close(pipefd[0]);
for (int i = 0; i < n; i += 1) {
const char *num = (const char *)&numbers[i];
printf("[parent] pushing: %d\n", numbers[i]);
write(pipefd[1], num, isz);
}
// done
close(pipefd[1]); /* Reader will see EOF */
wait(NULL); /* Wait for child */
exit(EXIT_SUCCESS);
}
}
~/blogs/atfork $ cc -o rd rd.c
~/blogs/atfork $ ./rd
[parent] pushing: 1
...
[parent] pushing: 0
[child] received 1
...
[child] received 0
[child] sum = 45
Other interesting ideas:
- implement
fork()withclone(), possible if one can examine the states/attributes of the current thread for the anything about the program counter or something
TODO: I want to implement something like
strace()but I haven’t figured out how to examine the arguments to each syscall