内核视角的进程 —— task_struct 与它挂着的一切资源
在用户眼里"进程"是一个正在运行的程序;在内核眼里,一个进程就是一个
task_struct——一张几百个字段的大表,外加它用指针挂出去的一堆资源对象(地址空间、打开的文件、信号、凭证、命名空间、IO 上下文、seccomp……)。本篇从内核数据结构的角度拆开:内核用什么表示一个进程、私有字段 vs 可共享资源对象怎么分界、进程和线程在这套结构里到底差在哪、以及怎么从/proc反向看到这些结构。
📚 历史背景:Linux 0.01 版本时内核只区分"任务",没有线程概念,每个task对应一个进程;Linux 0.99.11版本引入了POSIX线程的支持,通过clone系统调用实现了轻量级进程,也就是线程;Linux 2.2版本之后,task_struct的结构逐渐稳定,现在的task_struct有超过600个字段,包含了进程的所有信息。
📂 下钻子目录:task-resources/ —— 每一样资源对象逐篇展开详解(含新增的 task 私有字段、fs_struct、io_context/seccomp/rseq)。
📚 子系统索引:task-struct-knowledge-framework,按子系统维度将 task_struct 字段映射到仓库文档——想从字段出发找对应文档时查这张表。
相关:进程怎么被创建出来(fork/clone 复制/共享这些结构)见 process-creation;线程为什么是"共享大部分结构的 task"见 thread-creation;地址空间的段布局见 memory-layout;
mm_struct→页表翻译见 tlb;进程调度相关见 scheduling;上下文切换见 context-switch。
零、一句话认知:进程 = 一个 task_struct + 它指向的资源对象
Linux 内核不区分"进程"和"线程"这两个概念——它只有一种调度实体,叫 task(任务),每个 task 对应一个 struct task_struct。所谓"进程"和"线程",只是一组 task 之间共享了多少资源对象的不同:共享得多就是"同一个进程里的多个线程",共享得少就是"不同进程"。
核心类层次图

