---
title: task_struct 与进程生命周期
url: https://doc.liz6.com/linux-kernel/01-process-management/01-task-struct-and-process-lifecycle
locale: zh
area: linux-kernel
tags:
- linux-kernel
- 进程管理
date: 2026-06-30
modified: 2026-07-11
description: '覆盖: task_struct 核心字段与演进 → 进程状态机 → fork/exec/exit 完整路径 → 内核线程 内核版本: 2.6 ~ 6.x，重点标注 5.x+ 变更'
---

# task_struct 与进程生命周期

> 覆盖: task_struct 核心字段与演进 → 进程状态机 → fork/exec/exit 完整路径 → 内核线程
> 内核版本: 2.6 ~ 6.x，重点标注 5.x+ 变更

## 概述

Linux 中一切皆进程。内核线程是进程，用户线程也是进程——区别仅在于 `clone()` 时共享了什么。这套统一模型之所以能工作，全靠 `task_struct` 这个巨型结构体把调度、内存、文件、信号等子系统串联在一起。

本文从 task_struct 的核心字段出发，追踪一个进程从 `fork()` 诞生到 `exit()` 消亡的完整生命周期，解释每一步内核做了什么、数据结构如何变化。

---

## task_struct：进程描述符

定义在 `include/linux/sched.h`，是内核中最复杂的结构体之一（当前主线 ~700 行）。初次阅读不需要逐字段记忆，理解**分层归属**即可：

```mermaid
flowchart LR
    subgraph TS["task_struct (进程描述符)"]
        ID["🆔 身份标识<br/>pid, tgid<br/>comm (进程名)"]
        SCHED["⏱️ 状态与调度<br/>__state, prio<br/>se (CFS) / rt / dl"]
        MM["🧠 内存管理<br/>mm / active_mm<br/>vmacache"]
        FS["📂 文件与 IO<br/>files (fd table)<br/>fs (root/pwd)"]
        REL["👥 亲属关系<br/>parent / children<br/>sibling"]
        SIG["📡 信号<br/>signal (共享描述)<br/>sighand (处理表)"]
        STACK["📚 内核栈<br/>stack → thread_info"]
        STAT["📊 统计与审计<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
```

### 为什么线程也是进程？

Linux 不区分"进程"和"线程"——两者都是 `task_struct`。区别仅在于 `clone()` 的 flags：

```c
// fork(): 全新进程
clone(SIGCHLD, NULL);

// pthread_create(): 共享地址空间的"线程"
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND |
      CLONE_THREAD | CLONE_SYSVSEM, ...);

// vfork(): 阻塞父进程直到子进程 exec/exit (已基本被 fork+COW 取代)
clone(CLONE_VFORK | CLONE_VM | SIGCHLD, NULL);
```

关键 flags:
| Flag | 效果 |
|------|------|
| `CLONE_VM` | 共享 `mm_struct`（不复制地址空间） |
| `CLONE_FS` | 共享 `fs_struct`（root/pwd 一致） |
| `CLONE_FILES` | 共享 `files_struct`（fd 表共享） |
| `CLONE_SIGHAND` | 共享 `sighand_struct`（信号处理函数共享） |
| `CLONE_THREAD` | 同一线程组（tgid 与父进程相同） |
| `CLONE_VFORK` | 父进程阻塞直到子进程 exec/exit |

`getpid()` 返回的是 `tgid`（线程组 ID），不是 `pid`。同一个进程的所有线程有相同 tgid，但各自有独立的 pid。这就是为什么 `ps -eLf` 能看到同一个 PID 下列出多个 LWP。

### task_struct 如何被找到？

```c
// 当前进程的 task_struct (x86_64 上最快)
#define current get_current()

// 实现: 从内核栈指针的低位掩码得到 thread_info，再取 task
static __always_inline struct task_struct *get_current(void) {
    return this_cpu_read_stable(pcpu_hot.current_task);
}
```

5.x+ 已把 `task_struct *` 直接放进 per-CPU 变量 `pcpu_hot.current_task`，不再需要从内核栈指针间接计算。

---

