Linux 里没有”进程对象”和”线程对象”的区分——两者都是
struct task_struct。这个结构体定义在
include/linux/sched.h,2026 年的 6.6 内核里约
1600 行,成员 300+ 个。它是 Linux
内核里最大、最”杂”的结构体之一。
读这个结构体的意义:
- 理解一次
fork、exec、schedule实际动了什么 - 分析 oops / kdump 时能认出关键字段
- 写内核模块时知道哪些字段能读、哪些必须锁
本文按功能区切分 task_struct,每区挑出关键字段讲语义和变更。
一、总览:九个功能区
flowchart TB
T[task_struct]
T --> ID[标识<br/>pid / tgid / comm / parent]
T --> SC[调度<br/>prio / policy / se / dl / rt]
T --> ST[状态<br/>state / flags / exit_state]
T --> MM[内存<br/>mm / active_mm / vmacache]
T --> FS[文件系统<br/>fs / files / nsproxy]
T --> SIG[信号<br/>signal / sighand / blocked]
T --> CR[凭据<br/>cred / real_cred / group_info]
T --> NS[命名空间<br/>nsproxy / cgroups]
T --> TR[追踪/审计<br/>audit / ptrace / seccomp / perf]
每个区域都有自己的锁规则和生命周期规则,下面分区讨论。
二、身份区:pid、tgid、comm
struct task_struct {
pid_t pid; // thread id(线程级)
pid_t tgid; // thread group id(进程级 = 主线程 pid)
char comm[TASK_COMM_LEN]; // 15 字符的命令名
struct task_struct __rcu *real_parent;
struct task_struct __rcu *parent;
struct list_head children;
struct list_head sibling;
...
};几个常见误解:
- “PID” 在 Linux 里其实是 tid。shell 里
ps看到的PID是主线程 tid = tgid;其他线程的 tid 不等于 tgid getpid()返回 tgid(POSIX 兼容)- comm 只有 15
字节。超长命令名被截断。
prctl(PR_SET_NAME)可改 - real_parent vs parent:ptrace 时 parent 指向 tracer,real_parent 保留生父
2.1 PID 分配
PID 分配器(kernel/pid.c)在 task 所属 pid
namespace 里找空闲号;namespace 之间独立。一个 task
在每个祖先 ns 里都有一个 pid——保存在 struct pid
引用链里,而不是 task_struct 直接字段。这让 pid namespace
嵌套成为可能。
三、调度区
struct task_struct {
int prio;
int static_prio;
int normal_prio;
unsigned int policy; // SCHED_NORMAL/FIFO/RR/BATCH/IDLE/DEADLINE
const struct sched_class *sched_class;
struct sched_entity se; // CFS / EEVDF 的调度实体
struct sched_rt_entity rt;
struct sched_dl_entity dl;
cpumask_t cpus_mask;
int on_cpu;
unsigned int rcu_read_lock_nesting;
...
};sched_class:指向 CFS / RT / DL / stop / idle 等类的 ops 表se:CFS 的 vruntime、weight 都在这里;6.6 后 EEVDF 复用 sched_entitycpus_mask:sched_setaffinity绑核结果on_cpu:当前在哪个 CPU 上跑(或 -1)
调度相关详情在 C 子系列深入。
四、状态与标志
unsigned int __state; // TASK_RUNNING, INTERRUPTIBLE, UNINTERRUPTIBLE, STOPPED, ZOMBIE, DEAD
unsigned long flags; // PF_* 标志:PF_KTHREAD、PF_EXITING、PF_SIGNALED 等
int exit_state;
int exit_code;
int exit_signal;state 的经典值:
TASK_RUNNING:在 runqueue 上或正在跑TASK_INTERRUPTIBLE:睡眠(可被信号唤醒)TASK_UNINTERRUPTIBLE:睡眠(D state,不可打断)——ps里 D 状态的来源TASK_STOPPED:被 SIGSTOPTASK_TRACED:被 ptrace 停住EXIT_ZOMBIE:进程已退出,但父还没 waitEXIT_DEAD:进程结构体准备释放
D state 是生产故障的常客:NFS
卡、磁盘坏、kernel bug 都会让 task 卡在
D。cat /proc/<pid>/stack 看内核栈。
五、内存管理
struct mm_struct *mm;
struct mm_struct *active_mm;mm:指向用户空间进程的地址空间描述符(所有线程共享同一 mm)active_mm:内核线程没有自己的 mm(为 NULL),借用上一个 user task 的 mm,active_mm 指向它以保持 refcount
mm_struct 本身是另一个大结构体(D-33
专题),包含 vma、页表根、exec 名、参数区等。切换 task
时如果 mm 相同,不切
CR3;这是线程切换比进程切换快的本质。
vmacache 在旧版本是小缓存;5.x 后改为
maple tree 的 LRU,vmacache
字段被移除。
六、文件系统
struct fs_struct *fs; // cwd, root, umask
struct files_struct *files; // fd table
struct nsproxy *nsproxy;fs:cwd(当前工作目录)、root(chroot 后的根)、umaskfiles:fd 数组 + close_on_exec bitmapnsproxy:mnt/pid/net/uts/ipc/cgroup/time 七个 namespace 指针(user ns 单独)
CLONE_FS / CLONE_FILES 决定是否共享 fs / files。线程共享(同一个 files 指针);fork 后各自一份。
七、信号
struct signal_struct *signal; // 线程组共享
struct sighand_struct *sighand; // 线程组共享(handlers)
sigset_t blocked; // 每线程的 mask
sigset_t real_blocked;
struct sigpending pending; // 线程级待处理
...注意:
signal和sighand在整个线程组内共享——所有 pthread 看到的 sigaction 一致blocked是每线程的——每个 pthread 可以独立sigprocmask- pending
分为线程级和进程级(
signal->shared_pending)
递送规则(B-13 专题):先查线程私有 pending;无则看共享;共享信号递送给任何一个未屏蔽它的线程。
八、凭据
const struct cred __rcu *real_cred; // 真实凭据
const struct cred __rcu *cred; // 有效凭据
char comm[TASK_COMM_LEN];struct cred 包含
uid、euid、suid、fsuid、gid、egid 以及 capabilities
三个集合(permitted/effective/inheritable/ambient)、keyring、LSM
security blob。
RCU 保护:credentials 不可变,切换用
commit_creds() 原子替换指针,旧指针 RCU
释放。这让所有 capable() 检查可以无锁。
九、追踪与审计
struct audit_context *audit_context;
struct task_struct __rcu *ptracer;
struct list_head ptrace_entry;
unsigned long ptrace; // flags
struct seccomp seccomp;
...
struct io_uring_task *io_uring;
struct perf_event_context *perf_event_ctxp[perf_nr_task_contexts];perf、bpf、ftrace、audit、seccomp 都在 task_struct 里有落脚点。这也是为什么现代 task_struct 越长越大——每一个可观测性功能都要加字段。
十、task_struct 本身的分配
task_struct 本身通过 slab 分配。内核栈是另外分配的——典型 16KB(x86_64 THREAD_SIZE=8KB 一开始,后因 KASAN 加大到 16KB)。
栈和 task_struct 的关系经历过两次变革:
- 2.4 时代:栈顶存
struct thread_info,通过current_thread_info()从 sp 反推,再从 thread_info 拿task_struct * - 4.9 引入
CONFIG_THREAD_INFO_IN_TASK:thread_info 直接嵌在 task_struct 里。current改为从 GS:%[current_task](x86_64)读 per-CPU 变量 - VMAP_STACK(4.9+):内核栈用 vmalloc 分配,有 guard 页检测栈溢出
current 在不同架构实现:
- x86_64:
%gs:[current_task]per-CPU - arm64:
SP_EL0寄存器
读 current 是一条指令级开销。
十一、task 的生命周期里 task_struct 的命运
sequenceDiagram
participant P as 父进程
participant K as kernel
participant C as 子进程 task_struct
P->>K: clone3()
K->>C: alloc_task_struct + copy fields
K->>C: add to pid hash, runqueue
C->>C: schedule → run
C->>K: exit() / _exit() / signal
K->>C: do_exit: 清 files, mm, signal, cleanup IO
C->>K: __state = EXIT_ZOMBIE
P->>K: waitpid()
K->>C: release_task: 释放 task_struct (RCU 延迟)
关键:task_struct 在
do_exit 后还不能立刻释放(还在 runqueue
上要清理、父进程要看 exit_code)。必须等
release_task,后者在父 wait 或 subreaper
处理后调。RCU 再延迟一次以保证
find_task_by_vpid 等 readers 安全。
僵尸进程(zombie)只占一个 task_struct + kernel stack(~20KB),几乎不占资源——但大量僵尸会消耗 pid 空间。
十二、大小与开销
task_struct 大小(x86_64, 6.6):
- ~3KB 结构体本身
- 16KB 内核栈
- 链接的 mm_struct(400B)、cred(200B)、signal(~400B)等
一个进程起步 ~20KB;数万 task 时 task_struct 占用 GB 级内存。
减小 task_struct 是持续工程——例如
struct task_struct 的
rcu_node_entry 条件编译、INIT_TASK
宏的演化、CLONE_THREAD 合并
signal/sighand。
十三、读 task_struct 的实战用法
内核调试场景:
crash> ps
crash> task ffff88003a7e0000
crash> struct task_struct.pid,tgid,comm,state ffff88003a7e0000
crash> foreach UN ps # 所有 D state
eBPF 场景:
// bpftrace
kprobe:do_exit {
printf("%s[%d] exiting\n", comm, pid);
}BCC 和 bpftrace 把 task_struct
的字段暴露给脚本。F-44 会详细讲 tracing。
十四、小结
- Linux 的 task = 一个 task_struct + 一个内核栈(可选 vmap)
- 结构体按九个功能区组织,每区有独立的并发/生命周期规则
- 线程组 = 共享 signal、sighand、mm、files 等 “组级” 结构体的一组 task
current通过 per-CPU 变量查,线程切换时的 GS/SP_EL0 是关键- zombie = 已 do_exit 但未 release_task 的 task,需要 wait/subreaper 回收
下一篇 B-12 沿 task 的一生走一遍——从 clone 到 release 的每个阶段动了什么。
参考文献
- Linux source:
include/linux/sched.h,kernel/fork.c,kernel/exit.c - Corbet, J. “Per-task kernel stacks.” LWN.net 2016
- Corbet, J. “Virtually mapped kernel stacks.” LWN.net 2016
- Gorman, M. Understanding the Linux Virtual Memory Manager(对 mm_struct 的经典解剖)
- Bovet, D., Cesati, M. Understanding the Linux Kernel, Chapter 3
- Love, R. Linux Kernel Development, 3rd ed.
- Documentation/filesystems/proc.rst —— /proc/
/* 的字段来源
工具
cat /proc/<pid>/status—— task_struct 主要字段的文本投影cat /proc/<pid>/sched—— 调度器字段cat /proc/<pid>/stack—— 当前内核栈crash+ vmlinux + vmcore —— kdump 分析bpftrace -e 'kprobe:wake_up_new_task { ... }'—— fork 路径追踪
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】mm_struct 与 VMA
进程地址空间在内核里靠 mm_struct + VMA 链表/树描述。本文讲 mm_struct 核心字段、VMA 从红黑树到 maple tree 的改造、anon_vma 反向映射、mmap_lock 争抢。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。