task_struct 私有字段 —— 进程描述符本体详解
这是 task-struct 里"task_struct 本体存什么"的展开。本篇把 task_struct 里不通过指针挂出去、只属于这个 task 自己的字段拆开讲透:身份标识、进程树关系、状态机、调度参数、线程上下文(寄存器+内核栈)、统计计数、任务标志位、以及内核怎么把所有 task 串在一起。可共享的资源对象(mm/files/signal/cred/nsproxy...)另见独立文档。
零、一句话认知:task_struct 的字段分两类
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。

| 字段 | 含义 | 谁设置 | 谁读取 |
|---|---|---|---|
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 的层级链表实现。

二、进程树 —— task 之间怎么串成父子关系
每个 task 都有一组指针把自己嵌进"进程树":

| 字段 | 类型 | 含义 |
|---|---|---|
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 五大基本状态

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 退出过程

| 字段 | 作用 |
|---|---|
exit_code | 退出码,wait() 读走(WEXITSTATUS 提取的低 8 位) |
exit_signal | 退出时发给父进程的信号(通常是 SIGCHLD) |
exit_state | ZOMBIE / DEAD 过渡状态 |
flags 中的 PF_EXITING | 正在退出中 |
四、调度参数 —— 调度器怎么选这个 task
调度是 scheduling 的主题,这里只列出 task_struct 里存的"参数":
4.1 优先级体系(五层)

| 字段 | 说明 |
|---|---|
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 |
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。

| 字段簇 | 关键字段 | 在上下文切换中的作用 |
|---|---|---|
| 通用寄存器 | 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 时间 | — | — |
# 示例:计算进程已运行多久
HZ=$(getconf CLK_TCK) # 通常是 100
# /proc/<pid>/stat 第 22 字段是 start_time (自启动的 jiffies)
# /proc/uptime 是系统已运行秒数
# 进程运行时间 = uptime_secs - start_time / HZutime/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 的信号 | core-dump |
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 —— 四套查找结构
内核不是只用一条链表遍历所有进程,而是用四套结构应对不同查找场景:

| 结构 | 使用场景 | 复杂度 |
|---|---|---|
全局链表 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) |
| RCU | task_struct 本身的释放延迟(call_rcu + delayed_put_task_struct)——遍历进程链表时拿到的 task 不会突然被 free |
优先级继承(PI)

| 字段 | 作用 |
|---|---|
pi_waiters | 等本 task 持有的 PI 锁的 task 列表(优先级可能被提升) |
pi_blocked_on | 本 task 在等的 PI 锁 |
pi_lock | 保护 PI 链的修改 |
十、观测
# 状态与身份
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是对外窗口。*