## 进程状态机

```mermaid
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 : 信号 / wake_up()
    TASK_UNINTERRUPTIBLE --> TASK_RUNNING : IO 完成 / wake_up()
    TASK_STOPPED --> TASK_RUNNING : SIGCONT
    EXIT_ZOMBIE --> EXIT_DEAD : 父进程 wait()
    EXIT_DEAD --> [*] : RCU grace period
```

### 各状态详解

**TASK_RUNNING (R 状态)** — 正在 CPU 上执行，或在 runqueue 中等待调度。ps/top 看到的所有 R 状态进程都可能正在等 CPU，不一定是当前在跑的那个。
```bash
cat /proc/<pid>/status | grep State  # 也可看 /proc/<pid>/stat 第3字段
```

**TASK_INTERRUPTIBLE (S 状态)** — 可中断睡眠。绝大多数等待都是这种：等 pipe 数据、等 socket、等信号量。可以被信号唤醒（返回 `-EINTR`）。

**TASK_UNINTERRUPTIBLE (D 状态)** — 不可中断睡眠，典型场景是等磁盘 IO 完成。这个状态之所以存在，是因为某些 IO 路径不能被信号打断（比如写回页缓存时的 `lock_page()`）。D 状态过长 → top/负载中显示为 D state hang。

5.x+ 引入 **TASK_KILLABLE**：一种新的睡眠模式，挂载在 NFS 等慢 IO 时可用。和 UNINTERRUPTIBLE 的区别是——可以被 SIGKILL 杀死。这解决了经典的"NFS 挂了进程 D 状态永远杀不掉"问题。实现上是 `wait_event_killable()` 宏，内部检查 `fatal_signal_pending()`。

**TASK_STOPPED (T 状态)** — `SIGSTOP`/`SIGTRAP`/ptrace 暂停。进程被暂停后仍在进程表中，可以通过 `SIGCONT` 恢复。

**TASK_TRACED (t 状态)** — 被 ptrace 调试中。与 STOPPED 的区别在于：信号传递和 ptrace 事件处理方式不同。

**EXIT_ZOMBIE (Z 状态)** — 进程已死亡但 task_struct 还在，等待父进程 `wait()` 读取退出码。僵尸不占内存（mm 已释放），只占一个 PID 和一个 task_struct。

**EXIT_DEAD** — 父进程 `wait()` 之后，task_struct 等待 RCU grace period 后真正回收。这是为了防止其他 CPU 还持有 `rcu_dereference` 引用。

### 状态存储演进

```c
// 旧 (2.6 ~ 5.12): long state
task->state = TASK_UNINTERRUPTIBLE;

// 新 (5.14+): 通过 WRITE_ONCE/READ_ONCE 访问
WRITE_ONCE(task->__state, TASK_UNINTERRUPTIBLE);
```

改为 `__state` 的目的是强制使用 `WRITE_ONCE`/`READ_ONCE` 宏，避免编译器撕裂写（tearing）或乱序导致的状态不一致。

---

## fork：进程诞生

### 入口与路径

```c
// glibc: fork() → clone(SIGCHLD, ...)
// kernel (5.x 统一入口):
SYSCALL_DEFINE0(fork) → kernel_clone(SIGCHLD, ...)
SYSCALL_DEFINE2(clone3, ...) → kernel_clone(args->flags, ...)
```

`kernel_clone()` (`kernel/fork.c`) 是统一的 clone 入口（5.x 重构，之前叫 `_do_fork()`）。传入的 flags 决定了共享哪些资源。

### copy_process — 核心（~1500行）

