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
clone(SIGCHLD, NULL);

// pthread_create(): a "thread" sharing address space
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
      CLONE_THREAD | CLONE_SYSVSEM, ...);

// vfork(): blocks the parent until the child execs/exits (largely replaced by fork+COW)
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, NULL);

Key flags:

FlagEffect
CLONE_VMShare mm_struct (do not copy address space)
CLONE_FSShare fs_struct (consistent root/pwd)
CLONE_FILESShare files_struct (shared fd table)
CLONE_SIGHANDShare sighand_struct (shared signal handlers)
CLONE_THREADSame thread group (tgid same as parent)
CLONE_VFORKParent 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)
#define current get_current()

// Implementation: get thread_info from the low bits mask of the kernel stack pointer, then get task
static __always_inline struct task_struct *get_current(void) {
    return this_cpu_read_stable(pcpu_hot.current_task);
}

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.

cat /proc/<pid>/status | grep State  # Also check field 3 in /proc/<pid>/stat

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
WRITE_ONCE(task->__state, TASK_UNINTERRUPTIBLE);

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):
SYSCALL_DEFINE0(fork)kernel_clone(SIGCHLD, ...)
SYSCALL_DEFINE2(clone3, ...)kernel_clone(args->flags, ...)

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_process(): The copy pipeline inside fork (kernel/fork.c) Executed roughly in this order; some steps may be skipped depending on clone flags ① dup_task_struct() · Copy task_struct + kernel stack (vmap stack) ② sched_fork() · Initialize scheduling entity se/rt/dl ③ copy_semundo() · SysV sem undo ④ copy_files() · Copy fd table ⑤ copy_fs() · Copy fs_struct ⑥ copy_sighand() · Copy signal handler table ⑦ copy_signal() · Copy signal_struct ⑧ copy_mm() · COW mapping, copy page tables but not physical pages ⑨ copy_namespaces() · Copy namespaces ⑩ copy_io() · Copy IO context ⑪ copy_thread() · Set child process return address ⑫ sched_post_fork() · Post-scheduling processing ⑬ Write pid / tgid Key is ⑧ copy_mm(): Unless CLONE_VM, dup_mmap() only copies page tables and marks them read-only (COW), actual data copying is deferred until a write triggers a page fault—this is why fork() can be done in <1ms.

Copy-on-Write is the Secret to Fast fork

// kernel/fork.c: copy_page_range() → copy_p4d_range() → ... → copy_pte_range()
static inline int copy_present_pte(struct vm_area_struct *dst_vma, ...) {
    // 1. Both processes' PTEs point to the same physical page
    // 2. Both PTEs are marked as read-only (even if originally writable)
    // 3. Physical page refcount++
    
    if (is_cow_mapping(vma->vm_flags)) {
        ptep_set_wrprotect(src_mm, addr, src_pte);  // Parent PTE → read-only
        set_pte_at(dst_mm, addr, dst_pte, entry);   // Child PTE → read-only
        atomic_inc(&page->_refcount);                 // Physical page +1 reference
    }
}

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
SYSCALL_DEFINE3(execve, ...)do_execve()do_execveat_common()exec_binprm()

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

exec_binprm(): The six stages of exec blood replacement (fs/exec.c) Order: Read left column ①→③ first, then right column ④→⑥ ① prepare_binprm() Read ELF header (first 256B) → Identify format: ELF / #! / a.out / misc ② Search binary handler search_binary_handler() iterates through the formats list load_elf_binary / load_script(#!) / load_misc_binary ③ flush_old_exec() de_thread() detach from thread group → flush_old_files() close O_CLOEXEC fds → exec_mmap() release old mm, create new mm ④ setup_new_exec() __set_task_comm() set new process name (truncated to 15 chars) setup_arg_pages() arrange stack: argv + envp + auxv ⑤ load_elf_binary() elf_map()→vm_mmap() map LOAD segments; set_brk() set heap start load_elf_interp() load ld.so; start_thread() set IP→entry ⑥ Return to user mode Start execution from entry point (or ld.so) exec does not create a new process, but completely replaces the current process's address space—PID remains unchanged, but code, data, and stack are all new.

setuid/setgid and Credentials

If the ELF file has the suid bit set, exec will also replace the credentials:

// fs/exec.c
if (bprm->cred->uid != bprm->file->f_owner.uid) {
    // setuid binary: replace euid/egid etc.
    bprm->cred->euid = bprm->file->f_owner.uid;
}

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 do_exit(long code) {
    // 1. Set flags
    tsk->flags |= PF_EXITING;
    
    // 2. Release resources
    exit_mm(tsk);        // Release mm_struct (if no other threads share it)
    exit_sem(tsk);       // SysV sem undo
    exit_shm(tsk);       // SysV shm
    exit_files(tsk);     // Close files (if not shared)
    exit_fs(tsk);        // Release fs_struct
    exit_task_namespaces(tsk);
    exit_rcu(tsk);
    
    // 3. Notify
    exit_notify(tsk, group_dead);
    //    ├─ Find new parent for children (reparent)
    //    └─ Send SIGCHLD to parent
    
    // 4. Final scheduling — never returns
    do_task_dead();
}

Zombie Process Details

Zombie process reclamation chain: Three stages from do_exit() to true release EXIT_ZOMBIE (After do_exit()) · task_struct still exists · mm/files may already be released (when no other threads share them) Parent wait() Parent wait() → release_task() · __exit_signal() clean up signals · __unhash_process() remove from PID hash grace period After RCU grace period True reclamation · put_task_struct() delayed release · delayed_put_task_struct() · free_task() → kmem_cache_free() Zombies do not consume memory (mm is released), occupying only one PID and task_struct; if the parent does not wait(), the only option is to kill the parent, after which init (PID 1) adopts it and automatically reclaims it—this is why PID 1 loops wait().

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
ps aux | awk '$8 == "Z" { print $2, $11 }'

# Resources occupied by zombies
cat /proc/<zombie_pid>/status  # You can see State: Z, but most fields are 0

# Parent of the zombie
cat /proc/<zombie_pid>/stat | awk '{print "PPID:", $4}'

# 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 *kthread_create(int (*threadfn)(void *data), ...);
void kthread_bind(struct task_struct *k, unsigned int cpu);
int kthread_stop(struct task_struct *k);
bool kthread_should_stop(void);

// Shortcut: Create + wake up in one step
#define kthread_run(threadfn, data, namefmt, ...)

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

ThreadRole
ksoftirqd/<N>Handle softirqs (network RX, timers, etc.)
kworker/<N>:<name>workqueue worker
migration/<N>Scheduler load balancing
rcu_gp / rcu_par_gpRCU grace period management
jbd2/<dev>-<N>ext4 journal commit
kcompactd<N>Memory defragmentation
# View all kernel threads
ps -eo pid,comm,state | grep -E '\[.*\]' | head -20

# Child process tree of kthreadd
pstree -p 2

Key /proc Interfaces

# /proc/<pid>/stat — Process state (pure numbers, fast parsing)
cat /proc/1/stat
# 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
cat /proc/self/status
# Contains: Name, State, Tgid, Pid, PPid, Uid, VmSize, VmRSS, Threads, voluntary_ctxt_switches, nonvoluntary_ctxt_switches

# /proc/<pid>/task/<tid>/ — Thread directory
ls /proc/self/task/  # One directory per thread

# /proc/sys/kernel/pid_max — PID upper limit (default 32768, adjustable to 4194304)
cat /proc/sys/kernel/pid_max

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 definition
    • kernel/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