﻿# task_struct 私有字段 —— 进程描述符本体详解

> 这是 [../task-struct.md](/concepts/process/task-struct.md) 里"task_struct 本体存什么"的展开。本篇把 task_struct 里**不通过指针挂出去、只属于这个 task 自己的字段**拆开讲透：身份标识、进程树关系、状态机、调度参数、线程上下文（寄存器+内核栈）、统计计数、任务标志位、以及内核怎么把所有 task 串在一起。可共享的资源对象（mm/files/signal/cred/nsproxy...）另见独立文档。

## 零、一句话认知：task_struct 的字段分两类

```bash
task_struct ≈ 私有字段（本篇） + 资源对象指针（各子文档）
```

- **私有字段**：pid/tgid、状态、调度、寄存器、统计——这些只描述**这一个 task 本身**，不和别的 task 共享。
- **资源指针**：`mm`/`files`/`fs`/`signal`/`cred`/`nsproxy`/`io_context`...——指向独立分配、可被多 task 共享的对象。

> 理解这个分界是理解 fork/clone 的关键：fork 复制私有字段（然后 CoW）、clone 的 flags 决定资源指针是复制还是共享。

## 一、身份标识 —— 内核怎么区分每一个 task

### 1.1 pid / tgid：用户看到的 PID 和 TID

用户说的"进程 PID"在内核里叫 **tgid（线程组 ID）**，用户说的"线程 TID"才是内核的 **pid**。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<a>> #E3F2FD
  BorderColor<<a>> #1976D2
  BackgroundColor<<b>> #C8E6C9
  BorderColor<<b>> #388E3C
}
rectangle "用户视角" <<a>> as USER {
  rectangle "进程 PID=1000\n(3 个线程)" as PROC
}
rectangle "内核视角" <<b>> as KERNEL {
  rectangle "task A\npid=1000, tgid=1000\n(主线程, group_leader)" as A
  rectangle "task B\npid=1001, tgid=1000" as B
  rectangle "task C\npid=1002, tgid=1000" as C
}
PROC --> A : getpid()=tgid
PROC --> B : gettid()=pid
PROC --> C
note right
主线程 pid==tgid, group_leader 指向它\n同一线程组的 task 通过 thread_group 链表串在一起
end note
@enduml
```

| 字段 | 含义 | 谁设置 | 谁读取 |
|------|------|--------|--------|
| `pid` | 线程 ID（内核视角的任务号） | `alloc_pid()` 分配 | `gettid()`、`/proc/<tid>/` |
| `tgid` | 线程组 ID（用户视角的进程号） | clone 时继承或新建 | `getpid()`、`/proc/<tgid>/` |
| `group_leader` | 指向本线程组的主线程 task_struct | — | 遍历同组线程的入口 |

### 1.2 comm —— 进程名

`task->comm[TASK_COMM_LEN]`（16 字节）：`ps`/`top` 显示的进程名。`prctl(PR_SET_NAME)` 或 `pthread_setname_np()` 改它。**注意**：`comm` 不是 argv[0]，`execve` 时会用 basename 覆盖前 15 字节。

### 1.3 进程号分配 —— pid namespace 的层次

`task->pids[PIDTYPE_PID/PIDTYPE_PGID/PIDTYPE_SID]`：记录本 task 在**每一层 PID namespace** 中的编号。PID namespace 可以嵌套，同一 task 在宿主的 PID（比如 12345）和容器内的 PID（比如 1）**同时存在**——靠 `struct pid` 的层级链表实现。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<h>> #FFE0B2
  BorderColor<<h>> #EF6C00
  BackgroundColor<<c>> #E3F2FD
  BorderColor<<c>> #1976D2
}
rectangle "宿主 PID namespace\n(最外层, level=0)" <<h>> as HOST {
  rectangle "struct upid { nr=12345, ns=... }" as HUPID
}
rectangle "容器 PID namespace\n(内层, level=1)" <<c>> as CTR {
  rectangle "struct upid { nr=1, ns=... }" as CUPID
}
HUPID --> CUPID : 同一个 struct pid 挂着\n两层 upid(每层一个号)
note bottom of CUPID : /proc/12345/status→NSpid: 1 12345\n容器内看 pid=1, 宿主看 pid=12345
@enduml
```

## 二、进程树 —— task 之间怎么串成父子关系

