HN Debrief

Moving beyond fork() + exec()

  • Infrastructure
  • Programming
  • Operating Systems
  • Developer Tools

The post is about an LWN article on attempts to move Linux beyond the traditional Unix model of creating a process by first cloning the parent with fork() and then replacing it with exec(). That model dates back to tiny early Unix machines, where it was a clever way to start a new program without needing both parent and child fully resident in memory at once. On modern systems, the common case is not "make a copy of me" but "start a different program with a small, explicit set of inherited state". The article argues Linux still lacks a first-class kernel primitive for that cleaner case.

If your software frequently launches subprocesses from large or multi-threaded parents, treat process creation as a design concern rather than a cheap primitive. Prefer spawn-style APIs where your platform has them, and watch Linux kernel work here because better process startup semantics would reduce latency, simplify sandboxing, and make process isolation cheaper to use in hot paths.

Discussion mood

Mostly supportive of moving past fork as the default process-launch mechanism. The mood was pragmatic, not ideological: people see fork as a brilliant old hack that now leaks too much state, interacts badly with threads and huge address spaces, and forces workarounds like zygotes, while still wanting to preserve it for the small set of cases where cloning really is the point.

Key insights

  1. 01

    Fork bakes VM and threading policy into the OS

    The deeper objection is not raw speed. A fast fork pushes the kernel toward copy-on-write everywhere, generous overcommit, page table churn, and expensive TLB shootdowns on multicore systems. That is a lot of memory-management policy to inherit just because applications want to launch a small helper process. Once parents are huge JVMs or servers with tens of gigabytes mapped, the abstraction itself is the problem.

    If you are designing runtimes, containers, or operating systems, separate "start a new executable" from "snapshot my current process" in your own abstractions. Treat them as different primitives with different memory and threading costs.

      Attribution:
    • mort96 #1
    • vbezhenar #1
    • thayne #1
    • tliltocatl #1
  2. 02

    Threads make post-fork setup fundamentally brittle

    The ugly part of fork is what happens before exec in a multi-threaded program. Only the calling thread remains alive in the child, but the child's memory still contains locks, heap metadata, and partial updates left behind by vanished sibling threads. That is why the safe set of library calls between fork and exec gets so small in practice. vfork helps with memory overhead, but not with the need for careful, constrained setup logic.

    If your parent process is multi-threaded, keep child setup minimal and async-signal-safe, or move process launching into a dedicated helper process. Do not assume arbitrary library code is safe between fork and exec.

      Attribution:
    • asveikau #1
    • josefx #1
    • cryptonector #1
  3. 03

    posix_spawn is only useful when the kernel really supports it

    A lot of the argument collapses into whether posix_spawn is just a nicer wrapper or an actual primitive. On Linux it is commonly implemented on top of clone or fork plus exec, so it cannot escape the underlying baggage. On macOS and NetBSD, native support gets much closer to the semantics people actually want, which makes posix_spawn look less like API sugar and more like the missing process-creation model.

    When benchmarking or choosing subprocess APIs, check how your target OS implements posix_spawn instead of assuming the name tells you the cost model. The same API can mean very different things across platforms.

      Attribution:
    • toast0 #1
    • JdeBP #1
    • plorkyeran #1
    • dcrazy #1
  4. 04

    Zygotes are a workaround, not a clean replacement

    Fork still shines when you deliberately want to preserve expensive initialization, which is why Chrome, Android, and older systems like kdeinit use zygote-style preforking. But that pattern only works if you plan for it early, keep the zygote in a carefully controlled state, and avoid features like threads that ruin the model. That makes it a valuable optimization for a few systems, not a general answer to subprocess creation.

    Use zygotes only when you can control process lifetime and initialization from the start. Do not count on them as a drop-in fix for arbitrary subprocess overhead in a mature codebase.

      Attribution:
    • pizlonator #1
    • vlovich123 #1
    • p_l #1
    • cyberax #1
  5. 05

    File descriptor inheritance is backwards by default

    One of the most concrete pain points was file descriptor handling. The default Unix model is inheritance, which means the parent must remember to mark descriptors close-on-exec or scrub them in the child. That is exactly the wrong default for most launches, and it creates subtle bugs when large applications or libraries open lots of descriptors that the child should never see. Several comments argued a modern spawn primitive should take an explicit descriptor table instead.

    Audit subprocess creation around file descriptor inheritance, especially in code that mixes libraries and long-lived services. Favor explicit pass-through of stdin, stdout, stderr, and selected handles over relying on global close-on-exec hygiene.

      Attribution:
    • stabbles #1
    • sanderjd #1
    • gmueckl #1
    • JdeBP #1

Against the grain

  1. 01

    Fork plus exec is still the most composable interface

    The strongest pushback was that fork plus exec stays attractive because it lets child setup use ordinary system calls rather than forcing everything into one giant spawn structure. That composability is not nostalgia. It is a real ergonomics advantage when you need to tweak process groups, descriptors, credentials, namespaces, or other launch-time details without waiting for a new all-in-one API to grow flags for them.

    If your platform does not yet have a capable native spawn primitive, keep fork plus exec for complex launch paths instead of forcing a weaker abstraction. The existing model still wins when you need unusual pre-exec customization.

      Attribution:
    • uecker #1
    • matheusmoreira #1
    • jcranmer #1
  2. 02

    exec may be the bigger cost than fork

    Some comments argued the article overstates fork's share of the pain. Copy-on-write means fork is often mostly page table and VMA work, while exec then pays for loading the binary, invoking the dynamic linker, opening shared libraries, and mapping them into memory. If the goal is radically faster subprocess startup, fixing fork alone may not move the needle as much as advertised for dynamically linked programs.

    Profile fork and exec separately before redesigning around process startup. On many real systems the binary loader and dynamic linking path may dominate, which points to different optimizations than a new spawn syscall.

      Attribution:
    • jcalvinowens #1
    • Sophira #1
    • zerobees #1
  3. 03

    Repeated process creation may indicate the wrong architecture

    A few people were unconvinced that the kernel should optimize heavily for workloads that constantly spawn subprocesses. If you are launching the same logic again and again, a library call, worker pool, or long-lived service may simply be the better design. That does not erase the need for clean process isolation, but it challenges the idea that every hot path deserves cheaper process creation instead of a different decomposition.

    Before betting on faster spawn as your main fix, ask whether a process pool, daemon, or in-process library would remove the churn entirely. Kernel improvements help, but they do not automatically justify a subprocess-heavy architecture.

      Attribution:
    • jcalvinowens #1
    • lokar #1
    • m132 #1

In plain english

copy-on-write
A memory optimization where two processes share the same memory pages until one of them modifies a page, at which point a private copy is made.
exec()
A family of Unix system calls that replace the current process image with a new program.
fork()
A Unix system call that creates a new process by duplicating the calling process.
overcommit
An operating system policy that allows processes to reserve more memory than is physically available, assuming not all of it will be used at once.
posix_spawn
A POSIX API for starting a new process, intended as a higher-level alternative to fork() followed by exec().
TLB
Translation Lookaside Buffer, a CPU cache that stores recent virtual-to-physical memory mappings.
VMA
Virtual Memory Area, a kernel data structure describing a contiguous mapped range in a process's virtual address space.

Reference links

Background papers and articles

Process creation APIs and implementations

Workarounds and practical examples