<svg viewBox="0 0 720 390" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="copy_process 内部主要步骤:从复制 task_struct 到写入 pid/tgid">
  <rect width="720" height="390" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">copy_process():fork 内部的复制流水线(kernel/fork.c)</text>
  <text x="360" y="46" text-anchor="middle" font-size="11" fill="#64748b">大致按此顺序执行,部分步骤依 clone flags 可跳过</text>

  <rect x="30" y="54" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="73" font-size="10.5" fill="#3730a3">① dup_task_struct() · 复制 task_struct+内核栈(vmap栈)</text>

  <rect x="30" y="90" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="109" font-size="10.5" fill="#3730a3">② sched_fork() · 初始化调度实体 se/rt/dl</text>

  <rect x="30" y="126" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="145" font-size="10.5" fill="#3730a3">③ copy_semundo() · SysV sem undo</text>

  <rect x="30" y="162" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="181" font-size="10.5" fill="#3730a3">④ copy_files() · 复制 fd table</text>

  <rect x="30" y="198" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="217" font-size="10.5" fill="#3730a3">⑤ copy_fs() · 复制 fs_struct</text>

  <rect x="30" y="234" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="253" font-size="10.5" fill="#3730a3">⑥ copy_sighand() · 复制信号处理表</text>

  <rect x="30" y="270" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="289" font-size="10.5" fill="#3730a3">⑦ copy_signal() · 复制 signal_struct</text>

  <rect x="370" y="54" width="320" height="30" rx="6" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="384" y="73" font-size="10.5" fill="#0f766e">⑧ copy_mm() · COW 映射,只复制页表不复制物理页</text>

  <rect x="370" y="90" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="384" y="109" font-size="10.5" fill="#3730a3">⑨ copy_namespaces() · 复制 namespace</text>

  <rect x="370" y="126" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="384" y="145" font-size="10.5" fill="#3730a3">⑩ copy_io() · 复制 IO context</text>

  <rect x="370" y="162" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="384" y="181" font-size="10.5" fill="#3730a3">⑪ copy_thread() · 设置子进程返回地址</text>

  <rect x="370" y="198" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="384" y="217" font-size="10.5" fill="#3730a3">⑫ sched_post_fork() · 调度后处理</text>

  <rect x="370" y="234" width="320" height="30" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="384" y="253" font-size="10.5" fill="#3730a3">⑬ 写 pid / tgid</text>

  <rect x="30" y="316" width="660" height="54" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="46" y="336" font-size="12" fill="#115e59">关键在 ⑧ copy_mm():除非 CLONE_VM,dup_mmap() 只复制页表并标记只读(COW),</text>
  <text x="46" y="356" font-size="12" fill="#115e59">真正的数据复制推迟到写入触发的 page fault——这是 fork() 能做到 &lt;1ms 的原因。</text>
</svg>

### Copy-on-Write 是 fork 快的秘密

```c
// 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. 两个进程的 PTE 指向同一个物理页
    // 2. 两个 PTE 都标记为只读 (即使原来是可写的)
    // 3. 物理页的 refcount++
    
    if (is_cow_mapping(vma->vm_flags)) {
        ptep_set_wrprotect(src_mm, addr, src_pte);  // 父进程 PTE → 只读
        set_pte_at(dst_mm, addr, dst_pte, entry);   // 子进程 PTE → 只读
        atomic_inc(&page->_refcount);                 // 物理页 +1 引用
    }
}
```

后继 page fault 时才真正复制：
```
父子任一进程写入 → page fault → do_wp_page()
  → wp_page_copy() → alloc_page() → copy_user_highpage() → 各自可写
```

这就是为什么 `fork()` 可以 <1ms，而实际内存开销只发生在写入时。

### vfork 的微妙之处

`vfork()` 曾经是为了避免 fork 复制页表的开销（在 COW 出现之前）。现在已基本被 COW 淘汰，但在 `posix_spawn()` 路径中仍然使用——父进程阻塞在 `CLONE_VFORK` 上直到子进程 `exec`，避免无意义的 COW fault。内核通过 `completion` 机制实现父进程的阻塞等待。

---

## exec：脱胎换骨

```c
// fs/exec.c
SYSCALL_DEFINE3(execve, ...) → do_execve() → do_execveat_common() → exec_binprm()
```

exec 不创建新进程，而是把当前进程的地址空间整个换掉。PID 不变，但运行的代码、数据、栈全部换新。

### 执行流程

