Threads as Tasks and the CLONE Flags
Linux has no separate “thread” object. The kernel’s only schedulable entity is the
task_struct, and every one of them — whether userspace calls it a process or a thread — is born from the sameclonefamily of system calls. What distinguishes a “thread” from a “process” is nothing but a set ofCLONE_*flags: a thread iscloneasking the child to share its creator’s address space, file-descriptor table, signal handlers, and thread group, while a process iscloneasking for fresh, private copies of those things. Mechanically there is one creation path and one resource-sharing knob per resource (clone(2)). This note explains what each flag shares, how thread groups are wired throughtgid/group_leader, and how POSIX threads (pthread_create) map onto exactly oneclone3call — verified against Linux 6.12 LTS (uapi/linux/sched.h) and glibc 2.40 (nptl/pthread_create.c).
Mental Model: One Task, A Dial Per Resource
The unifying idea is that a process is just a collection of resources — an address space, a file table, a filesystem context, a signal-handler table, a set of pending/blocked signals, namespaces, an I/O context — bundled around one or more threads of execution. Other operating systems reify “process” and “thread” as two distinct kernel objects. Linux reifies neither: it has only the task (struct task_struct, see The task_struct Process Descriptor), and each resource is a separately reference-counted sub-structure (mm_struct, files_struct, fs_struct, sighand_struct, signal_struct, …) that a task points at. Creating a new task means deciding, for each of those pointers, whether the child gets the same object (incremented refcount, shared) or a fresh one (copied). Each decision is one CLONE_* bit.
flowchart TB subgraph PARENT["Parent task_struct"] PMM["mm (address space)"] PFILES["files (fd table)"] PFS["fs (cwd, root, umask)"] PSIG["sighand (handlers)"] end subgraph THREAD["Child created with CLONE_VM|CLONE_FILES|CLONE_FS|CLONE_SIGHAND|CLONE_THREAD<br/>= a THREAD"] direction TB TPTR["task_struct"] end subgraph PROC["Child created with no CLONE flags (plain fork)<br/>= a PROCESS"] direction TB CMM["mm (COW copy)"] CFILES["files (copy)"] CFS["fs (copy)"] CSIG["sighand (copy)"] end PMM -. shared .-> TPTR PFILES -. shared .-> TPTR PFS -. shared .-> TPTR PSIG -. shared .-> TPTR PMM ==> CMM PFILES ==> CFILES PFS ==> CFS PSIG ==> CSIG
The thread-vs-process spectrum as a set of per-resource dials. What it shows: a “thread” (left child) points at the very same mm, files, fs, and sighand objects as its parent — sharing is a pointer copy plus a refcount bump; a “process” (right child) gets its own copies of each (the address space copied copy-on-write, not eagerly). The insight: “thread” and “process” are the two endpoints of a continuum, and clone lets you pick any point in between — share the fd table but not the address space, or share the address space but keep separate signal handlers. There is no third kind of object; there is only which dials you turn.
The Resource-Sharing Flags, One by One
The flag values are stable architecture-independent constants in the user-space ABI header (uapi/linux/sched.h, v6.12). The most important group decides what is shared:
CLONE_VM(0x00000100) — “the calling process and the child process run in the same memory space. Memory writes performed by the calling process or by the child process are also visible in the other process” (clone(2)). In the kernel this is the single branch incopy_mm()that decides between bumping the existingmm_struct’s refcount (mmget(oldmm); mm = oldmm;) and duplicating it (dup_mm()) (kernel/fork.c, v6.12).CLONE_VMis the defining flag of a thread: shared address space is what lets two threads see each other’s heap and globals.CLONE_FILES(0x00000400) — “the calling process and the child process share the same file descriptor table. Any file descriptor created by the calling process or by the child process is also valid in the other process.” Without it, the child gets a copy of the table: the same underlying open-file descriptions (so offsets are shared), but a private array of descriptors, so a laterclose()oropen()in one does not change the other’s fd numbers.CLONE_FS(0x00000200) — “the caller and the child process share the same filesystem information. This includes the root of the filesystem, the current working directory, and the umask.” Achdir()orumask()by one then affects the other.CLONE_SIGHAND(0x00000800) — “the calling process and the child process share the same table of signal handlers” (struct sighand_struct). Note: even withCLONE_SIGHAND, “the calling process and child processes still have distinct signal masks” — the handlers (thesigactiontable) are shared, but each task’s blocked-signal mask is its own (clone(2)).CLONE_SYSVSEM(0x00040000) — share the System V semaphore undo (SEM_UNDO) list, so semaphore adjustments are pooled rather than per-task.CLONE_IO(0x80000000) — share the block-I/O context, so the I/O scheduler treats the tasks as one for fairness accounting.
The thread-group flag and its hard dependencies
CLONE_THREAD(0x00010000) — “the child is placed in the same thread group as the calling process.” This is what makes the new task a thread of the same process rather than a new process: it shares the thread group ID and reports back to the same parent. The kernel enforces a dependency chain incopy_process():CLONE_THREADrequiresCLONE_SIGHAND(if ((clone_flags & CLONE_THREAD) && !(clone_flags & CLONE_SIGHAND)) return ERR_PTR(-EINVAL);), andCLONE_SIGHANDin turn requiresCLONE_VM(if ((clone_flags & CLONE_SIGHAND) && !(clone_flags & CLONE_VM)) return ERR_PTR(-EINVAL);) (kernel/fork.c, v6.12). The comments in the source spell out the reasoning: “Thread groups must share signals as well,” and “Shared signal handlers imply shared VM … Blocking this case allows for various simplifications in other code.” So you cannot ask for a thread group without also sharing handlers and the address space — the kernel makes the dial dependencies explicit.
Thread-group identity: tgid, group_leader, and the shared PID
The shared “process ID” of a multithreaded program is the thread group ID (tgid), and the relationship is set in copy_process():
if (clone_flags & CLONE_THREAD) {
p->group_leader = current->group_leader; /* point at the existing leader */
p->tgid = current->tgid; /* inherit the thread-group ID */
} else {
p->group_leader = p; /* I am my own leader */
p->tgid = p->pid; /* a new process: tgid == pid */
}(kernel/fork.c, v6.12). Every task always has a unique kernel-internal pid (what user space calls a thread ID / TID), but the value getpid() returns is the tgid. The first thread of a process is its own group_leader (tgid == pid); every additional thread inherits the leader’s tgid, so all threads of one process share a single getpid() result. The per-task pid is the TID that gettid() returns and that clone can write back to user space (see CLONE_PARENT_SETTID below). This split — TID per task, TGID per process — is the subject of Process and Thread Identifiers in Linux.
A subtlety enforced in the same region: a CLONE_THREAD child takes the same parent as its creator rather than parenting onto its creator (if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) p->real_parent = current->real_parent;), and its exit_signal is the group leader’s, because a thread’s death should not deliver SIGCHLD to its peer thread (kernel/fork.c, v6.12).
The TID-reporting and TLS flags
These flags don’t change sharing; they wire up the thread-library bookkeeping that user space needs:
CLONE_SETTLS(0x00080000) — “The TLS (Thread Local Storage) descriptor is set totls.” The architecture decides whattlsmeans: on x86-64 it sets the%fsbase register that thread-local variables are addressed through (clone(2)). Without per-thread TLS, C11_Thread_local/__threadvariables could not work.CLONE_PARENT_SETTID(0x00100000) — “Store the child thread ID at the location pointed to byparent_tidin the parent’s memory.” The library gets the new TID written into a known slot in the parent beforeclonereturns.CLONE_CHILD_SETTID(0x01000000) — the same store, but into the child’s memory, completing before the call returns in the child.CLONE_CHILD_CLEARTID(0x00200000) — “Clear (zero) the child thread ID at the location pointed to bychild_tidin child memory when the child exits, and do a wakeup on the futex at that address” (clone(2)). This is the mechanism behindpthread_join: the thread library passes the address of the thread’s TID slot, and on thread exit the kernel zeroes it and does a futex wake, so a joiner blocked on that address is released. This is why glibc’s exit comment notes it “cannot call_exithere … The ‘exit’ implementation in the kernel will signal when the process is really dead since ‘clone’ got passed theCLONE_CHILD_CLEARTIDflag” (nptl/pthread_create.c, glibc 2.40).CLONE_PIDFD(0x00001000) — allocate a pidfd (a file descriptor that refers to the child process, usable withpoll/waitid) and store it in the parent. This is the modern, race-free alternative to PID-number-based signalling.
The namespace flags
A large block of flags requests new namespaces rather than resource sharing: CLONE_NEWNS (mount), CLONE_NEWUTS (hostname), CLONE_NEWIPC, CLONE_NEWPID, CLONE_NEWNET, CLONE_NEWUSER, CLONE_NEWCGROUP, and CLONE_NEWTIME (0x00000080) (uapi/linux/sched.h, v6.12). These are the substrate of containers and are covered in the Linux Containers and Isolation MOC; the only scheduling-relevant fact here is that CLONE_THREAD is mutually exclusive with CLONE_NEWUSER/CLONE_NEWPID — a thread cannot live in a different PID or user namespace than its peers (copy_process() rejects it with -EINVAL).
clone3 and struct clone_args: the Extensible API
The original clone() packs everything into ordered register arguments and is hemmed in by a 32-bit flag word that ran out of room (note CLONE_CLEAR_SIGHAND and CLONE_INTO_CGROUP are 0x1_00000000 and 0x2_00000000 — above bit 31). The modern entry point is clone3() (added in Linux 5.3), which takes a single versioned structure:
/* include/uapi/linux/sched.h, v6.12 */
struct clone_args {
__aligned_u64 flags; /* the CLONE_* bitmask, now a full 64-bit field */
__aligned_u64 pidfd; /* where to store the CLONE_PIDFD descriptor */
__aligned_u64 child_tid; /* CLONE_CHILD_SETTID / CLEARTID target */
__aligned_u64 parent_tid; /* CLONE_PARENT_SETTID target */
__aligned_u64 exit_signal; /* signal sent to parent on exit (e.g. SIGCHLD) */
__aligned_u64 stack; /* lowest byte of the child stack */
__aligned_u64 stack_size; /* size of that stack */
__aligned_u64 tls; /* CLONE_SETTLS value */
__aligned_u64 set_tid; /* array of desired PIDs (one per nested pidns) */
__aligned_u64 set_tid_size; /* number of entries in set_tid */
__aligned_u64 cgroup; /* CLONE_INTO_CGROUP target cgroup fd */
};The versioning trick is size-based: the kernel defines CLONE_ARGS_SIZE_VER0 = 64, VER1 = 80, VER2 = 88, and the syscall takes the structure size as its second argument. copy_clone_args_from_user() calls copy_struct_from_user(), which zero-fills any fields a smaller (older) caller omitted and rejects any non-zero bytes a newer caller set beyond the size the kernel understands (kernel/fork.c, v6.12). New fields can therefore be appended forever without a new syscall number, and old binaries keep working — the same “extensible struct versioned by size” pattern used by openat2 and sched_setattr. The BUILD_BUG_ON(offsetofend(struct clone_args, tls) != CLONE_ARGS_SIZE_VER0) assertions in the source are compile-time guards that the field offsets never drift from the published ABI sizes.
clone3 also cleans up two ABI warts: exit_signal is a separate 64-bit field instead of being crammed into the low byte of the flags word (the old CSIGNAL mask), and clone3 adds genuinely new capabilities — CLONE_INTO_CGROUP (place the child directly into a target cgroup v2 by fd) and set_tid (request specific PIDs across a PID-namespace hierarchy, used by checkpoint/restore).
Worked Example: How pthread_create Maps Onto One clone3
The cleanest proof that “a thread is just clone-with-everything-shared” is glibc’s own thread creator. In glibc 2.40, pthread_create ultimately issues a single clone3 with this exact flag set (nptl/pthread_create.c):
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM
| CLONE_SIGHAND | CLONE_THREAD
| CLONE_SETTLS | CLONE_PARENT_SETTID
| CLONE_CHILD_CLEARTID
| 0);
struct clone_args args = {
.flags = clone_flags,
.pidfd = (uintptr_t) &pd->tid,
.parent_tid = (uintptr_t) &pd->tid, /* CLONE_PARENT_SETTID writes TID here */
.child_tid = (uintptr_t) &pd->tid, /* CLONE_CHILD_CLEARTID clears it on exit */
.stack = (uintptr_t) stackaddr, /* the thread's pre-allocated stack */
.stack_size = stacksize,
.tls = (uintptr_t) tp, /* CLONE_SETTLS: the new thread's TCB */
};
int ret = __clone_internal (&args, &start_thread, pd);Reading the flags top to bottom is reading the POSIX thread contract:
CLONE_VM | CLONE_FS | CLONE_FILES— shared address space, working directory, and file descriptors: POSIX threads of one process see each other’s memory, sharechdir, and share open files.CLONE_SIGHAND | CLONE_THREAD— one shared signal-handler table and one thread group, so a signal sent to the process can be handled by any thread andgetpid()is identical across them. (glibc’s comment: this “selects the POSIX signal semantics and various other kinds of sharing (itimers, POSIX timers, etc.).”)CLONE_SETTLS— give the new thread its own thread-control block soerrnoand__threadvariables are per-thread.CLONE_PARENT_SETTID— write the new TID into thepthread’s descriptor so the creating thread immediately knows the child’s TID (glibc notes this is cheaper in the kernel thanCLONE_CHILD_SETTID).CLONE_CHILD_CLEARTID— the join mechanism: on exit the kernel zeroes&pd->tidand futex-wakes anyone inpthread_join.- The exit signal is deliberately zero (“The termination signal is chosen to be zero which means no signal is sent”) — a thread’s death must not send
SIGCHLDto anyone, unlike a child process.
Contrast this with fork(), which is clone with none of the sharing flags and exit_signal = SIGCHLD: a brand-new process with its own copies of everything. The full creation call path — how these flags actually drive copy_process() — is the subject of fork clone and exec System Calls.
Common Misunderstandings
“A thread is lighter because the kernel schedules it differently.” No. The kernel schedules tasks; a thread and a process are the same kind of schedulable object (task_struct) and go through the identical [[The Core Scheduler and __schedule|__schedule()]] path. Threads are “lighter” only at creation (no address-space copy) and in communication (shared memory), not in scheduling.
“getpid() returns the thread ID.” It returns the TGID, shared by all threads. The TID is gettid(). Confusing the two is the classic bug when sending signals: kill(getpid(), ...) targets the whole thread group, while tgkill/pthread_kill target one thread. See Process and Thread Identifiers in Linux.
“You can make a thread with just CLONE_THREAD.” The kernel rejects it: CLONE_THREAD demands CLONE_SIGHAND, which demands CLONE_VM. The dials have dependencies.
“CLONE_VM alone makes a thread.” It makes two tasks sharing an address space but not a thread group — they still have distinct TGIDs, distinct parents reporting via SIGCHLD, separate signal handlers. This is a valid but unusual configuration (some sandboxes and language runtimes use it); it is not a POSIX thread.
Uncertain
Verify: these flag values,
struct clone_argslayout, thecopy_process()dependency checks, and the glibc flag set are confirmed against 6.12 LTS sources and glibc 2.40. They are not re-checked against 6.18 LTS — these ABI constants are extremely stable (the flag values are a frozen user-space contract), so they almost certainly match, but this note does not assert 6.18 behavior. To resolve: diffinclude/uapi/linux/sched.handkernel/fork.cat thev6.18tag. uncertain
See Also
- fork clone and exec System Calls — the call path (
kernel_clone→copy_process) these flags drive - The task_struct Process Descriptor — the object every task is, and the sub-structures the flags share
- Process and Thread Identifiers in Linux — PID/TID/TGID/PGID and how the thread-group ID works
- Copy-on-Write and fork — how a non-
CLONE_VMchild gets a private address space cheaply - Process Exit wait and Zombie Reaping — the exit side, where
CLONE_CHILD_CLEARTIDfires - Futex and OS Synchronization Primitives — the futex wake that
pthread_joinrides on - Linux Containers and Isolation MOC — the
CLONE_NEW*namespace flags - Linux Process Scheduling MOC — parent map