Part I · How it works
1.5 How a program starts: fork and exec
alsoforkexecexecvecloneprocess creationcopy on writehow a program starts
TL;DR
- A new process starts as a copy of an existing one:
fork()duplicates the process that calls it. exec()then replaces the copy's program with another: the kernel loads the new ELF file and its loader (23.2).- Your shell does both for every command you type. Every process descends from PID 1.
pstree -p $$ # your shell and what it started
strace -f -e trace=clone,clone3,execve bash -c 'ls; true'
On your machine
$ atfq 1.5 Kernel 6.12.48-amd64, built with gcc 14.2.0 Build #1 SMP PREEMPT_DYNAMIC Debian 6.12.48-1 (2025-09-20) Work in the 3 days, 4 h since boot: 912.3 million context switches (3,330 per second), 1.2 million processes started, 51.2 million interrupts Now 3 tasks running or ready, 1 waiting on I/O Disk FS ext4, vfat, btrfs
An example: Alice's laptop, one of the test machines. In your terminal, this block shows your own machine, read live.
Pitfalls
forkcopies no memory up front: pages are shared until one side writes to them (copy-on-write), so forking even a big process is cheap.- After
exec, the PID stays the same: it's the same process running another program. - Environment variables and open files survive
exec: that's howexportand redirections reach the program (27.3). - A parent that never waits for its children leaves zombies (16.1).
Deep dive 2 questions
Why two steps instead of one?
Between fork and exec, the child still runs the parent's code, so it
can prepare its own setup: open a file as its output, change folder, drop
privileges, set limits. Then it becomes the new program, which inherits
all of that. That's how the shell handles cmd > file without cmd
knowing anything about it.
What does the kernel do in exec?
It reads the start of the file. For an ELF program, it builds a fresh
address space, maps the program's code and data into it, also maps the
dynamic loader when the program needs one, puts the arguments and the
environment on a new stack, and jumps to the entry point. For a file
starting with #!, it runs the interpreter named there instead.