核心记忆:
task_struct本身存的是"这个 task 私有的东西"(pid、状态、调度、寄存器现场);那些可共享的资源(内存、文件、信号……)被单独拆成独立对象,task_struct只用指针指过去。fork/clone 的全部戏法,就是决定这些指针复制一份新对象还是共享同一个对象(见 process-creation §三)。
📊 可视化说明:上图展示了task_struct与核心资源对象的关联关系,每个箭头代表一个指针字段,箭头方向表示资源的拥有关系。
一、task_struct 的两大部分:私有字段 vs 资源指针
1.1 私有字段(只属于这一个 task)
这些字段描述这一个 task 本身,不和别人共享。每个字段都只能被当前task访问和修改,不会被其他task共享。下面是主要的私有字段分类:
1.1.1 身份标识字段
| 字段 | 类型 | 含义 | 调试方法 |
|---|---|---|---|
pid | pid_t | 线程ID,每个线程唯一的ID,对应gettid()系统调用的返回值 | cat /proc/<pid>/task/<tid>/status |
tgid | pid_t | 线程组ID,同一进程的所有线程共享该ID,对应getpid()系统调用的返回值 | cat /proc/<pid>/status | grep Tgid |
group_leader | struct task_struct* | 线程组组长进程的指针,也就是主线程的task_struct | ps -o pid,tgid,comm -p <pid> |
| comm | char[16] | 进程的命令行名称,最多16个字符,对应ps命令中的COMM列 | cat /proc/<pid>/comm |
pid_links | struct hlist_node | 用于PID哈希表的链表节点,内核通过PID查找task_struct | hash PID |
1.1.2 进程树字段
| 字段 | 类型 | 含义 | 调试方法 |
|---|---|---|---|
parent | struct task_struct* | 指向父进程的task_struct | cat /proc/<pid>/status | grep PPid |
children | struct list_head | 子进程链表的头节点 | pstree -p <pid> |
sibling | struct list_head | 用于将子进程链接到父进程的children链表 | ls /proc/<pid>/task/ |
real_parent | struct task_struct* | 真实的父进程指针,不受ptrace影响 | cat /proc/<pid>/status | grep real_parent |
1.1.3 状态字段
| 字段 | 类型 | 含义 | 调试方法 |
|---|---|---|---|
__state | long | 进程的当前状态,比如TASK_RUNNING、TASK_INTERRUPTIBLE等 | cat /proc/<pid>/status | grep State |
flags | unsigned long | 进程的标志位,比如PF_EXITING、PF_KTHREAD等 | cat /proc/<pid>/status | grep Flags |
exit_state | int | 进程的退出状态,当进程退出时设置 | cat /proc/<pid>/status | grep Exit |
1.1.4 调度字段
| 字段 | 类型 | 含义 | 调试方法 |
|---|---|---|---|
prio | int | 动态优先级,调度器使用这个字段来选择下一个要运行的进程 | chrt -p <pid> |
static_prio | int | 静态优先级,由nice值设置 | cat /proc/<pid>/sched | grep static_prio |
normal_prio | int | 正常优先级,基于静态优先级和调度类计算得出 | cat /proc/<pid>/sched | grep normal_prio |
sched_class | struct sched_class* | 指向调度类的指针,比如fair_sched_class、rt_sched_class等 | cat /proc/<pid>/sched | grep sched_class |
cpus_ptr | const struct cpumask* | 指向CPU掩码的指针,决定这个进程可以在哪些CPU上运行 | taskset -p <pid> |
thread | struct thread_struct* | 指向thread_struct的指针,保存了CPU寄存器的上下文,用于上下文切换 | cat /proc/<pid>/stack |
1.1.5 统计字段
| 字段 | 类型 | 含义 | 调试方法 |
|---|---|---|---|
utime | unsigned long | 进程在用户态运行的时间,单位是jiffies | cat /proc/<pid>/stat | cut -d ' ' -f 14 |
stime | unsigned long | 进程在内核态运行的时间,单位是jiffies | cat /proc/<pid>/stat | cut -d ' ' -f 15 |
nivcsw | unsigned long | 进程的自愿上下文切换次数,比如进程等待IO时 | cat /proc/<pid>/stat | cut -d ' ' -f 11 |
nvcsw | unsigned long | 进程的非自愿上下文切换次数,比如时间片用完时被抢占 | cat /proc/<pid>/stat | cut -d ' ' -f 12 |
min_flt | unsigned long | 进程的次要缺页中断次数,比如从页缓存中读取页面 | cat /proc/<pid>/stat | cut -d ' ' -f 9 |
maj_flt | unsigned long | 进程的主要缺页中断次数,比如从磁盘中读取页面 | cat /proc/<pid>/stat | cut -d ' ' -f 10 |
📄 完整的私有字段详解请参考 task-private,包含了所有字段的详细解释和代码示例。
1.2 资源对象指针(可被多个 task 共享)
以下资源对象被拆成独立结构,由 task_struct 用指针指向。同进程线程共享指针、fork 复制指针所指对象:
| 资源对象 | 内核结构 | task->字段 | 详解文档 | 一句话 |
|---|---|---|---|---|
| 地址空间 | mm_struct | mm | mm-struct | VMA 集合+页表根+段边界;线程共享/fork 复制(CoW) |
| 打开文件表 | files_struct | files | files-struct | fd→file→inode 三层;偏移在 file 层,决定 dup/fork 语义 |
| 文件系统上下文 | fs_struct | fs | fs-struct | pwd/root/umask;线程共享(fork 复制) |
| 信号 | sighand_struct / signal_struct | sighand / signal | signals-kernel(导读+分类·传递路径·数据结构·性能) · signals-alternatives | 处理动作表(共享)+未决信号(分共享/私有);高性能替代方案(signalfd/eventfd/timerfd/futex) |
| 凭证 | cred | cred | cred | uid/gid/capabilities;四种 uid 为何存在 |
| 命名空间+限额 | nsproxy + cgroup | nsproxy | namespaces-cgroups | 容器底座:隔离视图+资源限额 |
| IO 上下文 | io_context | io_context | io-context §一 | AIO 上下文+IO 调度优先级;线程共享 |
| 系统调用过滤 | seccomp | seccomp | io-context §二 | BPF 过滤器,只能加严;Chrome/Docker 沙箱基石 |
| Futex 健壮列表 | robust_list_head | robust_list | io-context §四 | 线程死后内核代解锁 |
1.2.1 地址空间(mm_struct)
task->mm 指向 mm_struct,描述进程的虚拟内存地址空间。每个进程的虚拟内存空间都是独立的,通过页表映射到物理内存。
一个进程的地址空间包含多个段(代码段、数据段、堆、mmap 映射区、栈等),每个段对应一个 vm_area_struct(VMA)。所有 VMA 通过双向链表 + 红黑树双重索引组织:
- 双向链表
vm_next/vm_prev:按地址升序串联所有 VMA,遍历所有段时使用 - 红黑树
vm_rb:以起始地址为 key,实现按地址的 O(log n) 快速查找,缺页中断时内核需要快速定位目标地址属于哪个 VMA
核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
pgd | pgd_t* | 页全局目录指针,进程页表的根,切换时加载到 CR3 |
mmap | struct vm_area_struct* | VMA 双向链表头,按地址升序串联所有段 |
mm_rb | struct rb_root | VMA 红黑树根,按地址快速查找 O(log n) |
total_vm | unsigned long | 虚拟内存总页面数 |
start_code / end_code | unsigned long | 代码段起止地址(快捷指针,指向已有的 VMA) |
start_stack | unsigned long | 用户态栈起始地址 |
brk | unsigned long | 堆(heap)边界地址,sbrk() 移动此值 |
mm_users | atomic_t | 使用该 mm 的线程数,最后一个线程退出时释放 |
mm_count | atomic_t | 总引用计数,含内核 lazy 借用场景 |

