fork() creates a process. pthread_create() creates a thread.
These operations look fundamentally different at the programming level, but Linux implements both by creating a new schedulable task and choosing which kernel-managed resources that task will share.
The mechanism behind that choice is the clone family.
Linux
clonecreates a task and uses flags to decide which parts of the caller's execution environment are copied and which remain shared.
With few sharing flags, the result behaves like a separate process. With the sharing flags used by a thread library, the result behaves like another thread in the same process.
This model reveals that the deepest difference between a process and a thread is not whether the kernel can schedule it. Both are schedulable. The difference is which resources form a common ownership boundary.
Before examining clone, three relationships must be kept separate.
The child starts with the same value, but each side can later change its own copy independently.
This is how ordinary writable memory behaves across fork.
Both tasks refer to the same live object. A change made through one task is immediately part of the state seen by the other.
Threads share ordinary global and heap memory in this way.
Each task has its own table entry, but both entries point to the same kernel object.
Two table entries, one object. That is why closing fd 3 in one task leaves the other task's fd 3 working, while a change to the file offset is visible to both.
This is how corresponding file descriptors behave after fork. Closing one table entry does not erase the other entry, but operations can still interact through the shared underlying file description.
Many incorrect explanations reduce all three relationships to the word shared. Linux process behavior becomes much clearer once the exact relationship is named.
Linux represents every schedulable execution context with a task descriptor, commonly discussed through task_struct.
A single-threaded process has one task. A multithreaded process has several tasks that share process-wide resources.
Every task has its own:
The task descriptor also contains pointers to larger kernel objects, including the address space, file-descriptor table, filesystem context, and signal-handling state.
Creating a new task always requires new execution and scheduling state. The sharing flags determine whether several of those pointers lead to existing objects or to newly copied structures.
clone()?Linux exposes several related interfaces under the clone name.
The GNU C library provides a clone() wrapper with a function-oriented interface:
The caller supplies a function, an argument, a stack for the child, and a bit mask of flags. The new task begins by calling fn(arg).
The kernel also provides lower-level clone system-call interfaces. The newer clone3() packages its options into a struct clone_args, provides room for future extensions, and returns in both parent and child in a style closer to fork.
These interfaces are Linux-specific and intentionally low-level. Their exact stack, thread-local storage, signal, and cleanup requirements are easy to get wrong.
Application code should normally use the higher-level interface that matches its intent:
fork() for a new child processpthread_create() for a POSIX threadposix_spawn() for a combined create-and-execute operationThread libraries and container runtimes use clone-family mechanisms because they need controlled access to Linux-specific sharing and isolation features.
The flags mask is the heart of clone. Most flags in this table control sharing; CLONE_SETTLS prepares private per-thread state.
| Flag | When the flag is set | When the flag is absent |
|---|---|---|
CLONE_VM | Tasks share one virtual address space | Child receives a separate address space initialized from the caller |
CLONE_FILES | Tasks share one file-descriptor table | Child receives a copy of the descriptor table |
CLONE_FS | Tasks share current directory, root directory, and file-creation mask | Child receives a copy of that filesystem context |
CLONE_SIGHAND | Tasks share the signal-handler table | Child receives a copy of signal dispositions |
CLONE_THREAD | New task joins the caller's thread group | New task starts a new thread group |
CLONE_SETTLS | Kernel initializes thread-local-storage state for the new task | No TLS setup is requested through this flag |
Thread libraries also use flags such as CLONE_PARENT_SETTID and CLONE_CHILD_CLEARTID to publish a thread ID and coordinate termination. Those flags support thread management rather than defining the main resource-sharing boundary.
Some combinations depend on others. Linux requires CLONE_SIGHAND to be used with CLONE_VM, and CLONE_THREAD requires CLONE_SIGHAND. A normal thread therefore shares memory and signal dispositions as part of joining the same thread group.
The flags can produce combinations that are neither an ordinary Unix process nor a normal POSIX thread. For example, two tasks can share an address space without joining the same thread group, or separate address spaces can share one descriptor table.
That flexibility is useful to system software, but it is another reason application developers should prefer pthread_create and fork over assembling raw clone flags themselves.
fork() as a Particular Sharing PolicyOn modern glibc-based Linux systems, the C library's fork() wrapper is implemented using a clone-family operation configured to preserve traditional fork behavior.
Conceptually, it requests a new task without CLONE_VM, CLONE_FILES, CLONE_FS, CLONE_SIGHAND, or CLONE_THREAD. The result has a separate address-space description, copied resource tables, and a new thread group. Traditional SIGCHLD termination behavior lets the parent observe and collect the child.
The word copied describes the visible semantics. The kernel can share internal immutable objects or physical pages temporarily when doing so does not violate those semantics.
This is why fork() can create an independent process without eagerly duplicating every byte of the parent's physical memory.
fork() SeparatesThe child created by fork receives its own process identity and logical resource containers.
Parent and child have separate virtual address spaces. Ordinary writes made by one are not visible in the other.
Linux uses copy-on-write to implement the initial duplication efficiently. That optimization does not make normal writable memory into live shared memory.
Explicitly shared mappings are an exception. If the parent created a mapping intended to be shared between processes, the child can inherit a mapping of the same shared backing object.
The child gets a copied descriptor table. Opening or closing a descriptor in the child does not add or remove an entry in the parent's table.
The entries inherited at fork, however, refer to the same open file descriptions as the corresponding parent entries. File offsets and open-file status flags can therefore remain shared.
The child begins with the same current directory, root directory, and file-creation mask. Because the context was copied, a later chdir() in the child does not change the parent's current directory.
The child begins with copies of the parent's signal dispositions. A later change to a handler in one process does not alter the other's table.
The child starts a new thread group and receives a new process ID. If the parent has multiple threads, only the thread that called fork exists in the child.
fork() Still SharesSeparate processes are not disconnected islands.
Inherited descriptors can refer to the same files, pipes, sockets, and other kernel objects. Explicit shared-memory mappings can expose the same bytes. Both processes remain in many of the same system-wide namespaces unless isolation was requested separately.
The kernel manages these relationships with references. An underlying object remains alive as long as some valid reference still reaches it.
Consider an inherited socket. Both descriptor tables may contain an entry numbered 5 that refers to the same socket object. If the child closes its descriptor, the parent's entry remains valid, and the socket stays alive because the parent still holds a reference.
This distinction is central to prefork servers. A master process can open a listening socket and then fork worker processes. Each worker inherits a descriptor that reaches the same listening socket, while each worker keeps a separate address space.
On Linux, the Native POSIX Threads Library implements pthread_create() using clone-family mechanisms.
A simplified thread-like flag set looks conceptually like:
Real thread creation uses additional flags and data for thread-local storage, thread IDs, cleanup, and synchronization. The simplified set shows the major sharing decision, not a recipe for implementing pthread_create.
With these flags, both tasks point to common process-wide objects:
Each task keeps only what it needs to execute independently. Everything else is one copy with two users, which is what makes a write by either task visible to the other.
This produces the behavior programmers expect from threads: a global variable updated by one thread is part of the same memory observed by the others.
Threads share a process, but they are not one execution context.
Each thread has its own:
errnoThe phrase “each thread has its own stack” describes how the stack is used, not a hardware protection boundary. The stacks normally reside inside the process's shared address space. A bad pointer in one thread can still corrupt another thread's stack.
Separate register sets and stacks allow threads to execute different functions at the same time. Shared memory allows them to exchange data directly, which is convenient but requires synchronization when accesses can overlap.
Linux assigns every task a system-wide unique thread ID, or TID.
Tasks that form one process belong to the same thread group. The thread group ID, or TGID, is the value applications normally see as the process ID returned by getpid().
For the first thread in a process, the TID and TGID have the same numeric value. If getpid() reports 4200, for example, the main thread's TID is also 4200. Additional worker threads might have TIDs 4207 and 4211 while still belonging to TGID 4200.
A new process created by fork is the leader of a new thread group, so its new TID and new TGID initially have the same numeric value.
Linux exposes individual threads under:
The POSIX pthread_t value is a library-level thread identifier and should not be assumed to be numerically equal to the Linux TID.
| Property | Child created by fork() | Thread created by pthread_create() |
|---|---|---|
| Virtual address space | Separate, initially based on parent | Shared |
| Ordinary globals and heap | Changes normally diverge | Changes are visible process-wide |
| File-descriptor table | Copied | Shared |
| Inherited open file descriptions | Referenced by both tables | Reached through the same shared table |
| Closing a descriptor | Does not remove the other process's entry | Removes it from the process-wide table |
| Current working directory | Copied; later changes are independent | Shared; a change affects all threads |
| Signal dispositions | Copied | Shared |
| Signal mask | Copied initially, then independent | Per-thread |
Value returned by getpid() | Different | Same |
| Linux TID | Different | Different |
| Registers and stack | Separate | Separate |
| Primary completion API | waitpid() | pthread_join() for a joinable thread |
The table shows why “threads share everything” and “processes share nothing” are both misleading.
Threads retain private execution state. Forked processes can remain connected through shared kernel objects and explicitly shared memory.
Loading simulation...
The differences become concrete when a resource changes after creation.
After fork, changing an ordinary global modifies only that process's private memory.
After thread creation, changing the global modifies the process-wide value. Other threads can observe it, subject to the language's synchronization and memory-ordering rules.
After fork, each process has its own descriptor-table entry. Closing the child's descriptor does not close the parent's entry.
Among threads, the table is shared. If one thread closes descriptor 7, another thread that later uses 7 may fail—or may accidentally reach an unrelated object if that descriptor number has already been reused.
This is why close-versus-use races are serious in multithreaded servers.
Forked processes begin in the same directory but have copied filesystem contexts. A child can call chdir() without changing the parent.
Threads share filesystem context. A chdir() issued by one thread changes how relative paths resolve for every thread in the process.
Backend services often avoid changing the working directory after startup for precisely this reason.
The following program creates a thread and then a forked child. Both update the same global variable:
Compile it with thread support:
A typical run produces:
The thread has the same PID as the main thread and changes the shared value to 20. The main thread observes that change after pthread_join.
The forked child has a new PID. It changes its copy to 30, but the parent still sees 10.
The program joins the worker before calling fork, so it is single-threaded at the moment of the fork.
On Linux, trace the example with:
The trace will vary with the architecture, kernel, C library, and strace version.
Thread creation should show a clone-family call containing flags such as CLONE_VM, CLONE_FILES, CLONE_FS, CLONE_SIGHAND, and CLONE_THREAD.
The fork() wrapper may also appear as a clone-family call, but without those resource-sharing flags and with child-termination behavior equivalent to traditional fork.
This is not evidence that fork and thread creation have identical semantics. It shows that Linux can implement both semantics through one configurable task-creation mechanism.
To inspect the threads of an existing process:
The PID column remains the same across the process, while TID identifies each schedulable task.
Shared kernel objects commonly use reference counting.
If two tasks refer to one object, one task exiting does not necessarily destroy it. The object remains alive while another reference exists.
This applies to inherited files and sockets after fork, as well as process-wide structures shared by threads.
The exact effect of an operation depends on which layer changes:
Thinking in terms of tables, references, and underlying objects is more reliable than assuming that every resource simply “belongs” to one PID.
A prefork server and a thread-pool server can run the same request-handling code while having very different failure and sharing boundaries.
In a prefork design, workers have separate address spaces. A memory corruption bug in one worker does not directly overwrite another worker's heap. Workers can still share inherited sockets and explicit interprocess resources.
In a thread-pool design, workers share the heap, descriptor table, and most process-wide configuration. Communication through memory is direct and efficient, but a bad pointer, unsynchronized update, or accidental descriptor close can affect the entire process.
Neither design is universally better. The right question is:
Which state should updates make visible to every worker, and which failures should remain isolated?
The clone model makes that tradeoff concrete. Sharing improves direct communication and reduces duplication; separation strengthens isolation and independent lifecycle control.
The same clone framework can also request new Linux namespaces with CLONE_NEW* flags. Those flags control which system-wide views a new task enters rather than whether ordinary process resources are shared. Container runtimes combine namespace and resource controls to create isolated environments, but the underlying creation event is still the creation of Linux tasks.
Linux creates processes and threads as schedulable tasks. Clone flags determine whether a new task receives copied resource containers or shares the caller's address space, descriptor table, filesystem context, signal handlers, and thread group.
fork() chooses process-like semantics: separate memory and copied tables, while some inherited entries still refer to shared kernel objects. pthread_create() chooses thread-like semantics: process-wide resources are shared, but each thread retains its own execution context, stack, TID, scheduling state, signal mask, and thread-local storage.
The essential mental model is:
A Linux process or thread is a task plus a deliberate sharing policy.
5 quizzes