本页目录
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 行)。初次阅读不需要逐字段记忆,理解分层归属即可:
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:
// fork(): 全新进程
;
// pthread_create(): 共享地址空间的"线程"
;
// vfork(): 阻塞父进程直到子进程 exec/exit (已基本被 fork+COW 取代)
;
关键 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 如何被找到?
// 当前进程的 task_struct (x86_64 上最快)
// 实现: 从内核栈指针的低位掩码得到 thread_info,再取 task
static __always_inline struct task_struct *
5.x+ 已把 task_struct * 直接放进 per-CPU 变量 pcpu_hot.current_task,不再需要从内核栈指针间接计算。
进程状态机
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,不一定是当前在跑的那个。
|
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 引用。
状态存储演进
// 旧 (2.6 ~ 5.12): long state
task->state = TASK_UNINTERRUPTIBLE;
// 新 (5.14+): 通过 WRITE_ONCE/READ_ONCE 访问
;
改为 __state 的目的是强制使用 WRITE_ONCE/READ_ONCE 宏,避免编译器撕裂写(tearing)或乱序导致的状态不一致。
fork:进程诞生
入口与路径
// glibc: fork() → clone(SIGCHLD, ...)
// kernel (5.x 统一入口):
→
→
kernel_clone() (kernel/fork.c) 是统一的 clone 入口(5.x 重构,之前叫 _do_fork())。传入的 flags 决定了共享哪些资源。
copy_process — 核心(~1500行)
Copy-on-Write 是 fork 快的秘密
// kernel/fork.c: copy_page_range() → copy_p4d_range() → ... → copy_pte_range()
static inline int
后继 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:脱胎换骨
// fs/exec.c
→ → →
exec 不创建新进程,而是把当前进程的地址空间整个换掉。PID 不变,但运行的代码、数据、栈全部换新。
执行流程
setuid/setgid 与 credential
如果 ELF 文件设置了 suid bit,exec 还会替换 credential:
// fs/exec.c
if
这是内核中权限模型的核心——prepare_binprm() 中完成。
exit:进程消亡
do_exit 路径
// kernel/exit.c
void __noreturn
僵尸进程详解
孤儿进程收养:如果父进程先于子进程退出,exit_notify() 会调用 find_new_reaper() 把子进程过继给:
- 同一线程组的其他线程
- 同一 PID namespace 的
init(PID 1)
这就是为什么 PID 1 不会让僵尸积累——init 进程主动循环 wait()。
僵尸进程调试
# 查找僵尸进程
|
# 僵尸占用的资源
# 僵尸的父进程
|
# 如果父进程不 wait(),只能 kill 父进程
# 然后 init 收养僵尸 → 自动回收
内核线程
内核线程是没有用户空间的特殊进程(mm == NULL)。它们运行在内核态,创建和管理不同于用户进程。
// include/linux/kthread.h
struct task_struct *;
void ;
int ;
bool ;
// 快捷版: 创建 + 唤醒一步到位
kthreadd (PID 2)
所有内核线程的父进程。启动路径:
start_kernel() → rest_init() → kernel_thread(kernel_init, ...)
└─ kernel_thread(kthreadd, ...)
kthreadd 循环处理 kthread_create_list:取出请求 → 调用 create_kthread() → kernel_thread() 创建新线程。
内存与调度特点
// 内核线程没有自己的 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> | 内存碎片整理 |
# 查看所有内核线程
| |
# kthreadd 的子进程树
关键 /proc 接口
# /proc/<pid>/stat — 进程状态(纯数字,快速解析)
# 字段: pid comm state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime ...
# /proc/<pid>/status — 人类可读版本
# 含: Name, State, Tgid, Pid, PPid, Uid, VmSize, VmRSS, Threads, voluntary_ctxt_switches, nonvoluntary_ctxt_switches
# /proc/<pid>/task/<tid>/ — 线程目录
# /proc/sys/kernel/pid_max — PID 上限 (默认 32768,可调到 4194304)
参考与延伸
- 内核文档:
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