- 🏷️ 关键特性:
- 每个段(代码、数据、堆、mmap、栈…)都是一个
vm_area_struct,按起始地址升序串成双向链表 - 红黑树
mm_rb以vm_start为 key,缺页中断或mmap/munmap时 O(log n) 定位 VMA cat /proc/PID/maps可以看到进程所有 VMA 的起止地址、权限和映射文件- 页全局目录
pgd是进程页表的根,切换进程时需要将pgd加载到CR3寄存器 mm_users记录使用这个mm_struct的线程数,当最后一个线程退出时释放mm_structmm_count记录总引用数,包括内核的lazy借用- 写时复制(CoW):fork时复制mm_struct,但物理页面共享,当有线程修改页面时才复制新的物理页面
- 每个段(代码、数据、堆、mmap、栈…)都是一个
1.2.2 打开文件表(files_struct)
task->files 指向 files_struct,管理进程所有打开的文件。核心是一张动态扩容的 fd 指针数组,下标是文件描述符。
文件描述符的组织是三层引用链:
- 第 1 层 files_struct:fd 数组本身,按 fd 编号索引到
struct file*。fd 0/1/2 固定为 stdin/stdout/stderr,超过max_fds时数组自动扩容 - 第 2 层 struct file:代表"一次打开"的会话对象,持有独立的读写偏移
f_pos和打开标志f_flags。dup()创建的新 fd 指向同一个struct file,共享偏移 - 第 3 层 inode:磁盘文件本体,无论被打开多少次,系统中只有一份
files_struct 核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
fd_array | struct file** | fd 指针数组,0→stdin、1→stdout、2→stderr |
nr_open | int | 当前已打开的文件数量 |
max_fds | unsigned int | 数组最大容量,超限时自动扩容 |
fdtab | struct fdtable | 内嵌 fd 表,含 max_fds 和 close_on_exec 位图 |
struct file 核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
f_pos | loff_t | 当前读写偏移位置,dup 创建的 fd 共享此字段 |
f_flags | unsigned int | 打开模式 O_RDONLY/O_WRONLY/O_RDWR/O_APPEND 等 |
f_op | struct file_operations* | 文件操作函数表,由具体文件系统实现 |
f_inode | struct inode* | 指向磁盘文件本体 |

