ProcessesIntermediate

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

Strong green flag · Explains the split as a design choice — the child configures itself in the parent’s own language.
Green 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
Red flags
  • 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

F1
Why can a 10 GB JVM fail to spawn ls on a machine with 4 GB free?
F2
What goes wrong if a multi-threaded process forks and the child does not exec immediately?
F3
A server restart says "address already in use" although the old process is gone. Why?

Scenario

A Node.js API calls 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.

Learn this topic