skip to content

Processes and Threads

Windows creates processes and threads differently from Unix — CreateProcess rather than fork/exec — and adds job objects, thread pools, and fibers. The scheduling model (quantum, preemption, priority boosting, affinity) is the part interviews actually probe.

on this pageshow

questions

6

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?

level: middleimportance: must knowfreq 70%

answer

  1. one call, not two
  2. no address-space duplication
  3. inheritance is opt-in
  4. wait on a handle, not SIGCHLD
  5. closing the handle frees the object

basics

~20 s

CreateProcess 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 s

Unix 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
c
#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

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context

open as a page

On Windows, how do a process priority class and a thread's relative priority combine into what the scheduler runs, and what is priority boosting for?

level: middleimportance: must knowfreq 55%

basics

~20 s

Windows ranks threads on 32 priority levels. The process priority class sets a base, the thread's relative priority shifts it, and the scheduler always runs the highest-priority ready thread, preempting lower ones. Temporary boosts lift threads in the dynamic range 1-15 so waiters and starved threads make progress.

open as a page

A Windows build agent must guarantee that when a build ends, every process it spawned is gone — including grandchildren. Why isn't terminating the top process enough, and what does the OS give you?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Terminating a process on Windows kills only that process; there is no supervisory link that carries the kill to descendants. A job object does: assign the top process to a job, set JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, and every process in the job — children included — dies when the job handle closes.

open as a page

What goes wrong when a callback on the Windows default thread pool blocks for several seconds, and what does the thread pool API offer for genuinely long-running work?

level: seniorimportance: should knowfreq 33%

basics

~20 s

A blocked callback occupies a shared worker thread. Because the pool adds threads gradually rather than instantly, other queued work — timers, waits and I/O completions from anywhere in the process — is delayed behind it. Call CallbackMayRunLong, use a private pool, or run long work on a dedicated thread.

open as a page

When is hard-pinning threads with SetThreadAffinityMask on Windows justified, and what does it cost compared with leaving placement to the scheduler?

level: principalimportance: should knowfreq 28%

basics

~20 s

A hard affinity mask forbids the scheduler from using any other processor, so a pinned thread waits while cores sit idle and the mask encodes today's topology into the binary. Justify it only with measurements, and prefer softer expressions of preference such as an ideal processor or CPU sets.

open as a page

What is a Windows fiber, how does it differ from a thread, and why do so few applications use fibers?

level: middleimportance: nice to knowfreq 20%

basics

~20 s

A fiber is a user-mode execution context — its own stack and register state — that the application schedules itself by calling SwitchToFiber. The kernel never sees it and schedules only the host thread, so fibers are cooperative, give no parallelism on their own, and one blocking call stalls every fiber on that thread.

open as a page