- 🏷️ 关键特性:
dup()/dup2()创建的新 fd 指向同一个struct file,共享读写偏移- 两次独立
open()同一文件产生两个struct file,各自维护独立的f_pos close_on_exec:fd 可设FD_CLOEXEC标志,execve时自动关闭,防止泄漏给新程序- 进程退出时,内核遍历
files_struct关闭所有 fd
1.2.3 文件系统上下文(fs_struct)
task->fs 指向 fs_struct,记录进程在文件系统中的"立足点"——当前目录、根目录和权限掩码。它决定了进程如何解析路径名和创建文件。
root 和 pwd 存的不是字符串,而是 struct path = {vfsmount, dentry}:
- dentry(目录项缓存):指向目录的 inode 和父目录 dentry,即使目录已被删除,只要 dentry 还在缓存,进程仍可通过相对路径找到它
- vfsmount(挂载点):记录该路径所在文件系统的挂载信息,跨挂载点遍历时需要此信息
核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
root | struct path | 进程眼中的文件系统根目录 "/"(含 dentry + vfsmount) |
| pwd | struct path | 当前工作目录,所有相对路径解析的起点 |
| umask | umode_t | 新建文件/目录的默认权限掩码,实际权限 = mode & ~umask |

- 🏷️ 关键特性:
- 同进程的线程共享同一个
fs_struct(CLONE_FS),一个线程chdir会影响所有线程 - fork 默认复制
fs_struct,父子进程的文件系统上下文独立 chroot只改root而不改 pwd,root 用户可通过chdir("..")逃逸;容器用pivot_root+ 挂载命名空间实现更彻底的隔离
- 同进程的线程共享同一个
1.2.4 信号处理(sighand_struct / signal_struct)
信号管理被拆成两个独立结构,各司其职:
| 指针 | 指向结构 | 职责 |
|---|---|---|
task->sighand | sighand_struct | 信号处理动作表("收到信号时怎么处理") |
task->signal | signal_struct | 线程组共享状态(未决信号队列 + 资源限制) |
此外每个 task_struct 自身还持有私有的信号字段:blocked(阻塞掩码)和 pending(本线程私有的未决信号)。
"信号投递给谁"的分流规则:
- 发给整个进程的信号(如
kill -15 pid)→ 放入signal->shared_pending,由线程组中任意一个未阻塞该信号的线程处理 - 发给特定线程的信号(如
pthread_kill(tid, SIGSEGV)、线程自身触发的异常)→ 放入该线程的task->pending - 处理时按优先级:先检查私有
pending,再检查共享shared_pending
sighand_struct 核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
action[_NSIG] | struct k_sigaction[64] | 64 个信号的处理器数组,含 sa_handler(处理函数)和 sa_flags |
siglock | spinlock_t | 保护信号处理表的自旋锁 |
signal_struct 核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
shared_pending | struct sigpending | 线程组共享的未决信号队列 |
rlim[RLIM_NLIMITS] | struct rlimit[] | 资源限制数组(文件数、CPU 时间、内存等) |
tgid | pid_t | 线程组 ID |

- 🏷️ 关键特性:
- 同进程的线程共享
sighand_struct和signal_struct - 每个线程有自己的
blocked掩码,可独立阻塞/放行信号 - 未决信号分共享和私有两种队列,保证"发给进程"和"发给线程"语义正确
SIGKILL/SIGSTOP不可被捕获或忽略,也不受blocked影响
- 同进程的线程共享
1.2.5 凭证与权限(cred)
task->cred 指向 cred_struct,包含进程的用户/组身份和特权能力。这是 Linux 权限模型的载体——打开文件、发送信号、执行特权操作,全部依赖这张凭证。
四种 UID 的分工:
所有权限检查只看 euid(effective UID),其他三种 UID 是为 euid 在不同场景间安全切换服务的:
- real UID:进程的"真实身份",继承自父进程,几乎不变
- effective UID:用于所有权限检查——打开文件、执行系统调用、发送信号,内核只看它
- saved UID:setuid 程序的"旧 euid 备份",允许程序在 root 和原用户之间来回切换
- fs UID:专用于文件系统访问检查,默认等于 euid,可独立调整而不影响其他权限
典型 setuid 流程:普通用户执行 passwd(owner=root, setuid 位)→ euid 变成 root → 改完
/etc/shadow→seteuid(saved_uid)切回原用户,放弃特权
Capabilities 的分层设计:
| 能力集 | 含义 |
|---|---|
cap_inheritable | exec 新程序时,能被继承过去的能力 |
cap_permitted | 进程"被允许拥有"的能力池,是所有有效能力的超集 |
cap_effective | 当前实际生效的能力,每次权限检查看这个 |
cap_bset / cap_ambient | 边界集(上限锁)/ 环境集(exec 时自动传递) |
核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
uid / gid | kuid_t / kgid_t | 真实用户/组 ID |
euid / egid | kuid_t / kgid_t | 有效用户/组 ID,所有权限检查以此为据 |
suid / sgid | kuid_t / kgid_t | 保存的 UID/GID,setuid 程序切换身份的"备份" |
fsuid / fsgid | kuid_t / kgid_t | 文件系统 UID/GID,专用于文件访问检查 |
cap_inheritable | kernel_cap_t | exec 时继承的能力集 |
cap_permitted | kernel_cap_t | 允许使用的能力超集 |
cap_effective | kernel_cap_t | 当前有效的能力集,实际权限检查用 |