每个 task 都有一组指针把自己嵌进"进程树"：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<p>> #E3F2FD
  BorderColor<<p>> #1976D2
  BackgroundColor<<c>> #C8E6C9
  BorderColor<<c>> #388E3C
}
rectangle "task_struct (父)" <<p>> as PARENT {
  rectangle "children\n(list_head 链表头)" as CHL
}
rectangle "task_struct (子1)" <<c>> as C1 {
  rectangle "parent" as P1
  rectangle "real_parent" as RP1
  rectangle "sibling\n(链表节点)" as S1
}
rectangle "task_struct (子2)" <<c>> as C2 {
  rectangle "parent" as P2
  rectangle "real_parent" as RP2
  rectangle "sibling\n(链表节点)" as S2
}
CHL --> S1 : 下一个
CHL --> S2
S1 --> S2 : 双向链表
P1 --> PARENT
P2 --> PARENT
note bottom of PARENT : parent=real_parent 除非被 ptrace\n(ptrace 期间 parent=调试器, real_parent=真父)
@enduml
```

| 字段 | 类型 | 含义 |
|------|------|------|
| `real_parent` | `struct task_struct *` | **真正的父进程**——fork 它的那个 task |
| `parent` | `struct task_struct *` | **当前父进程**——通常 = real_parent，但被 ptrace 时会指向调试器（gdb/strace 就是靠这个"偷走"子进程） |
| `children` | `struct list_head` | 所有**直接子进程**的链表头 |
| `sibling` | `struct list_head` | 本 task 在父进程 children 链表中的节点 |
| `thread_group` | `struct list_head` | 本 task 在**线程组链表**中的节点（用于遍历同进程所有线程） |

> **ptrace 怎么"偷父" **：当你 `gdb ./a.out` 或 `strace ls`，调试器对被调试进程执行 `PTRACE_ATTACH`。内核把被调试进程的 `parent` 从 `real_parent` **临时改成调试器**。这是为了让 `waitpid()` 能捕捉到被调试进程的信号/退出——父进程的 `wait` 收不到，调试器先拦截。

## 三、进程状态 —— task->__state 状态机

### 3.1 五大基本状态

```plantuml
@startuml
skinparam shadowing false
skinparam state {
  BackgroundColor<<R>> #E8F5E9
  BorderColor<<R>> #2E7D32
  BackgroundColor<<S>> #E3F2FD
  BorderColor<<S>> #1565C0
  BackgroundColor<<D>> #FFF3E0
  BorderColor<<D>> #E65100
  BackgroundColor<<T>> #F3E5F5
  BorderColor<<T>> #7B1FA2
  BackgroundColor<<Z>> #FFEBEE
  BorderColor<<Z>> #C62828
}
state "TASK_RUNNING (R)\n可运行/正在运行\n(在 runqueue 里)" <<R>> as R
state "TASK_INTERRUPTIBLE (S)\n可中断睡眠\n等 lock/sleep/数据\n能被信号打断" <<S>> as S_INTR
state "TASK_UNINTERRUPTIBLE (D)\n不可中断睡眠\n等磁盘 IO 等硬件事件\n信号无法打断" <<D>> as D
state "__TASK_STOPPED (T)\n被 SIGSTOP/调试器暂停\n收到 SIGCONT 回到 R" <<T>> as T
state "EXIT_ZOMBIE (Z)\n已退出, 等父进程 wait()\n资源已释放, task_struct\n暂留存退出码" <<Z>> as Z
[*] --> R : fork/clone 创建
R --> S_INTR : 等 mutex/sem/\nsleep/socket data
R --> D : 发起阻塞磁盘 IO
R --> T : 收到 SIGSTOP/\nPTRACE_ATTACH
R --> Z : do_exit()
S_INTR --> R : 事件到达/被唤醒
S_INTR --> R : 被信号打断\n(这是和 D 的关键区别!)
D --> R : IO 完成后\n硬件中断唤醒
T --> R : 收到 SIGCONT
Z --> [*] : 父进程 wait() →\nrelease_task() 回收
@enduml
```

### 3.2 状态的位掩码本质

`__state` 不是枚举，而是**位掩码**（`TASK_RUNNING=0x0000`，其他状态各占 1 bit），可以组合：

| 状态位 | 值 | 场景 |
|--------|-----|------|
| `TASK_RUNNING` | 0x0000 | 在 runqueue 或正在跑 |
| `TASK_INTERRUPTIBLE` | 0x0001 | 睡眠，能被信号唤醒 |
| `TASK_UNINTERRUPTIBLE` | 0x0002 | 睡眠，不响应信号 |
| `__TASK_STOPPED` | 0x0004 | SIGSTOP 停止 |
| `__TASK_TRACED` | 0x0008 | 被 ptrace |
| `EXIT_DEAD` | 0x0010 | task_struct 即将释放 |
| `EXIT_ZOMBIE` | 0x0020 | 僵尸 |
| `TASK_KILLABLE` | (TASK_UNINTERRUPTIBLE \| TASK_WAKEKILL) | **可被致命信号杀死**的 D 状态（折中方案） |

> **TASK_KILLABLE** 是个实用折中：普通 D 状态连 `kill -9` 都不理（如等一个坏掉的 NFS 盘）→ 进程永远杀不掉。`TASK_KILLABLE` 允许 D 状态的进程**被 fatal signal（SIGKILL）唤醒**——"在等 IO，但如果你真要杀我，我就死"。内核很多地方已改用这种更安全的 D。

### 3.3 退出过程

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "task" as T
participant "内核" as K
participant "父进程" as P
T -> K : ① do_exit()\n设置 PF_EXITING
K -> K : ② exit_mm/exit_files/exit_fs...\n释放所有资源对象
K -> K : ③ exit_notify: 通知父进程\n子进程被 init 收养(如有需要)
K -> K : ④ 设 __state = EXIT_ZOMBIE\n设 exit_code 退出码
note over K : 此时 task 变僵尸——\n资源已释放, 只剩 task_struct
K -> P : ⑤ 向父进程发 SIGCHLD
P -> K : ⑥ wait()/waitpid()
K -> K : ⑦ release_task()\n从 pid hash/进程树移除\nfree task_struct
note over K : task_struct 彻底消失
@enduml
```

