AlgoMaster Logo

clone(): What fork and Thread Creation Share

16 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

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 clone creates 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.

Copying vs. Sharing

Before examining clone, three relationships must be kept separate.

Copied state

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.

Shared state

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.

Separate tables referring to one underlying object

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 Task Creation

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:

  • Kernel scheduling state
  • Saved CPU execution context
  • Kernel stack
  • System-wide thread ID

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.

What Is 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 process
  • pthread_create() for a POSIX thread
  • posix_spawn() for a combined create-and-execute operation

Thread libraries and container runtimes use clone-family mechanisms because they need controlled access to Linux-specific sharing and isolation features.

The Main Clone Flags

The flags mask is the heart of clone. Most flags in this table control sharing; CLONE_SETTLS prepares private per-thread state.

FlagWhen the flag is setWhen the flag is absent
CLONE_VMTasks share one virtual address spaceChild receives a separate address space initialized from the caller
CLONE_FILESTasks share one file-descriptor tableChild receives a copy of the descriptor table
CLONE_FSTasks share current directory, root directory, and file-creation maskChild receives a copy of that filesystem context
CLONE_SIGHANDTasks share the signal-handler tableChild receives a copy of signal dispositions
CLONE_THREADNew task joins the caller's thread groupNew task starts a new thread group
CLONE_SETTLSKernel initializes thread-local-storage state for the new taskNo 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 Policy

On 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.

What fork() Separates

The child created by fork receives its own process identity and logical resource containers.

Address space

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.

File-descriptor table

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.

Filesystem context

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.

Signal-handler table

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.

Thread group

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.

What fork() Still Shares

Separate 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.

Thread Creation as Another Sharing Policy

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.

What Threads Do Not Share

Threads share a process, but they are not one execution context.

Each thread has its own:

  • CPU registers and instruction pointer
  • Stack pointer and designated user-stack region
  • Linux thread ID
  • Scheduling state and CPU-affinity setting
  • Signal mask
  • Thread-local storage, including a thread-specific errno

The 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.

Process ID, Thread Group ID, and Thread ID

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.

Fork and Thread Creation Compared

PropertyChild created by fork()Thread created by pthread_create()
Virtual address spaceSeparate, initially based on parentShared
Ordinary globals and heapChanges normally divergeChanges are visible process-wide
File-descriptor tableCopiedShared
Inherited open file descriptionsReferenced by both tablesReached through the same shared table
Closing a descriptorDoes not remove the other process's entryRemoves it from the process-wide table
Current working directoryCopied; later changes are independentShared; a change affects all threads
Signal dispositionsCopiedShared
Signal maskCopied initially, then independentPer-thread
Value returned by getpid()DifferentSame
Linux TIDDifferentDifferent
Registers and stackSeparateSeparate
Primary completion APIwaitpid()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...

Three Sharing Surprises

The differences become concrete when a resource changes after creation.

Updating a global variable

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.

Closing a file descriptor

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.

Changing the current directory

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.

A Runnable Sharing Demonstration

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.

Observing the Underlying Creation Calls

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.

Reference-Based Resource Lifetimes

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:

  • Closing an entry in a copied descriptor table removes only that task's entry.
  • Closing an entry in a shared descriptor table changes the table seen by all sharers.
  • The underlying file or socket is released only when its final reference is gone.

Thinking in terms of tables, references, and underlying objects is more reliable than assuming that every resource simply “belongs” to one PID.

Why the Distinction Matters to Backend Systems

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.

Summary

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.

Quiz

clone(): What fork and Thread Creation Share Quiz

5 quizzes