- 🏷️ 关键特性:
- 内核只看 euid 做权限检查,real/saved/fs uid 只是辅助切换角色
- 四种 UID 让 setuid 程序可以在 root 和普通用户之间安全来回切换
- capabilities 将 root 全权拆分为 40+ 个独立位(
CAP_SYS_ADMIN、CAP_NET_RAW等),实现最小权限 cap_permitted是进程能拥有的能力的上限,cap_effective是当前实际在用的子集,可以临时关掉不需要的能力
1.2.6 命名空间与控制组(nsproxy + cgroup)
task->nsproxy 和 task->cgroups 共同构成容器的内核底座——命名空间隔离"看到什么",cgroup 限额"能用多少"。
命名空间的组织方式:nsproxy 是一个"代理"结构,内嵌 8 个命名空间指针。所有命名空间通过一个 nsproxy 统一管理,fork 时如果不想创建新命名空间,则直接共享 nsproxy,引用计数 +1。
nsproxy 核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
ns[NR_NAMESPACES] | struct ns_common*[] | 命名空间数组,含 PID/NET/MNT/UTS/IPC/USER/CGROUP/TIME 共 8 种 |
count | atomic_t | 引用计数,共享本 nsproxy 的 task 数量 |
8 种命名空间一览:
| 命名空间 | 隔离内容 | clone flag | 典型场景 |
|---|---|---|---|
| PID | 进程 ID 编号 | CLONE_NEWPID | 容器内 PID 1 是宿主机上的普通进程 |
| NET | 网络设备、IP、端口、路由表 | CLONE_NEWNET | 每个容器有独立网卡和 IP |
| MNT | 文件系统挂载点视图 | CLONE_NEWNS | 容器有自己的 / 挂载视图 |
| UTS | 主机名和域名 | CLONE_NEWUTS | 每个容器可设独立 hostname |
| IPC | System V IPC、POSIX 消息队列 | CLONE_NEWIPC | 防止容器间 IPC 通信 |
| USER | 用户/组 ID 映射 | CLONE_NEWUSER | 非 root 用户在容器内可以是 root |
| CGROUP | cgroup 层级视图 | CLONE_NEWCGROUP | 容器内的 cat /proc/self/cgroup 看到虚路径 |
| TIME | 系统时钟(启动时间、单调时间) | CLONE_NEWTIME | 容器可拥有独立的"开机时间" |
cgroup 的组织方式:task->cgroups 指向 css_set,记录该任务在所有 cgroup 子系统中的成员位置。cgroup 采用层级树结构,每个子系统独立建树,css_set 是 task 在各子树中的位置快照。