<svg viewBox="0 0 720 332" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="exec_binprm 六个阶段:从识别格式到返回用户态">
  <defs>
    <marker id="exah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="332" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">exec_binprm():exec 换血的六个阶段(fs/exec.c)</text>
  <text x="360" y="46" text-anchor="middle" font-size="11" fill="#64748b">顺序:先读左列 ①→③,再读右列 ④→⑥</text>

  <rect x="30" y="56" width="330" height="46" rx="7" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="74" font-size="12" font-weight="700" fill="#3730a3">① prepare_binprm()</text>
  <text x="44" y="90" font-size="10" fill="#4f46e5">读 ELF 头(前 256B)→ 识别格式:ELF / #! / a.out / misc</text>
  <line x1="195" y1="102" x2="195" y2="112" stroke="#475569" stroke-width="1.6" marker-end="url(#exah)"/>

  <rect x="30" y="112" width="330" height="62" rx="7" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="44" y="130" font-size="12" font-weight="700" fill="#3730a3">② 搜索 binary handler</text>
  <text x="44" y="146" font-size="10" fill="#4f46e5">search_binary_handler() 遍历 formats 链表</text>
  <text x="44" y="160" font-size="10" fill="#4f46e5">load_elf_binary / load_script(#!) / load_misc_binary</text>
  <line x1="195" y1="174" x2="195" y2="184" stroke="#475569" stroke-width="1.6" marker-end="url(#exah)"/>

  <rect x="30" y="184" width="330" height="62" rx="7" fill="#e2e8f0" stroke="#cbd5e1"/>
  <text x="44" y="202" font-size="12" font-weight="700" fill="#334155">③ flush_old_exec()</text>
  <text x="44" y="218" font-size="10" fill="#475569">de_thread() 脱线程组 → flush_old_files() 关 O_CLOEXEC fd</text>
  <text x="44" y="232" font-size="10" fill="#475569">→ exec_mmap() 释放旧 mm、创建新 mm</text>

  <rect x="370" y="56" width="330" height="62" rx="7" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="384" y="74" font-size="12" font-weight="700" fill="#3730a3">④ setup_new_exec()</text>
  <text x="384" y="90" font-size="10" fill="#4f46e5">__set_task_comm() 设新进程名(截断 15 字符)</text>
  <text x="384" y="104" font-size="10" fill="#4f46e5">setup_arg_pages() 布置栈:argv + envp + auxv</text>
  <line x1="535" y1="118" x2="535" y2="128" stroke="#475569" stroke-width="1.6" marker-end="url(#exah)"/>

  <rect x="370" y="128" width="330" height="62" rx="7" fill="#ccfbf1" stroke="#99f6e4"/>
  <text x="384" y="146" font-size="12" font-weight="700" fill="#0f766e">⑤ load_elf_binary()</text>
  <text x="384" y="162" font-size="10" fill="#0f766e">elf_map()→vm_mmap() 映射 LOAD 段;set_brk() 设堆起始</text>
  <text x="384" y="176" font-size="10" fill="#0f766e">load_elf_interp() 加载 ld.so;start_thread() 设 IP→entry</text>
  <line x1="535" y1="190" x2="535" y2="200" stroke="#475569" stroke-width="1.6" marker-end="url(#exah)"/>

  <rect x="370" y="200" width="330" height="46" rx="7" fill="#dcfce7" stroke="#4ade80"/>
  <text x="384" y="218" font-size="12" font-weight="700" fill="#166534">⑥ 返回用户态</text>
  <text x="384" y="234" font-size="10" fill="#15803d">从 entry point(或 ld.so)开始执行</text>

  <rect x="30" y="262" width="660" height="50" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="46" y="292" font-size="12" fill="#3730a3">exec 不创建新进程,而是把当前进程的地址空间整个换掉——PID 不变,但代码、数据、栈全部换新。</text>
</svg>

### setuid/setgid 与 credential

如果 ELF 文件设置了 suid bit，exec 还会替换 credential：
```c
// fs/exec.c
if (bprm->cred->uid != bprm->file->f_owner.uid) {
    // setuid binary: 替换 euid/egid 等
    bprm->cred->euid = bprm->file->f_owner.uid;
}
```

这是内核中权限模型的核心——`prepare_binprm()` 中完成。

---

## exit：进程消亡

### do_exit 路径

```c
// kernel/exit.c
void __noreturn do_exit(long code) {
    // 1. 设标志
    tsk->flags |= PF_EXITING;
    
    // 2. 释放资源
    exit_mm(tsk);        // 释放 mm_struct (如果没有其他线程共享)
    exit_sem(tsk);       // SysV sem undo
    exit_shm(tsk);       // SysV shm
    exit_files(tsk);     // 关闭文件 (如果没有共享)
    exit_fs(tsk);        // 释放 fs_struct
    exit_task_namespaces(tsk);
    exit_rcu(tsk);
    
    // 3. 通知
    exit_notify(tsk, group_dead);
    //    ├─ 给子进程找新父进程 (reparent)
    //    └─ 给父进程发 SIGCHLD
    
    // 4. 最后一次调度 — 永不再返回
    do_task_dead();
}
```

### 僵尸进程详解

<svg viewBox="0 0 720 270" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="僵尸进程回收链:do_exit 到真正释放的三个阶段">
  <defs>
    <marker id="zoah" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker>
  </defs>
  <rect width="720" height="270" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">僵尸进程回收链:do_exit() 到真正释放的三个阶段</text>

  <rect x="30" y="60" width="200" height="110" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="130" y="80" text-anchor="middle" font-size="13" font-weight="700" fill="#3730a3">EXIT_ZOMBIE</text>
  <text x="130" y="94" text-anchor="middle" font-size="10" fill="#4f46e5">(do_exit() 后)</text>
  <text x="42" y="112" font-size="10" fill="#3730a3">· task_struct 还在</text>
  <text x="42" y="126" font-size="10" fill="#3730a3">· mm/files 可能已释放</text>
  <text x="42" y="140" font-size="10" fill="#3730a3">  (无其他线程共享时)</text>

  <line x1="230" y1="115" x2="260" y2="115" stroke="#475569" stroke-width="1.6" marker-end="url(#zoah)"/>
  <text x="245" y="108" text-anchor="middle" font-size="9.5" fill="#64748b">父进程 wait()</text>

  <rect x="260" y="60" width="200" height="110" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="360" y="80" text-anchor="middle" font-size="13" font-weight="700" fill="#115e59">父进程 wait()</text>
  <text x="360" y="94" text-anchor="middle" font-size="10" fill="#0f766e">→ release_task()</text>
  <text x="272" y="112" font-size="10" fill="#115e59">· __exit_signal() 清理信号</text>
  <text x="272" y="126" font-size="10" fill="#115e59">· __unhash_process() 移出</text>
  <text x="272" y="140" font-size="10" fill="#115e59">  PID hash</text>

  <line x1="460" y1="115" x2="490" y2="115" stroke="#475569" stroke-width="1.6" marker-end="url(#zoah)"/>
  <text x="475" y="108" text-anchor="middle" font-size="9.5" fill="#64748b">grace period</text>

  <rect x="490" y="60" width="200" height="110" rx="8" fill="#dcfce7" stroke="#4ade80"/>
  <text x="590" y="80" text-anchor="middle" font-size="13" font-weight="700" fill="#166534">RCU grace period 后</text>
  <text x="590" y="94" text-anchor="middle" font-size="10" fill="#15803d">真正回收</text>
  <text x="502" y="112" font-size="10" fill="#166534">· put_task_struct() 延迟释放</text>
  <text x="502" y="126" font-size="10" fill="#166534">· delayed_put_task_struct()</text>
  <text x="502" y="140" font-size="10" fill="#166534">· free_task() → kmem_cache_free()</text>

  <rect x="30" y="190" width="660" height="54" rx="8" fill="#f0fdfa" stroke="#99f6e4"/>
  <text x="46" y="210" font-size="12" fill="#115e59">僵尸不占内存(mm 已释放),只占一个 PID 和 task_struct;若父进程不 wait(),</text>
  <text x="46" y="228" font-size="12" fill="#115e59">只能 kill 父进程,由 init(PID 1)收养后自动回收——这是 PID 1 循环 wait() 的原因。</text>
</svg>

**孤儿进程收养**：如果父进程先于子进程退出，`exit_notify()` 会调用 `find_new_reaper()` 把子进程过继给：
- 同一线程组的其他线程
- 同一 PID namespace 的 `init` (PID 1)

这就是为什么 PID 1 不会让僵尸积累——`init` 进程主动循环 `wait()`。

### 僵尸进程调试

```bash
# 查找僵尸进程
ps aux | awk '$8 == "Z" { print $2, $11 }'

# 僵尸占用的资源
cat /proc/<zombie_pid>/status  # 能看到 State: Z，但大多数字段是 0

# 僵尸的父进程
cat /proc/<zombie_pid>/stat | awk '{print "PPID:", $4}'

# 如果父进程不 wait()，只能 kill 父进程
# 然后 init 收养僵尸 → 自动回收
```

---

## 内核线程

内核线程是没有用户空间的特殊进程（`mm == NULL`）。它们运行在内核态，创建和管理不同于用户进程。

```c
// 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);

// 快捷版: 创建 + 唤醒一步到位
#define kthread_run(threadfn, data, namefmt, ...)
```

### kthreadd (PID 2)

所有内核线程的父进程。启动路径：
```
start_kernel() → rest_init() → kernel_thread(kernel_init, ...)
                                    └─ kernel_thread(kthreadd, ...)
```

`kthreadd` 循环处理 `kthread_create_list`：取出请求 → 调用 `create_kthread()` → `kernel_thread()` 创建新线程。

### 内存与调度特点

```c
// 内核线程没有自己的 mm，借用上一个进程的 active_mm
task->mm = NULL;
task->active_mm = &init_mm;  // 全局内核地址空间引用

// 上下文切换时:
// if (prev->mm == NULL)  // 前一个进程是内核线程
//     enter_lazy_tlb(prev->active_mm, next);
//     不切换页表，因为内核地址空间是共享的
```

这解释了为什么内核线程上下文切换极快——不需要刷 TLB（x86: 不写 CR3）。

### 常见内核线程

| 线程 | 作用 |
|------|------|
| `ksoftirqd/<N>` | 处理 softirq（网络收包、timer 等） |
| `kworker/<N>:<name>` | workqueue worker |
| `migration/<N>` | 调度器负载均衡 |
| `rcu_gp / rcu_par_gp` | RCU grace period 管理 |
| `jbd2/<dev>-<N>` | ext4 journal 提交 |
| `kcompactd<N>` | 内存碎片整理 |

```bash
# 查看所有内核线程
ps -eo pid,comm,state | grep -E '\[.*\]' | head -20

# kthreadd 的子进程树
pstree -p 2
```

---

## 关键 /proc 接口

```bash
# /proc/<pid>/stat — 进程状态（纯数字，快速解析）
cat /proc/1/stat
# 字段: pid comm state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime ...

# /proc/<pid>/status — 人类可读版本
cat /proc/self/status
# 含: Name, State, Tgid, Pid, PPid, Uid, VmSize, VmRSS, Threads, voluntary_ctxt_switches, nonvoluntary_ctxt_switches

# /proc/<pid>/task/<tid>/ — 线程目录
ls /proc/self/task/  # 每个线程一个目录

# /proc/sys/kernel/pid_max — PID 上限 (默认 32768，可调到 4194304)
cat /proc/sys/kernel/pid_max
```

---

## 参考与延伸

- **内核文档**: `Documentation/filesystems/proc.rst`, `Documentation/scheduler/sched-design-CFS.rst`
- **LWN 深度文章**: 
  - "The task_struct and process creation" 系列
  - "clone(), fork(), and vfork()" (lwn.net/Articles/176929/)
  - "TASK_KILLABLE" (lwn.net/Articles/288056/)
- **源码文件**:
  - `include/linux/sched.h` — task_struct 定义
  - `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 管理

---

*关键词: task_struct, fork, COW, exec, zombie, kernel_clone, TASK_KILLABLE, 内核线程, kthreadd*
