Skip to main content
Bytes & Beyond

Process Creation

How processes are created using fork() and CreateProcess()

Process Creation: How Your OS Brings Programs to Life

Every time you double-click an app icon or type a command into a terminal, something remarkable happens behind the scenes. The operating system has to create a new process — allocating memory, setting up file descriptors, creating a PCB, and getting everything ready before your program executes a single instruction.

But the interesting part is how different operating systems go about doing this. Unix/Linux and Windows take fundamentally different approaches, and understanding both reveals a lot about their respective design philosophies.


What Actually Happens When a Process is Created?

Regardless of the OS, creating a process involves a predictable sequence of work. The kernel needs to create a new PCB, reserve some memory, load the program’s code, set up standard file descriptors (stdin, stdout, stderr), and finally place the new process in the ready queue so the scheduler can pick it up.


The Unix Way: fork()

In Unix, creating a new process usually means copying an existing one — and that is exactly what fork() does.

When a process calls fork(), the kernel creates a near duplicate of the process. In modern systems this uses copy-on-write, meaning the memory is shared initially and only copied when either process modifies it. The child process inherits file descriptors and picks up execution from the exact same point in the code. The only meaningful difference is the return value of fork() itself — the parent gets back the child’s PID, while the child gets back zero. That single difference is how both processes figure out which “side” they’re on.

pid_t pid = fork();

if (pid == 0) {
    // We're the child
    printf("Child: PID = %d\n", getpid());
} else if (pid > 0) {
    // We're the parent
    printf("Parent: my child's PID = %d\n", pid);
} else {
    // Something went wrong
    perror("fork failed");
}

After fork() returns, you have two independent processes running the same code. They share the same program code (text segment), and initially share memory pages using copy-on-write until modifications occur. Each process has its own copy of the data — heap, stack, global variables, all of it. Modify a variable in the child, and the parent’s copy is completely unaffected.

int x = 10;
pid_t pid = fork();

if (pid == 0) {
    x = 20;  // Only changes the child's copy
    printf("Child sees x = %d\n", x);   // prints 20
} else {
    wait(NULL);
    printf("Parent sees x = %d\n", x);  // prints 10
}

But What if You Want to Run a Different Program?

fork() alone just clones the parent. If the shell wants to run ls or vim, it needs to replace the child’s entire process image with a completely different program. That’s where exec() comes in.

The pattern is so common it’s practically a Unix idiom: fork a child, then have the child call exec() to transform itself into whatever program you actually want to run. If exec() succeeds, the child’s original code and data are gone — replaced entirely by the new program. The parent, meanwhile, just waits.

pid_t pid = fork();

if (pid == 0) {
    execl("/bin/ls", "ls", "-la", NULL);
    // If we reach this line, exec failed
    perror("execl failed");
} else {
    wait(NULL);
    printf("ls finished\n");
}

This fork-then-exec pattern is how your shell runs every command you type.


The Windows Way: CreateProcess()

Windows does not implement a fork() system call. Instead, it provides CreateProcess(), which directly starts a new process running a specified program. This is a more explicit approach — a single function call that directly launches a new, independent process running a specified program.

Where fork() is elegant and minimal, CreateProcess() is thorough and explicit. You tell it exactly what program to run, how to set up the new process’s window, what security attributes to apply, whether to inherit handles, and more. All of that configuration happens upfront, in one call.

STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;

BOOL success = CreateProcess(
    NULL,           // use command line for app name
    "notepad.exe",  // command line
    NULL, NULL,     // security attributes
    FALSE,          // don't inherit handles
    0,              // creation flags
    NULL, NULL,     // environment and directory
    &si, &pi        // startup info and output
);

if (success) {
    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}

There’s no ambiguity about which “side” you’re on — the parent always calls CreateProcess(), and the child is a brand new process running whatever program you specified.


fork() vs CreateProcess(): Two Different Philosophies

The contrast between these two approaches is striking once you see it clearly.

fork() is built on the idea that creating a new process is fundamentally about copying an existing one. It’s minimal by design — one syscall, a handful of return value checks, and you’re done. The tradeoff is that running a different program takes a second step (exec), and the copy-on-write semantics, while clever, add some complexity under the hood.

CreateProcess() treats process creation as an explicit act of construction. You specify exactly what you want, upfront. It’s more verbose, but there’s no ambiguity — what you see is what you get. There’s no “which side am I on?” puzzle to solve.

Neither approach is strictly better. Fork’s simplicity made it easy to build powerful abstractions (like shells and process pools) on top of it. CreateProcess’s explicitness makes the intent of your code immediately clear.


The Zombie Problem

One thing you have to watch out for on Unix: zombie processes.

When a child process exits, it doesn’t disappear immediately. It sits in a zombie state, holding onto its process table entry, waiting for the parent to acknowledge its death by calling wait(). If the parent never does, the zombie lingers — not consuming CPU or memory, but occupying a slot in the process table indefinitely.

The fix is simple: always call wait() after forking.

// Bad: creates a zombie
if (fork() == 0) {
    exit(0);
} else {
    sleep(100);  // child is a zombie the whole time
}

// Good: no zombie
if (fork() == 0) {
    exit(0);
} else {
    wait(NULL);  // parent acknowledges child's exit
}

On Windows, the equivalent responsibility is closing the process handle with CloseHandle() after WaitForSingleObject() returns. Same idea, different API.


The Bigger Picture

Process creation might seem like low-level plumbing, but it’s everywhere. Every tab your browser opens, every worker in a web server’s process pool, every command your shell runs — all of it flows through either fork()/exec() or CreateProcess().

The shell you use every day is basically a loop that reads your input, forks a child, execs the program you asked for, and waits. Over and over. Understanding that loop makes a lot of other things click into place.


Who Creates the First Process?

You might wonder: if every process is created by a parent, who creates the very first process? The answer is the kernel itself.

On Unix/Linux systems, after the kernel finishes booting, it directly creates the very first user-space process—traditionally called init (PID 1), or systemd on modern Linux. This process has no parent in the usual sense; its parent is the kernel.

From there, init/systemd starts system services and user shells, and those in turn fork and exec user programs. Every user-space process (except PID 1) is ultimately a descendant of this first process.