- 🏷️ 关键特性:
- 命名空间实现进程的视图隔离——同一组内核资源,在不同 ns 中看到不同的"名称"
- cgroup 实现进程的资源限额——将 task 分组,每组配置 CPU/内存/IO 上限
- 容器 = 命名空间(隔离视图)+ cgroup(限额资源)+ seccomp(过滤系统调用)+ pivot_root(隔离文件系统)
1.2.7 IO上下文(io_context)
task->io_context 指向 io_context,管理进程的异步 IO 请求和 IO 调度优先级。它是块设备 IO 调度器的进程级控制入口。
两种职责的分工:
- AIO 请求跟踪:当进程调用
io_submit()发起异步 IO,内核在io_context中创建aio_kiocb记录请求状态。这个链表是内核 IO 完成时查找回调函数的依据 - IO 调度优先级:通过
ionice系统调用写入ioprio字段,分 RT / Best-effort / Idle 三级,影响 CFQ、BFQ 等调度器分配给该进程的 IO 时间片
核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
ioprio | unsigned short | IO 调度优先级,ionice 设置,分 RT/BE/IDLE 三级,每级 0~7 子级 |
aio_ring | struct aio_ring* | AIO 环形缓冲区,用于内核→用户态传递完成事件 |
refcount | atomic_t | 引用计数,CLONE_IO 的线程共享时 +1 |

- 🏷️ 关键特性:
io_submit/io_getevents的 AIO 请求全部挂在io_context中,内核完成时通过aio_ring通知用户态ionice分三级:RT(最高,独占 IO 带宽)> Best-effort(默认,0~7 子级)> Idle(仅闲时 IO)- 同进程线程通过
CLONE_IO共享io_context,IO 优先级统一
1.2.8 seccomp 系统调用过滤
task->seccomp 指向 seccomp_filter 链表,在每个系统调用入口处拦截、检查、过滤。它是进程沙箱的最后一道防线——在系统调用真正执行前完成 BPF 规则匹配。
组织方式:seccomp 过滤器是一个单向链表,每个节点包含一条 BPF 程序和对应的过滤模式。多个过滤器可以叠加,内核依次执行每个过滤器中的 BPF 程序,按"最严格结果"原则生效。一旦设置了第一个过滤器,只能向后追加更严格的规则,不能移除。
两种模式:
| 模式 | 行为 | 适用场景 |
|---|---|---|
SECCOMP_SET_MODE_STRICT | 仅允许 read/write/_exit/sigreturn,其他调用直接 SIGKILL | 极简沙箱 |
SECCOMP_SET_MODE_FILTER | 加载自定义 BPF 程序,按规则匹配系统调用号和参数 | Chrome、Docker 等复杂沙箱 |
BPF 返回值及含义:
| 返回值 | 行为 |
|---|---|
SECCOMP_RET_ALLOW | 放行,执行系统调用 |
SECCOMP_RET_ERRNO | 返回指定错误码(如 EPERM),不执行系统调用 |
SECCOMP_RET_TRAP | 向进程发送 SIGSYS 信号 |
SECCOMP_RET_KILL | 直接杀死进程 |
核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
filter | struct seccomp_filter* | 过滤器链表头,每节点含 BPF 程序和过滤模式 |
mode | int | 当前模式:0=禁用 / 1=STRICT / 2=FILTER |

- 🏷️ 关键特性:
- seccomp 过滤器只能追加、不能移除——安全不可逆,只能加严不能放宽
- BPF 程序可以检查系统调用号、参数值和架构,实现精细过滤
- fork 子进程继承过滤器,exec 新程序保留过滤器,沙箱贯穿整个进程生命周期
- Chrome 渲染进程和 Docker 容器默认使用 seccomp,拦截危险系统调用(如
clone、mount)
1.2.9 Futex 健壮列表(robust_list)
task->robust_list 指向 robust_list_head,解决多线程编程中的致命问题:持有锁的线程异常退出 → 等待者永久死锁。
工作流程:
- 注册:线程获取 robust mutex 时,glibc 将该锁的用户态地址写入
robust_list链表 - 检测:线程异常退出(被信号杀死、段错误等),内核在
do_exit()中遍历robust_list,找出所有该线程持有的 futex 锁 - 代解锁:内核执行
futex(WAKE)标记锁为FUTEX_OWNER_DIED,唤醒等待队列中的线程 - 恢复:被唤醒的线程收到
EOWNERDEAD,知道锁的前任持有者已死亡,可检查共享数据一致性并继续
核心字段:
| 字段 | 类型 | 含义 |
|---|---|---|
robust_list | struct robust_list_head* | 健壮锁链表头指针,每个线程一条链表 |
futex_offset | long | futex 字段在锁结构内部的偏移量 |

