fork, exec and why they are separate
“Walk me through what happens when a shell runs `ls > out.txt`. What do fork and exec each do, and why are they two calls instead of one?”
What this tests
- The semantics of fork: a copy that returns twice, copy-on-write
- What exec replaces and what it preserves (PID, descriptors)
- Why the split exists: the child configures itself before exec
- Cost and the hazards: large-process fork, multi-threaded fork, descriptor inheritance
Answers by level
Read the beginner answer first and notice what is missing.
fork() asks the kernel for a new process that is a near-identical copy of the caller: same code, same open descriptors, same environment, a new PID. The kernel does not copy the memory; it copies the page tables and marks every writable page copy-on-write, so the fork of a large process takes milliseconds rather than seconds and pages are only duplicated when one side writes to them (Copy-on-Write). fork returns twice: the child sees 0, the parent sees the child’s PID.
Between fork and exec the child is still running shell code — and that is the point of the split. It can rearrange its own world before the new program arrives: open("out.txt") and dup2() it onto descriptor 1 for >, wire a pipe for |, close descriptors the child should not inherit, reset signal handlers, change the process group. Then execve("/bin/ls", argv, envp) throws away the address space and loads the new image. The PID stays, the open descriptors stay (unless marked O_CLOEXEC), the redirections stay — ls simply finds that its stdout is a file. The parent calls waitpid() and collects the exit status (Creating Processes: fork, exec, wait).
Doing it as one call would mean the kernel API has to accept every possible pre-launch configuration as parameters. Windows CreateProcess takes that approach — no fork, a large struct describing handles, environment and window settings — and posix_spawn is the POSIX equivalent for cases where the flexibility is not needed and the fork cost matters.
The failure implications: descriptors that were not CLOEXEC leak into every child, which is how a restarted server finds its port "already in use" — an old child still holds the listening socket. And fork from a process with many threads copies only the calling thread; any lock another thread held is held forever in the child, so the child may only call async-signal-safe functions until exec.
Green flags · Red flags
- Says fork copies page tables and marks pages copy-on-write
- Explains that redirection is set up by the child between fork and exec with dup2
- Knows exec preserves PID and descriptors and replaces the address space
- Mentions CLOEXEC or descriptor inheritance as a hazard
- Can contrast with CreateProcess / posix_spawn
- Believes fork physically copies all memory
- Thinks exec creates a new process
- Cannot explain how
>ends up as descriptor 1 of the child
Follow-up questions
ls on a machine with 4 GB free?Scenario
child_process.exec("convert …") per request under load. Memory balloons and some spawns fail with ENOMEM. Explain what fork is doing here and what you would change.