fork, exec and wait

os · Expert · 14 min

Debugging hangs and crashes in multiprocessing, daemons or servers requires knowing what fork actually copies.

On Unix, new processes are created in two steps:

  • os.fork() duplicates the current process. It returns 0 in the child and the child's PID in the parent. The child gets a copy-on-write copy of memory and duplicates of all open descriptors.
  • An os.exec* call (os.execv, os.execvp, os.execve...) replaces the current program image with another program. It never returns on success; the process keeps its PID and open inheritable descriptors.

The parent must reap children with os.waitpid(pid, 0), which returns (pid, status); decode with os.waitstatus_to_exitcode(status). Unreaped children linger as zombies. A child should finish with os._exit(code), not sys.exit, so it doesn't run the parent's cleanup handlers or flush its copied buffers twice.

The big trap: fork copies only the calling thread. If another thread held a lock (logging, malloc, an I/O buffer) at fork time, the child inherits it locked forever. Since 3.12, os.fork() emits a DeprecationWarning when the process has other threads, and in 3.14 multiprocessing on Linux defaults to the forkserver start method rather than fork. Use os.register_at_fork to reset state in children.

subprocess uses posix_spawn or fork+exec carefully for you; os.posix_spawn exposes the fast path directly. Windows has no fork.

Example

import os
import sys

if not hasattr(os, "fork"):
    sys.exit("Unix only")

sys.stdout.flush()                   # don't let the child re-flush our buffer
pid = os.fork()
if pid == 0:                         # child
    os.write(1, b"child running\n")
    os._exit(7)                      # skip parent's cleanup handlers
_, status = os.waitpid(pid, 0)       # parent reaps the child
print("exit code", os.waitstatus_to_exitcode(status))   # exit code 7

pid = os.fork()
if pid == 0:
    os.execvp("echo", ["echo", "replaced by echo"])  # never returns
os.waitpid(pid, 0)
print("done")                        # done

Remember

  • fork returns 0 in the child and the child's PID in the parent
  • exec* replaces the program in the same process; always waitpid your children
  • Fork copies only the calling thread, so forking a multithreaded process is unsafe

Watch out for

  • Calling sys.exit in a forked child; use os._exit to avoid running the parent's atexit handlers and flushing buffers twice.

Check yourself

  1. In a process with a background logging thread, why can a forked child deadlock?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/library/os.html#os.fork