- 🏷️ 关键特性:
- 线程异常退出时,内核自动代解锁所有健壮 futex 锁,防止等待者永久死锁
- 被唤醒者收到
EOWNERDEAD,知道数据可能处于半修改状态,需要自行一致性检查 - 每个线程维护自己的
robust_list,只记录本线程持有的锁
二、资源对象与共享行为速查
§1.2.1~1.2.9 已逐项剖析了各资源对象的结构、字段表和 PlantUML 类图,本节仅汇总 fork 与线程的共享行为差异——同一资源对象,两种创建方式处理完全不同:
| § | 资源对象 | fork 行为 | 同进程线程 | 关键后果 |
|---|---|---|---|---|
| 1.2.1 | mm_struct | 复制(CoW) | 共享(CLONE_VM) | 线程间指针互传、全局变量互见 |
| 1.2.2 | files_struct | 复制fd表(条目共指同一file) | 共享(CLONE_FILES) | fork后父子共享读写偏移 |
| 1.2.3 | fs_struct | 复制 | 共享(CLONE_FS) | 一个线程chdir全员cwd变 |
| 1.2.4 | sighand / signal | 复制 | 共享(CLONE_SIGHAND/CLONE_THREAD) | 信号处理动作全线程组一致 |
| 1.2.5 | cred | 复制(继承) | 共享 | setuid影响所有线程 |
| 1.2.6 | nsproxy | 默认继承(可CLONE_NEW*) | 共享 | unshare仅改变当前task的ns |
| 1.2.7 | io_context | 继承(引用+1) | 共享(CLONE_IO) | IO优先级全线程统一 |
| 1.2.8 | seccomp | 继承(只能加严) | 共享 | 沙箱贯穿进程生命周期 |
| 1.2.9 | robust_list | 清空 | 每线程独立 | 子进程不继承父的futex锁 |
📄 各资源的结构、字段、组织方式详见 §1.2.1~1.2.9;clone flag 完整对照表见 §三;每类资源的逐篇深度剖析见 task-resources/。
三、进程 vs 线程:完整共享规则总表
同一套 clone 调用,决定这些指针指向新对象(进程)还是指向同一对象(线程)。共享得越多越像线程,越少越像进程。
| 资源对象 | 内核结构 | fork 子进程 | 同进程线程 | clone flag |
|---|---|---|---|---|
| 地址空间 | mm_struct | 复制(CoW) | 共享 | CLONE_VM |
| 打开文件表 | files_struct | 复制 | 共享 | CLONE_FILES |
| 文件系统上下文 | fs_struct | 复制 | 共享 | CLONE_FS |
| 信号处理表 | sighand_struct | 复制 | 共享 | CLONE_SIGHAND |
| 线程组信号状态 | signal_struct | 复制 | 共享 | CLONE_THREAD |
| 凭证 | cred | 复制(继承) | 共享 | — |
| 命名空间 | nsproxy | 默认继承(可 CLONE_NEW*) | 共享 | CLONE_NEW* |
| IO 上下文 | io_context | 继承(引用+1) | 共享 | CLONE_IO |
| seccomp filter | seccomp | 继承(只加严) | 共享 | CLONE_SECCOMP |
| robust_list | robust_list_head | 清空 | 每线程独立 | — |
| rseq | rseq | 不继承 | 每线程独立注册 | — |
| 私有字段 | pid/state/调度/统计... | 复制/清零 | 各自独立 | — |
fork 与 clone 时的完整场景图


更完整的 fork 过程(CoW、fd 复制、信号处理)见 process-creation;线程创建细节见 thread-creation。
四、pid vs tgid:内核怎么看"线程组"
用户说的"进程 PID",内核里叫 tgid(thread group id);用户说的"线程 TID",才是内核的 pid。一个多线程进程 = 一组共享 tgid 的 task:

这解释了为什么
top -H、ps -L、/proc/<pid>/task/<tid>能把一个进程"摊开"成多个线程——它们本就是内核里一组独立的 task,只是共享了 tgid 和资源对象。
五、进程的生命周期状态(内核视角)
task->__state 记录进程当前处于哪个状态,调度器据此决定要不要给它 CPU。详见 task-private §三。

- R(RUNNING):在运行队列里,正跑或随时可跑。
- S(可中断睡眠):等某个事件,能被信号打断——绝大多数"闲着"的进程是这个状态。
- D(不可中断睡眠):不响应信号,
kill -9都杀不掉。大量 D 状态是 IO 卡死的信号。折中方案TASK_KILLABLE允许被致命信号唤醒。 - T(停止):被
SIGSTOP或调试器暂停。 - Z(僵尸):进程已
exit,资源大都释放,但task_struct暂留存退出码,等父wait()回收。父不 wait → 僵尸堆积;父先死 → 子被 init(PID 1)收养。
六、从 /proc 反向看这些内核结构
/proc/<pid>/ 是内核把 task_struct 及其资源对象导出成文件的窗口——读它就是在读内核里那个进程的实时状态:
cat /proc/<pid>/status # 人类可读汇总:Name/State/Tgid/Pid/PPid/Threads/Uid/CapEff...
cat /proc/<pid>/stat # 单行原始字段:state/utime/stime/min_flt/maj_flt/starttime/prio...
ls /proc/<pid>/task/ # 线程组所有线程(每个 tid 一个子目录) → 对应多个 task_struct
ls /proc/<pid>/fd/ # files_struct:打开的每个 fd 及其指向(→ lsof)
cat /proc/<pid>/maps # mm_struct 的 VMA 列表:每段虚拟地址区间/权限/后备文件
cat /proc/<pid>/cmdline # exec 进来的命令行
ls -l /proc/<pid>/cwd /proc/<pid>/root # fs_struct:当前目录、根目录
ls -l /proc/<pid>/ns/ # nsproxy:各命名空间
cat /proc/<pid>/cgroup # 所属 cgroup
cat /proc/<pid>/limits # rlimit 资源上限
cat /proc/<pid>/sched # 调度参数:policy/prio/se...
cat /proc/<pid>/status | grep Seccomp # seccomp 状态:0=禁用/1=STRICT/2=FILTER对照本篇:status/stat/sched 是 task_struct 私有字段的导出、maps↔mm_struct、fd/↔files_struct、cwd/root↔fs_struct、ns/↔nsproxy、task/↔线程组里的多个 task、Seccomp↔seccomp 过滤器。
七、和本仓库其他文档的关系
- 各资源对象的逐篇详解:子目录 task-resources/ —— task-private · mm-struct · files-struct · fs-struct · signals-kernel · cred · namespaces-cgroups · io-context。
- 进程/线程创建:process-creation(fork/exec/clone 怎么复制或共享这些结构、CoW)、thread-creation(线程 = 共享大部分对象的 task)、fork-and-threads(多线程 fork 只带调用线程的坑)。
- 调度:scheduling(task 怎么被选中跑)、thread-affinity(
cpus_ptr亲和性)、top(STAT 状态列)、pidstat(utime/stime/上下文切换)。 - 切换与冻结点:context-switch(
switch_to的 SP 切换魔法)、kernel-stack-snapshot(内核栈快照——thread_struct.sp 如何保存冻结点)。 - 反向观测:procfs(
/proc/<pid>/怎么导出这些结构)。
八、一句话总结
内核不分进程与线程,只有一种调度实体 task,每个 task 一张
task_struct。这张表里分两类东西:私有字段(pid/tgid、状态、调度参数、CPU 寄存器、统计计数——只描述这个 task 自己)和资源对象指针(mm_files/fs/signal/cred/nsproxy/io_context/seccomp...——指向可被多 task 共享的独立对象)。同进程线程通过 clone flags 共享资源指针(共指同一对象),不同进程 fork 时复制这些对象(各自独立)。fork/clone 的全部戏法就是决定这些指针复制新对象还是共指同一份——共享得多就是线程、共享得少就是进程。这一切都能透过/proc/<pid>/反向看到。