<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Isbat-Bin-Hossain</title>
    <description>The latest articles on DEV Community by Isbat-Bin-Hossain (@isbatbinhossain).</description>
    <link>https://dev.to/isbatbinhossain</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3395809%2Faaa18b90-f47e-4004-a875-62eedb759b8f.png</url>
      <title>DEV Community: Isbat-Bin-Hossain</title>
      <link>https://dev.to/isbatbinhossain</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/isbatbinhossain"/>
    <language>en</language>
    <item>
      <title>fork() and exec(): The Weird and Elegant Idea Behind Unix Process Creation</title>
      <dc:creator>Isbat-Bin-Hossain</dc:creator>
      <pubDate>Mon, 30 Mar 2026 08:08:11 +0000</pubDate>
      <link>https://dev.to/isbatbinhossain/fork-and-exec-the-weird-and-elegant-idea-behind-unix-process-creation-15mp</link>
      <guid>https://dev.to/isbatbinhossain/fork-and-exec-the-weird-and-elegant-idea-behind-unix-process-creation-15mp</guid>
      <description>&lt;p&gt;At first glance, Unix’s &lt;code&gt;fork()&lt;/code&gt; + &lt;code&gt;exec()&lt;/code&gt; model feels… wrong.&lt;/p&gt;

&lt;p&gt;Why would an operating system &lt;strong&gt;copy an entire process&lt;/strong&gt;, only to immediately replace it with something else?&lt;/p&gt;

&lt;p&gt;It seems wasteful, indirect, and unnecessarily complicated.&lt;/p&gt;

&lt;p&gt;And yet, this design has survived for over 50 years, and still powers modern systems today.&lt;/p&gt;

&lt;p&gt;In this post, we’ll explore what &lt;code&gt;fork()&lt;/code&gt; and &lt;code&gt;exec()&lt;/code&gt; actually do, why this design exists, and why it remains so hard to replace.&lt;/p&gt;

&lt;h2&gt;
  
  
  fork()
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;fork()&lt;/em&gt; is a system call in &lt;em&gt;POSIX&lt;/em&gt; operating systems. It creates an almost exact replica of the process that calls it. The calling process is known as the &lt;strong&gt;parent&lt;/strong&gt;, and the new one is the &lt;strong&gt;child&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;fork()&lt;/em&gt; takes no arguments and returns the &lt;strong&gt;PID&lt;/strong&gt; of the child to the parent and &lt;code&gt;0&lt;/code&gt; to the child. This return value is used to determine whether a process is the parent or the child.&lt;/p&gt;

&lt;p&gt;When &lt;em&gt;fork()&lt;/em&gt; was first introduced, the parent and child ran in separate memory spaces with identical contents. This was expensive, especially for large processes with large memory footprints.&lt;/p&gt;

&lt;p&gt;To optimize this, modern implementations of &lt;em&gt;fork()&lt;/em&gt; use &lt;strong&gt;copy-on-write (COW)&lt;/strong&gt;. The parent and child have separate &lt;strong&gt;virtual address spaces&lt;/strong&gt;, but initially they point to the same physical memory. Instead of copying all memory eagerly, the kernel duplicates pages only when one of the processes modifies them. This significantly reduces memory overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  exec()
&lt;/h2&gt;

&lt;p&gt;The &lt;em&gt;exec()&lt;/em&gt; family of functions replaces the current process image with a new one. It does &lt;strong&gt;not create a new process&lt;/strong&gt;; it transforms the existing one.&lt;/p&gt;

&lt;p&gt;In essence, it wipes the current address space and loads a new program into it. On success, it never returns.&lt;/p&gt;




&lt;h2&gt;
  
  
  fork() + exec() pattern
&lt;/h2&gt;

&lt;p&gt;So what is the &lt;em&gt;fork() + exec()&lt;/em&gt; pattern?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The parent calls &lt;em&gt;fork()&lt;/em&gt; to create a child process&lt;/li&gt;
&lt;li&gt;The child calls &lt;em&gt;exec()&lt;/em&gt; to replace itself with a new program&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is where the design initially feels counterintuitive. We just duplicated the address space (or at least created new page tables in modern implementations) only to discard it immediately. Why not just create a new process directly?&lt;/p&gt;

&lt;p&gt;This is where the genius of the design becomes clear.&lt;/p&gt;

