Win32 has no fork(): how does CreateProcess differ from the Unix fork/exec model, and what does that change for code ported between the two?
answer
- one call, not two
- no address-space duplication
- inheritance is opt-in
- wait on a handle, not SIGCHLD
- closing the handle frees the object
basics
~20 sCreateProcess builds a new process from an executable image in one call, inheriting only handles that were explicitly marked inheritable. Unix fork first duplicates the caller's address space, so copy-on-write tricks and pre-fork server designs have no direct Win32 equivalent.
solid answer
~50 sUnix splits process creation in two: `fork()` duplicates the caller — address space, descriptors, signal state — and the child usually calls `exec` to replace that image. Win32 has only `CreateProcessW`, which does the whole thing at once: allocate a fresh address space, map the named executable, run the loader, and start one initial thread. Nothing of the parent's memory comes across, so any pattern that relies on inheriting live in-memory state through copy-on-write simply does not port. Handle inheritance is opt-in: the handle must be created or marked inheritable and `bInheritHandles` must be TRUE. There is also no exec-in-place — a process cannot replace its own image — and no SIGCHLD or zombies. The parent holds a process handle, waits on it, reads the exit code with `GetExitCodeProcess`, and must close the handle or leak the kernel object.
code
c · 28 lines#include <windows.h>
#include <stdio.h>
int main(void)
{
STARTUPINFOW si;
PROCESS_INFORMATION pi;
DWORD code = 0;
/* lpCommandLine must be writable: CreateProcessW may modify it in place. */
wchar_t cmd[] = L"cmd.exe /c echo hello from the child";
ZeroMemory(&si, sizeof si);
si.cb = sizeof si;
ZeroMemory(&pi, sizeof pi);
if (!CreateProcessW(NULL, cmd, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
printf("CreateProcessW failed: %lu\n", GetLastError());
return 1;
}
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &code);
printf("child exited with %lu\n", code);
CloseHandle(pi.hThread); /* leaking these leaks kernel objects */
CloseHandle(pi.hProcess);
return 0;
}go deeper
Know that Windows starts a child with CreateProcessW in one call and that the child does not get a copy of the parent's memory. Be able to say that you wait on the returned process handle to learn when it finished.
Explain the fork/exec split versus the single-call model, opt-in handle inheritance via bInheritHandles plus inheritable handles, and how the exit code is retrieved. Mention that both returned handles must be closed.
Show you have ported real code: identify which Unix idioms break (pre-fork pools, copy-on-write snapshots, re-exec, double-fork daemons), and call out the operational traps — leaked inheritable handles keeping a pipe open, the STILL_ACTIVE ambiguity, unquoted image paths.
Own the design consequence: on Windows the unit of concurrency is the thread and the thread pool, not the process, so isolation you would buy with processes on Unix costs far more here. Be ready to argue when the isolation is still worth the cost and what enforces cleanup of the resulting tree.
## Two different creation models On Unix, creating a process that runs a different program takes two primitives. `fork()` produces a near-identical copy of the calling process — same code, same heap and stack contents, same open file descriptors, same current directory — differing mainly in the returned PID. The copy is cheap because the kernel shares pages copy-on-write rather than physically duplicating them. The child then typically calls one of the `exec` family, which discards that address space and replaces it with a new executable image while keeping descriptors, PID and parent relationship. Win32 exposes no equivalent of the first half. The single call is `CreateProcessW` (and its variants `CreateProcessAsUserW`, `CreateProcessWithLogonW`), which takes an application name and/or a command line, security attributes for the process and its first thread, an inherit flag, creation flags, an environment block, a working directory, a `STARTUPINFOW`, and an out `PROCESS_INFORMATION`. The kernel creates a new process object with an empty address space, maps the executable image into it, sets up the process and thread environment blocks, and creates one thread that begins in the loader. The loader resolves imports and runs `DllMain` with `DLL_PROCESS_ATTACH` for every DLL before your entry point sees control. ## Nothing is inherited unless you ask This is the point ports break on. A `fork()` child starts life owning everything the parent had. A `CreateProcessW` child starts with nothing of the parent's memory. Three things can cross the boundary, and each is explicit: - **Handles.** A handle is inherited only if it was created with `SECURITY_ATTRIBUTES.bInheritHandle` set, or later marked with `SetHandleInformation(h, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT)`, *and* `CreateProcessW` is called with `bInheritHandles = TRUE`. Since Windows Vista you can be precise instead of blanket, passing an explicit list through `PROC_THREAD_ATTRIBUTE_HANDLE_LIST` on a `STARTUPINFOEX` via `UpdateProcThreadAttribute`. Blanket inheritance is a real bug source: an unrelated inheritable handle leaking into a long-lived child keeps a file locked or a pipe unclosed, so the reader never sees end-of-file. - **Standard streams**, passed through `STARTUPINFOW.hStdInput/hStdOutput/hStdError` with `STARTF_USESTDHANDLES` — the Windows way to build the equivalent of a shell pipeline. - **Environment and working directory**, passed as parameters (NULL means "copy the parent's"). ```c SECURITY_ATTRIBUTES sa = { sizeof sa, NULL, TRUE }; /* bInheritHandle = TRUE */ HANDLE r, w; CreatePipe(&r, &w, &sa, 0); SetHandleInformation(r, HANDLE_FLAG_INHERIT, 0); /* keep the read end private */ ``` ## No exec-in-place, no zombies A Win32 process cannot replace its own image; the closest you get is spawning a replacement and exiting. That removes a whole family of Unix idioms — re-exec to reload configuration, exec after dropping privileges, the shell's fork-then-exec. Termination reporting is also different. There is no SIGCHLD and no zombie state that a parent must reap to free a PID. Instead `PROCESS_INFORMATION` hands you `hProcess` and `hThread`; the process handle becomes signalled when the process exits, so `WaitForSingleObject(hProcess, INFINITE)` is the wait, and `GetExitCodeProcess` reads the status. The process *object* survives as long as any handle to it is open — that is what lets you read the exit code after the fact — so forgetting `CloseHandle` on both handles leaks kernel objects, the Windows analogue of leaking zombies. One sharp edge: `GetExitCodeProcess` returns `STILL_ACTIVE` (259) for a running process, so a program that genuinely exits with 259 is indistinguishable from one still running; wait on the handle first. ## Cost, and why Windows code prefers threads Because every creation is a full image load plus loader work plus DLL initialisation, and because there is no copy-on-write shortcut, process creation on Windows is considerably more expensive than `fork()`. That shaped the platform's idioms: Windows software reaches for threads, the thread pool and asynchronous I/O where Unix software might reach for another process. A design that spawns a process per request is defensible on Linux and usually a mistake on Windows. ## Ported-code checklist Watch for: a parent that assumed the child sees its heap; reliance on descriptor numbers 0/1/2 as integers rather than handles; `daemon()`-style double-fork (use a service instead); pre-fork worker pools; and copy-on-write snapshotting of large in-memory state. Also two `CreateProcessW` API traps — `lpCommandLine` must point to writable memory because the function may modify it in place, and when `lpApplicationName` is NULL an unquoted path containing spaces is parsed ambiguously, the classic `C:\Program Files\...` hijack. The NT kernel itself is not incapable of address-space cloning — the old POSIX/Interix subsystem used a native routine to implement `fork` — but Win32 does not expose it and it is undocumented for application use.
- Why is creating a process measurably more expensive on Windows than calling fork on Linux?Every `CreateProcessW` builds a fresh address space, maps the executable, then runs the loader: imports resolved and `DllMain` called with `DLL_PROCESS_ATTACH` for every dependent DLL. `fork()` maps the parent's pages copy-on-write and does none of that. This is why Windows designs favour threads, the thread pool and async I/O over process-per-request.
- How does a Win32 parent wait for a child and get its exit status, and what can go wrong?Wait on `PROCESS_INFORMATION.hProcess` with `WaitForSingleObject`, then call `GetExitCodeProcess`. Two traps: `GetExitCodeProcess` returns `STILL_ACTIVE` (259) while the process runs, so read it only after the wait; and both `hProcess` and `hThread` must be closed, otherwise the process and thread objects stay allocated even though the process is gone.
- If a handle is marked inheritable, does the child know what it received?No. Inheritance copies the handle value into the child's handle table but tells the child nothing. The parent must communicate it out of band — as a command-line argument, in the environment, or through `STARTUPINFOW.hStdInput/hStdOutput/hStdError`, which is why redirecting the standard streams is the usual way to wire a child up.
fork is photocopying yourself and then changing clothes; CreateProcess is hiring a new person and handing them only the documents you deliberately chose to pass over.
saying these in an interview costs you the question
- Calling CreateProcess just fork and exec fused together
- Assuming children inherit all handles like Unix descriptors
- Expecting zombie processes that must be reaped
- Thinking a process can exec a new image over itself
- Designing process-per-request on Windows as on Linux