| 字段 | 作用 |
|------|------|
| `exit_code` | 退出码，`wait()` 读走（`WEXITSTATUS` 提取的低 8 位） |
| `exit_signal` | 退出时发给父进程的信号（通常是 SIGCHLD） |
| `exit_state` | ZOMBIE / DEAD 过渡状态 |
| `flags` 中的 `PF_EXITING` | 正在退出中 |

## 四、调度参数 —— 调度器怎么选这个 task

调度是 [../scheduling.md](/concepts/process/scheduling.md) 的主题，这里只列出 task_struct 里存的"参数"：

### 4.1 优先级体系（五层）

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<a>> #E3F2FD
  BorderColor<<a>> #1976D2
  BackgroundColor<<b>> #C8E6C9
  BorderColor<<b>> #388E3C
}
rectangle "用户空间设置" <<a>> as USR {
  rectangle "nice 值 (-20~+19)\nnice(2) / setpriority(2)\n→ 映射到 static_prio"
}
rectangle "内核内部计算" <<b>> as KERN {
  rectangle "static_prio (100~139)\n= MAX_RT_PRIO + nice + 20\n普通进程的基础优先级"
  rectangle "normal_prio\n= 根据调度类和 static_prio\n算出的"正常"优先级"
  rectangle "prio (实际使用)\n调度器做决策用的值\n考虑了 PI(优先级继承)\n临时调整"
  rectangle "rt_priority (0~99)\n实时进程专用, 越大越优先"
}
USR --> KERN
note bottom of KERN : 调度器只看 prio——值越小越优先\n普通进程 prio ∈ [100,139]\n实时进程 prio = 99 - rt_priority ∈ [0,99]
@enduml
```

| 字段 | 说明 |
|------|------|
| `static_prio` | 普通进程 base = 120 + nice；实时进程不用这个（用 rt_priority） |
| `normal_prio` | `effective_prio()` 函数计算的结果，考虑了调度类和 static_prio |
| `prio` | **调度器实际使用的值**——通常 = normal_prio，但 PI 继承时会临时降低（变高优先级） |
| `rt_priority` | 实时进程专用，值越大优先级越高（`sched_setscheduler` 设置） |

### 4.2 调度类与调度实体

| 字段 | 说明 |
|------|------|
| `sched_class` | 指针指向调度类操作表（`fair_sched_class` / `rt_sched_class` / `dl_sched_class` / `idle_sched_class`） |
| `se` | `sched_entity`——CFS 调度实体（vruntime、权重、运行时间统计...） |
| `rt` | `sched_rt_entity`——RT 调度实体（时间片...） |
| `dl` | `sched_dl_entity`——Deadline 调度实体（运行时间/周期/截止时间...） |

### 4.3 运行位置约束

| 字段 | 说明 | 相关文档 |
|------|------|---------|
| `cpus_ptr` | 允许在哪些 CPU 核上运行的位图 | [thread-affinity.md](/concepts/process/thread-affinity.md) |
| `wake_cpu` | 上次被唤醒在哪个 CPU——调度器倾向原地唤醒 | — |
| `on_cpu` | 当前是否在某个 CPU 上跑着 | — |
| `on_rq` | 当前是否在运行队列里 | — |

### 4.4 调度统计

| 字段 | 说明 | `/proc` 来源 |
|------|------|-------------|
| `nvcsw` | 自愿上下文切换次数（主动让出 CPU——sleep/wait/IO） | `/proc/<pid>/status` voluntary_ctxt_switches |
| `nivcsw` | 非自愿上下文切换次数（时间片用完被抢走） | `/proc/<pid>/status` nonvoluntary_ctxt_switches |
| `last_switch_count` | 上次切换的计数 | — |
| `last_switch_time` | 上次切换的时间戳 | — |
| `policy` | 调度策略（SCHED_NORMAL/SCHED_FIFO/SCHED_RR/SCHED_DEADLINE） | — |

> **nivcsw 高的含义**：被抢占次数多 → CPU 争抢激烈、或负载太重。这是性能瓶颈的信号。

## 五、线程上下文 —— thread_struct（CPU 现场的保存点）

上下文切换时，被切走的 task 的 CPU 寄存器现场**保存在 `task->thread`** 里。详见 [../context-switch.md](/concepts/process/context-switch.md)。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<ts>> #E3F2FD
  BorderColor<<ts>> #1976D2
  BackgroundColor<<th>> #C8E6C9
  BorderColor<<th>> #388E3C
  BackgroundColor<<st>> #FFE0B2
  BorderColor<<st>> #EF6C00
}
rectangle "task_struct" <<ts>> as TS {
  rectangle "thread (thread_struct)" <<th>> as THR
  rectangle "stack\n(内核栈底指针)" as STK
}
rectangle "thread_struct\n(保存/恢复的寄存器)" <<th>> as THRDETAIL {
  rectangle "sp\n内核栈指针(被切走时栈在哪)"
  rectangle "sp0\n内核栈底(特权级切换到 ring0 时会 RSP←sp0)"
  rectangle "ip\n下一条指令地址"
  rectangle "fs / gs\nx86-64 FS/GS 段基址\n(用于线程局部存储 TLS)"
  rectangle "cr2\n最后一次缺页异常的地址"
  rectangle "trap_nr\n最后一次异常/中断号"
  rectangle "iopl\nI/O 特权级"
}
rectangle "内核栈 (thread_union)\nx86-64 默认 16KB\n= 4 页 THREAD_SIZE" <<st>> as KSTACK {
  rectangle "struct thread_info\n(在栈顶/底, 存 flags/preempt_count...)"
  rectangle "栈空间(剩余)\n当函数调用/中断嵌套时\nSP 往低地址增长"
}
THR --> THRDETAIL
STK --> KSTACK
note bottom of KSTACK : 每个 task 有自己的内核栈\n切进程 ≠ 切栈: 线程共享地址空间但不共享内核栈
@enduml
```

