On this page
task_struct and Process Lifecycle
Coverage: task_struct core fields and evolution → Process state machine → Complete fork/exec/exit path → Kernel threads Kernel version: 2.6 ~ 6.x, with emphasis on 5.x+ changes
Overview
In Linux, everything is a process. Kernel threads are processes, and user threads are also processes—the only difference lies in what is shared during clone(). This unified model works thanks to the massive task_struct structure, which ties together subsystems such as scheduling, memory, files, and signals.
This article starts with the core fields of task_struct, tracing the complete lifecycle of a process from its birth via fork() to its demise via exit(), explaining what the kernel does at each step and how data structures change.
task_struct: The Process Descriptor
Defined in include/linux/sched.h, it is one of the most complex structures in the kernel (currently ~700 lines in the mainline). You don't need to memorize every field on the first read; understanding the hierarchical grouping is sufficient:
flowchart LR
subgraph TS["task_struct (Process Descriptor)"]
ID["🆔 Identity<br/>pid, tgid<br/>comm (process name)"]
SCHED["⏱️ State and Scheduling<br/>__state, prio<br/>se (CFS) / rt / dl"]
MM["🧠 Memory Management<br/>mm / active_mm<br/>vmacache"]
FS["📂 Files and IO<br/>files (fd table)<br/>fs (root/pwd)"]
REL["👥 Relationships<br/>parent / children<br/>sibling"]
SIG["📡 Signals<br/>signal (shared descriptor)<br/>sighand (handler table)"]
STACK["📚 Kernel Stack<br/>stack → thread_info"]
STAT["📊 Statistics and Auditing<br/>start_time, utime/stime<br/>sum_exec_runtime"]
end
classDef ts fill:#e3f2fd,stroke:#1565c0
classDef cat fill:#fff3e0,stroke:#ef6c00
class ID,SCHED,MM,FS,REL,SIG,STACK,STAT cat
Why are threads also processes?
Linux does not distinguish between "processes" and "threads"—both are task_struct. The difference lies only in the clone() flags:
// fork(): brand new process
;
// pthread_create(): a "thread" sharing address space
;
// vfork(): blocks the parent until the child execs/exits (largely replaced by fork+COW)
;
Key flags:
| Flag | Effect |
|---|---|
CLONE_VM | Share mm_struct (do not copy address space) |
CLONE_FS | Share fs_struct (consistent root/pwd) |
CLONE_FILES | Share files_struct (shared fd table) |
CLONE_SIGHAND | Share sighand_struct (shared signal handlers) |
CLONE_THREAD | Same thread group (tgid same as parent) |
CLONE_VFORK | Parent blocks until child execs/exits |
getpid() returns the tgid (Thread Group ID), not the pid. All threads of the same process share the same tgid, but each has its own independent pid. This is why ps -eLf can list multiple LWPs under the same PID.
How is task_struct found?
// task_struct of the current process (fastest on x86_64)
// Implementation: get thread_info from the low bits mask of the kernel stack pointer, then get task
static __always_inline struct task_struct *
In 5.x+, the task_struct * is placed directly into the per-CPU variable pcpu_hot.current_task, eliminating the need for indirect calculation from the kernel stack pointer.
Process State Machine
stateDiagram-v2
[*] --> TASK_RUNNING : fork()
TASK_RUNNING --> TASK_INTERRUPTIBLE : wait_event_*()
TASK_RUNNING --> TASK_UNINTERRUPTIBLE : io_schedule()
TASK_RUNNING --> TASK_STOPPED : SIGSTOP / ptrace
TASK_RUNNING --> EXIT_ZOMBIE : do_exit()
TASK_INTERRUPTIBLE --> TASK_RUNNING : Signal / wake_up()
TASK_UNINTERRUPTIBLE --> TASK_RUNNING : IO Complete / wake_up()
TASK_STOPPED --> TASK_RUNNING : SIGCONT
EXIT_ZOMBIE --> EXIT_DEAD : Parent wait()
EXIT_DEAD --> [*] : RCU grace period
Detailed State Descriptions
TASK_RUNNING (R State) — Executing on the CPU, or waiting in the runqueue for scheduling. All R-state processes seen by ps/top might be waiting for CPU time; they are not necessarily the one currently running.
|
TASK_INTERRUPTIBLE (S State) — Interruptible sleep. Most waits are of this type: waiting for pipe data, sockets, or semaphores. It can be woken up by a signal (returning -EINTR).
TASK_UNINTERRUPTIBLE (D State) — Uninterruptible sleep, typically waiting for disk IO to complete. This state exists because certain IO paths cannot be interrupted by signals (e.g., lock_page() during page cache writeback). Prolonged D state → appears as a D state hang in top/load averages.
In 5.x+, TASK_KILLABLE was introduced: a new sleep mode usable during slow IO like NFS. The difference from UNINTERRUPTIBLE is that it can be killed by SIGKILL. This solves the classic problem of "NFS hangs causing processes to be stuck in D state and unkillable." The implementation uses the wait_event_killable() macro, which internally checks fatal_signal_pending().
TASK_STOPPED (T State) — Paused by SIGSTOP/SIGTRAP/ptrace. The process remains in the process table while paused and can be resumed via SIGCONT.
TASK_TRACED (t State) — Being debugged via ptrace. The difference from STOPPED is that signal delivery and ptrace event handling mechanisms differ.
EXIT_ZOMBIE (Z State) — The process is dead but the task_struct remains, waiting for the parent to wait() to read the exit code. Zombies do not consume memory (mm is released), occupying only one PID and one task_struct.
EXIT_DEAD — After the parent wait(), the task_struct waits for the RCU grace period before being truly reclaimed. This prevents other CPUs from still holding rcu_dereference references.
Evolution of State Storage
// Old (2.6 ~ 5.12): long state
task->state = TASK_UNINTERRUPTIBLE;
// New (5.14+): Access via WRITE_ONCE/READ_ONCE
;
The change to __state aims to enforce the use of WRITE_ONCE/READ_ONCE macros, avoiding state inconsistencies caused by compiler tearing or out-of-order execution.
fork: Process Birth
Entry and Path
// glibc: fork() → clone(SIGCHLD, ...)
// kernel (unified entry in 5.x):
→
→
kernel_clone() (kernel/fork.c) is the unified clone entry (refactored in 5.x, previously called _do_fork()). The passed flags determine which resources are shared.
copy_process — The Core (~1500 lines)
Copy-on-Write is the Secret to Fast fork
// kernel/fork.c: copy_page_range() → copy_p4d_range() → ... → copy_pte_range()
static inline int
Actual copying happens only on subsequent page faults:
Either parent or child writes → page fault → do_wp_page()
→ wp_page_copy() → alloc_page() → copy_user_highpage() → both become writable
This is why fork() can be completed in <1ms, with actual memory overhead occurring only upon write.
The Nuance of vfork
vfork() was originally created to avoid the overhead of fork copying page tables (before COW existed). It is largely obsolete now, but still used in the posix_spawn() path—the parent blocks on CLONE_VFORK until the child execs, avoiding meaningless COW faults. The kernel implements the parent's blocking wait via the completion mechanism.
exec: Metamorphosis
// fs/exec.c
→ → →
exec does not create a new process; instead, it completely replaces the current process's address space. The PID remains unchanged, but the code, data, and stack are all new.
Execution Flow
setuid/setgid and Credentials
If the ELF file has the suid bit set, exec will also replace the credentials:
// fs/exec.c
if
This is the core of the kernel's privilege model—completed in prepare_binprm().
exit: Process Demise
do_exit Path
// kernel/exit.c
void __noreturn
Zombie Process Details
Orphan Process Adoption: If the parent exits before the child, exit_notify() calls find_new_reaper() to re-parent the child to:
- Other threads in the same thread group
init(PID 1) in the same PID namespace
This is why PID 1 does not accumulate zombies—the init process actively loops wait().
Zombie Process Debugging
# Find zombie processes
|
# Resources occupied by zombies
# Parent of the zombie
|
# If the parent does not wait(), you can only kill the parent
# Then init adopts the zombie → automatic reclamation
Kernel Threads
Kernel threads are special processes without user space (mm == NULL). They run in kernel mode and are created and managed differently from user processes.
// include/linux/kthread.h
struct task_struct *;
void ;
int ;
bool ;
// Shortcut: Create + wake up in one step
kthreadd (PID 2)
The parent of all kernel threads. Startup path:
start_kernel() → rest_init() → kernel_thread(kernel_init, ...)
└─ kernel_thread(kthreadd, ...)
kthreadd loops processing kthread_create_list: take request → call create_kthread() → kernel_thread() to create a new thread.
Memory and Scheduling Characteristics
// Kernel threads have no own mm, borrowing the previous process's active_mm
task->mm = NULL;
task->active_mm = &init_mm; // Reference to global kernel address space
// During context switch:
// if (prev->mm == NULL) // Previous process was a kernel thread
// enter_lazy_tlb(prev->active_mm, next);
// Do not switch page tables, as the kernel address space is shared
This explains why kernel thread context switching is extremely fast—no need to flush the TLB (x86: do not write CR3).
Common Kernel Threads
| Thread | Role |
|---|---|
ksoftirqd/<N> | Handle softirqs (network RX, timers, etc.) |
kworker/<N>:<name> | workqueue worker |
migration/<N> | Scheduler load balancing |
rcu_gp / rcu_par_gp | RCU grace period management |
jbd2/<dev>-<N> | ext4 journal commit |
kcompactd<N> | Memory defragmentation |
# View all kernel threads
| |
# Child process tree of kthreadd
Key /proc Interfaces
# /proc/<pid>/stat — Process state (pure numbers, fast parsing)
# Fields: pid comm state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime ...
# /proc/<pid>/status — Human-readable version
# Contains: Name, State, Tgid, Pid, PPid, Uid, VmSize, VmRSS, Threads, voluntary_ctxt_switches, nonvoluntary_ctxt_switches
# /proc/<pid>/task/<tid>/ — Thread directory
# /proc/sys/kernel/pid_max — PID upper limit (default 32768, adjustable to 4194304)
References and Extensions
- Kernel Documentation:
Documentation/filesystems/proc.rst,Documentation/scheduler/sched-design-CFS.rst - LWN In-depth Articles:
- "The task_struct and process creation" series
- "clone(), fork(), and vfork()" (lwn.net/Articles/176929/)
- "TASK_KILLABLE" (lwn.net/Articles/288056/)
- Source Files:
include/linux/sched.h— task_struct definitionkernel/fork.c— copy_process(), kernel_clone()fs/exec.c— do_execve(), exec_binprm(), load_elf_binary()kernel/exit.c— do_exit(), release_task()kernel/kthread.c— kthreadd management
Keywords: task_struct, fork, COW, exec, zombie, kernel_clone, TASK_KILLABLE, kernel threads, kthreadd