&lt;p&gt;The key idea behind the &lt;em&gt;fork() + exec()&lt;/em&gt; pattern is the &lt;strong&gt;separation of process creation from program execution&lt;/strong&gt;. By separating these two concerns, we gain a powerful capability: a configurable “gap” between creation and execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;This “gap” allows you to modify the execution environment before calling &lt;code&gt;exec()&lt;/code&gt;. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file descriptors (via &lt;code&gt;dup2&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;working directory&lt;/li&gt;
&lt;li&gt;user/group IDs&lt;/li&gt;
&lt;li&gt;signal handlers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is what that looks like in C:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;pid_t&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We are inside the child process.&lt;/span&gt;
    &lt;span class="c1"&gt;// This is the "gap" where we can redirect I/O, change directories, etc.&lt;/span&gt;
    &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"ls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="n"&gt;execvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 

    &lt;span class="c1"&gt;// If exec succeeds, the code below here NEVER runs!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// We are the parent. We wait for the child to finish executing.&lt;/span&gt;
    &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've ever typed &lt;code&gt;ls &amp;gt; output.txt&lt;/code&gt; in your terminal, you've used this gap. The shell calls &lt;code&gt;fork()&lt;/code&gt;, the child process uses &lt;code&gt;dup2()&lt;/code&gt; to overwrite its standard output with &lt;code&gt;output.txt&lt;/code&gt;, and then it calls &lt;code&gt;exec('ls')&lt;/code&gt;. The &lt;code&gt;ls&lt;/code&gt; program has no idea its output is being redirected to a file, it just writes to standard output as usual. The shell sets the stage before the actor even arrives.&lt;/p&gt;

&lt;p&gt;Another important benefit is &lt;strong&gt;inheritance as a composition mechanism&lt;/strong&gt;. Instead of passing large configuration objects (looking at you, Windows 👀), processes inherit resources, making composition natural.&lt;/p&gt;

&lt;p&gt;A key example is &lt;strong&gt;pipes&lt;/strong&gt;. A pipe is a unidirectional data channel used for inter-process communication (IPC). It is extensively used in shells (via the &lt;code&gt;|&lt;/code&gt; operator) to pass the output of one process as input to another.&lt;/p&gt;

&lt;p&gt;Calling &lt;code&gt;pipe()&lt;/code&gt; returns two file descriptors representing the ends of the pipe. Because file descriptors are inherited, each process simply closes the ends it doesn’t use and voilà IPC just works.&lt;/p&gt;

&lt;p&gt;This design reflects the Unix philosophy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;small, composable primitives instead of one large, complex API&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of one complex interface, Unix provides two simple APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one to copy a process&lt;/li&gt;
&lt;li&gt;one to replace a process&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This simplicity creates a flexible and easy-to-reason-about system.&lt;/p&gt;




&lt;h2&gt;
  
  
  Trade-offs
&lt;/h2&gt;

&lt;p&gt;While the &lt;em&gt;fork() + exec()&lt;/em&gt; pattern is an inspired design that still forms the backbone of modern systems, it does have trade-offs.&lt;/p&gt;

&lt;p&gt;The most obvious one is the overhead of &lt;code&gt;fork()&lt;/code&gt;. Even with COW, the kernel must create new page tables for the child process. This can be expensive, especially for large processes like servers or language runtimes. It can also introduce page faults when memory is modified.&lt;/p&gt;

&lt;p&gt;A more subtle but significant drawback is its interaction with multi-threaded programs. When &lt;code&gt;fork()&lt;/code&gt; is called, only the calling thread survives in the child process, while the state of other threads (such as locks) may remain. This can easily lead to deadlocks.&lt;/p&gt;

&lt;p&gt;For this reason, in multi-threaded programs, &lt;code&gt;fork()&lt;/code&gt; is almost always followed immediately by &lt;code&gt;exec()&lt;/code&gt;. Anything more complex can lead to inconsistent state.&lt;/p&gt;

&lt;p&gt;Additionally, this model can be inefficient for high-frequency process creation workloads. Modern systems often prefer threads, async/event loops, or worker pools for such cases. Because of this, alternatives like &lt;code&gt;posix_spawn()&lt;/code&gt; exist.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Even after 50+ years, the &lt;em&gt;fork + exec&lt;/em&gt; model remains dominant. Not because it’s perfect, but because it offers a level of flexibility that is still hard to match.&lt;/p&gt;




&lt;p&gt;If you enjoyed this, I’m currently diving deep into systems programming—building a Unix shell from scratch, implementing a custom &lt;code&gt;malloc&lt;/code&gt; allocator, and more.&lt;/p&gt;

&lt;p&gt;I’ll be sharing what I learn along the way, so feel free to follow along.  &lt;/p&gt;

&lt;p&gt;You can also check out the code for the shell project is here:&lt;br&gt;&lt;br&gt;
👉 &lt;a href="https://github.com/IsbatBInHossain/ishell" rel="noopener noreferrer"&gt;https://github.com/IsbatBInHossain/ishell&lt;/a&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>systems</category>
      <category>operatingsys</category>
      <category>c</category>
    </item>
  </channel>
</rss>