| 字段簇 | 关键字段 | 在上下文切换中的作用 |
|--------|---------|--------------------|
| **通用寄存器** | sp, ip, bp... | `switch_to` 保存/恢复所有 callee-saved 寄存器 |
| **段基址** | fs, gs | 切换线程时需重载 FS/GS（TLS 用） |
| **浮点/SIMD** | fpu 状态 | 惰性切换：不切进程就不恢复→切时才触发异常再恢复 |
| **调试** | debugreg[] | 硬件断点寄存器 |
| **I/O** | iopl, io_bitmap | I/O 端口访问权限 |

### 内核栈与 thread_info

内核栈和 `thread_info` 的关系在历史上变过：

- **老内核（< 4.9）**：`thread_info` 嵌在栈底（`task->stack` 的最低位），可通过 `SP & ~(THREAD_SIZE-1)` 直接拿到。
- **新内核（≥ 4.9）**：`thread_info` 从栈底**移出**，只留在 `task_struct` 里——简化栈保护、避免栈溢出覆盖 `thread_info`。

`thread_info` 的核心字段：
- `flags`：`TIF_SIGPENDING`（信号未决，返回用户态时检查）、`TIF_NEED_RESCHED`（需调度）、`TIF_NOTIFY_RESUME`（trace/审计）
- `preempt_count`：抢占计数（非 0 时禁止抢占中断）
- `cpu`：当前所在的 CPU 编号

## 六、统计与计时 —— utime/stime/缺页

| 字段 | 含义 | `/proc` 导出 | 谁更新 |
|------|------|-------------|--------|
| `utime` | 用户态 CPU 时间（tick 数） | `/proc/<pid>/stat` 第 14 字段 | 时钟中断发现 task 在用户态时累加 |
| `stime` | 内核态 CPU 时间 | `/proc/<pid>/stat` 第 15 字段 | 时钟中断发现 task 在内核态时累加 |
| `start_time` | 进程启动时间（相对于系统启动） | `/proc/<pid>/stat` 第 22 字段 | fork 时设 |
| `start_boottime` | 进程启动的绝对时间（含休眠） | — | fork 时设 |
| `min_flt` | 次缺页次数（不需要磁盘 IO） | `/proc/<pid>/stat` 第 10 字段 | 每次缺页处理时累加 |
| `maj_flt` | 主缺页次数（需要磁盘 IO） | `/proc/<pid>/stat` 第 12 字段 | 同上 |
| `gtime` | 客户机（虚拟化）CPU 时间 | — | — |

```bash
# 示例：计算进程已运行多久
HZ=$(getconf CLK_TCK)  # 通常是 100
# /proc/<pid>/stat 第 22 字段是 start_time (自启动的 jiffies)
# /proc/uptime 是系统已运行秒数
# 进程运行时间 = uptime_secs - start_time / HZ
```

> **utime/stime 的精度局限**：基于 tick（通常 100Hz/250Hz/1000Hz），短于一个 tick 的 CPU 时间可能漏计或被近似。

## 七、任务标志位 —— task->flags (PF_*)

`task->flags` 是位掩码，记录 task 的各种"特殊状态"：

| 标志 | 含义 | 常见场景 |
|------|------|---------|
| `PF_EXITING` | 正在退出中 | do_exit() 的第一步 |
| `PF_EXITPIDONE` | 退出完成 | — |
| `PF_FORKNOEXEC` | fork 后还没 exec | CoW 优化依据 |
| `PF_SUPERPRIV` | 使用了超级用户权限 | 审计/记账 |
| `PF_DUMPCORE` | 收到要产生 core dump 的信号 | [crash/core-dump.md](/crash/core-dump.md) |
| `PF_SIGNALED` | 被致命信号杀死 | 通知等待的 syscall |
| `PF_MEMALLOC` | 允许使用紧急内存储备 | 内存回收路径自身 |
| `PF_KSWAPD` | 这是 kswapd 内核线程 | — |
| `PF_WQ_WORKER` | 这是 workqueue 工作线程 | — |
| `PF_KTHREAD` | 这是内核线程 | mm == NULL, 无用户地址空间 |
| `PF_RANDOMIZE` | ASLR（地址空间随机化）生效 | — |
| `PF_NO_SETAFFINITY` | 禁止改 CPU 亲和性 | systemd 等关键服务 |

## 八、内核怎么组织所有 task —— 四套查找结构

内核不是只用一条链表遍历所有进程，而是用**四套结构**应对不同查找场景：

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<a>> #E3F2FD
  BorderColor<<a>> #1976D2
  BackgroundColor<<b>> #C8E6C9
  BorderColor<<b>> #388E3C
  BackgroundColor<<c>> #FFE0B2
  BorderColor<<c>> #EF6C00
  BackgroundColor<<d>> #F3E5F5
  BorderColor<<d>> #7B1FA2
}
rectangle "task_struct" as TS
rectangle "① 全局链表 tasks\n(list_head)\n遍历所有进程" <<a>> as L1
rectangle "② PID 哈希表\npid_hash[]\n按 PID/TGID/PGID 快速查找" <<b>> as L2
rectangle "③ 运行队列 runqueue\nper-CPU 红黑树\nCFS: vruntime 排序\nRT/Deadline: 各自的优先级队列" <<c>> as L3
rectangle "④ 等待队列 wait_queue\nper-等待条件的链表\n(task 阻塞在某个事件上\n--> 事件到来遍历链表唤醒)" <<d>> as L4
TS --> L1 : 遍历用
TS --> L2 : 查找用
TS --> L3 : 调度用 (仅 RUNNING 状态)
TS --> L4 : 睡眠/等待用
note bottom of L1 : for_each_process() 宏遍历
note bottom of L2 : find_task_by_vpid() / find_get_pid()
note bottom of L3 : pick_next_task() 选下一个要跑的
note bottom of L4 : wake_up() 唤醒等待者
@enduml
```

| 结构 | 使用场景 | 复杂度 |
|------|---------|--------|
| 全局链表 `tasks` | `ps`/`top` 遍历所有进程、`for_each_process` 宏 | O(n) 遍历 |
| PID 哈希表 | `kill(pid)`、`/proc/<pid>/` 按 PID 查找 | O(1) 平均 |
| 运行队列 | 调度器选下一个 CPU 上的 task | CFS: O(log n)、RT: O(1) |
| 等待队列 | task 阻塞在锁/IO/signal 等事件上 | O(1) 入队，唤醒遍历 |

## 九、锁与同步 —— task_struct 自身的并发保护

task_struct 里有些字段被多核/中断并发访问，需要保护：

| 锁 | 保护什么 |
|----|---------|
| `pi_lock` | 调度器/优先级相关字段（轻量 raw_spinlock） |
| `alloc_lock` | 资源限制 rlimit 修改 |
| `sighand->siglock` | 信号状态的修改（见 [signals-kernel.md](/concepts/process/task-resources/signals-kernel.md)） |
| RCU | `task_struct` 本身的释放延迟（`call_rcu` + `delayed_put_task_struct`）——遍历进程链表时拿到的 task 不会突然被 free |

### 优先级继承（PI）

```plantuml
@startuml
skinparam shadowing false
skinparam sequence {
  ParticipantBackgroundColor #E3F2FD
  ParticipantBorderColor #1976D2
}
participant "高优先 task A" as A
participant "中优先 task C" as C
participant "低优先 task B\n(持锁)" as B
note over A,B : 初始: B 持锁, A 等锁
A -> B : A 等 B 的锁 → 阻塞
B -> B : pi_waiters ← A\nB 的 prio **继承** A 的优先级
note over B : B 暂时变高优先(pi 继承)\n→ 不被中优先抢占 → 快速放锁
B -> A : 放锁, prio 恢复
@enduml
```

| 字段 | 作用 |
|------|------|
| `pi_waiters` | 等本 task 持有的 PI 锁的 task 列表（优先级可能被提升） |
| `pi_blocked_on` | 本 task 在等的 PI 锁 |
| `pi_lock` | 保护 PI 链的修改 |

## 十、观测

```bash
# 状态与身份
cat /proc/<pid>/status | grep -E 'Name|State|Tgid|Pid|PPid|Threads|NSpid'
#    Name: nginx    State: S (sleeping)    Tgid: 1000    Pid: 1000    PPid: 1
# 调度统计
cat /proc/<pid>/status | grep -E 'voluntary|nonvoluntary'
cat /proc/<pid>/sched  # 详细的调度参数 (policy/prio/se... 全都在这)
# CPU 时间
cat /proc/<pid>/stat | awk '{print "utime="$14" stime="$15" starttime="$22}'
# 缺页
cat /proc/<pid>/stat | awk '{print "minflt="$10" majflt="$12}'
# 标志位（需要内核源码对照）
cat /proc/<pid>/stat | awk '{print "flags="$9}'
# 进程树
pstree -p <pid>
cat /proc/<pid>/task/*/children  # 直接子进程
```

## 十一、一句话总结

> **`task_struct` 的私有字段描述这**一个 task **本身：身份（pid/tgid/comm）、进程树位置（parent/children/sibling）、状态机（R/S/D/T/Z 五位掩码）、调度参数（五层优先级+调度类+运行位置）、线程上下文（内核栈+thread_struct 保存 CPU 寄存器）、统计计数（utime/stime/缺页）、标志位（PF_* 特判）。内核用全局链表/PID 哈希/运行队列/等待队列四套结构组织所有 task，用 pi_lock/RCU 保护并发访问。`/proc/<pid>/status`、`/proc/<pid>/stat`、`/proc/<pid>/sched` 是对外窗口。**

