Linux 进程与 PID 体系
进程(process)与进程号(PID)是理解 Linux 的第一道门槛。本文从"内核程序有没有 PID"这个高频疑问出发,系统梳理 内核态 vs 用户态、PID 0 / 1 / 2、内核线程、进程生命周期与状态机、task_struct 对象模型,并附实战排查手册。与 Linux 系统架构(偏整体分层与启动)、Linux 系统调用手册(偏 syscall)互补,本文聚焦"进程与 PID"。
1. 概述
1.1 核心辨析:内核程序有 PID 吗?
这是最常见的概念混淆点,一句话回答:
内核代码本身没有 PID。 PID 是内核为"用户态进程"分配的标识符,内核代码运行在进程模型之外(它没有 task_struct、不参与进程调度)。但内核会以"内核线程"的形式派生出有 PID 的执行实体——它们运行在内核态,但拥有进程描述符和 PID。
要分清楚三个层次:
| 层次 | 有 PID 吗 | 运行位置 | 说明 |
|---|---|---|---|
| 内核代码(函数、syscall 实现、驱动、中断处理) | 无 | 内核态(ring 0) | 在进程的"上下文"里被调用的代码,不构成独立调度实体 |
| 内核线程(kthreadd、kworker、ksoftirqd…) | 有(PID 2 起) | 内核态(ring 0) | 有 task_struct,无用户地址空间(mm = NULL),不可被用户直接 kill |
| 用户态进程(systemd、bash、nginx…) | 有(PID 1 起) | 用户态(ring 3),通过 syscall 陷入内核 | 系统里"绝大多数"进程,就是我们常说的"程序" |
关键验证命令:ps -e | wc -l 能数到内核线程,因为它们在进程表里;但内核自身的函数/中断永远不在进程表里。
1.2 PID 编号体系:0 / 1 / 2
Linux 启动时由内核按固定顺序建立最早的三类"进程",它们的 PID 有特殊含义:
| PID | 进程名 | 态别 | 职责 |
|---|---|---|---|
| 0 | swapper / idle(每 CPU 一个) | 内核态(调度器) | 空闲进程,没有可运行任务时调度它;严格说是内核调度的一部分,多数工具不显示 |
| 1 | systemd(init 的现代实现) | 用户态 | 所有用户态进程的"祖先",收养孤儿进程,拉起全部服务 |
| 2 | kthreadd | 内核态 | 内核线程的"孵化器",所有内核线程都由它 fork 产生 |
最容易答错的地方:PID 1(systemd)是用户态进程,不是内核态。它只是"权限最高的用户态进程"——以 root 运行、被内核特别对待(收养孤儿、屏蔽默认信号处理),但它的代码确实跑在 ring 3,靠系统调用请求内核服务。而 PID 2(kthreadd)才是真正的内核态常驻线程。
1.3 专有名词总览
专有名词:
- 进程 process — 正在运行的程序实例,内核以
task_struct描述它,用户态以 PID 指代。 - 内核线程 kernel thread — 内核态的调度实体,有 task_struct 但无用户地址空间(mm 为空),名字通常带方括号
[kworker/0:1]。 - PID / TGID / TID — PID(进程号,对多线程进程即线程组号 TGID);TID 是每个线程的线程号,
getpid()返回 TGID,gettid()返回 TID。 - PPID — 父进程号,
getppid()返回;父进程退出后孤儿进程被重交给 PID 1(或 subreaper)。 - 孤儿进程 orphan — 父进程先退出的子进程,被内核重交给 init(PID 1)收养。
- 僵尸进程 zombie — 子进程已退出但父进程未
wait()回收,占据进程表条目,状态显示 Z。 - 调度器 scheduler — 内核中决定"下一个运行谁"的组件,运行在 ring 0。
- 用户态 / 内核态 — CPU 特权级(ring 3 / ring 0)的通俗说法,二者通过系统调用切换。
- init 进程 — PID 1 的通用称呼,负责启动系统与收养孤儿,现代发行版多为 systemd。
2. 行为图(系统"做什么")
2.1 用例图 — 进程管理交互全景
围绕进程的创建、查询、控制、回收四大行为,看用户、管理员与内核各自的角色。
图 1:进程管理用例图(行为全景)
用例演示
ps aux | grep nginx # 查询:查看进程与 PID/PPID kill -TERM 1234 # 控制:发送信号 nice -n 5 ./job # 控制:调整调度优先级 systemctl start nginx # 控制:通过 PID 1 拉起服务 cat /proc/1234/status # 查询:读取进程详细属性
专有名词:fork / exec 是创建进程的两步——fork 复制一份当前进程(得新 PID),exec 用新程序覆盖其地址空间(PID 不变);kill 实为"发信号"系统调用,并不一定终止进程;wait 让父进程回收已退出的子进程,终结僵尸状态。
2.2 活动图 — 进程生命周期主流程
从 fork 创建到 wait 回收,展示进程的一生与内核态的介入点。
图 2:进程生命周期主流程活动图(带泳道)
用例演示
# 终端一:前台运行,观察 fork/exec 两阶段 strace -f -e trace=fork,execve sleep 10 # 终端二:对照进程树与状态 pstree -p | head ps -o pid,ppid,state,comm -p $(pgrep -n sleep)
专有名词:fork() 创建几乎完全相同的子进程(复制 task_struct、页表、fd 表),COW(写时复制)延迟物理内存复制;execve() 加载新二进制,重置地址空间与寄存器,但 PID 不变;exit() 释放大部分资源、进入僵尸态等待回收;wait() 收割僵尸并取回退出码。
2.3 状态机图 — 进程状态转换
ps 输出的 STAT 列就是这套状态机,看懂它等于看懂系统卡在哪。
图 3:进程状态机图
STAT 状态速查
| STAT | 含义 | 常见场景 |
|---|---|---|
| R | running,可运行/正在运行 | CPU 密集任务,或永远就绪 |
| S | sleeping,可中断睡眠 | 等待 I/O、等待事件、sleep(),最常见状态 |
| D | 不可中断睡眠(IO wait) | 等待磁盘/网络不可中断 I/O;长期 D 说明 IO 子系统卡死 |
| T | stopped,暂停 | Ctrl+Z 或 SIGSTOP 挂起 |
| Z | zombie,僵尸 | 已退出未回收,父进程没 wait() |
| + / s / l / < | 修饰符 | 前台进程组 / 会话首进程 / 多线程 / 高优先级 |
专有名词:不可中断睡眠(D) 是历史设计:某些内核操作(如磁盘驱动)不容被打断,进程收不到信号;现代内核已尽量用可中断路径替代。僵尸进程 不占用 CPU/内存,但占 PID 表项,大量堆积说明父进程有 bug(没调 wait)。
2.4 时序图 — fork() 内部调用
一次 fork() 从用户态进入内核态的完整过程。
图 4:fork() 内部调用时序图
用例演示
# 用 ftrace 观察 fork 在内核中的真实调用链 sudo trace-cmd record -p function_graph -l 'sys_fork' -l '_do_fork' sleep 0.2 sudo trace-cmd report | head -30
专有名词:系统调用 是用户态进入内核的唯一正规入口(x86-64 用
syscall 指令);copy_process() 是 fork 的核心实现,复制 task_struct 并分配新 PID;wake_up_new_task() 把新进程放入运行队列交给调度器;返回两次 —— 父子各返回一次,靠返回值区分(子进程返回 0,父进程返回子 PID)。
2.5 时序图 — 内核线程的诞生
内核线程不是从用户态 fork 来的,而是由 kthreadd(PID 2)在纯内核态孵化。
图 5:内核线程创建时序图
用例演示
# 查看当前所有内核线程(名字在方括号里的即是) ps -eo pid,comm | grep -E '\[.*\]' # 常见内核线程 # [kthreadd] PID 2 的孵化器 # [kworker/0:1] 每 CPU 工作队列线程 # [ksoftirqd/0] 每 CPU 软中断线程 # [kswapd0] 内存回收线程 # [xfsalloc] 文件系统后台线程
专有名词:kthreadd 内核启动后第一个真正的内核线程,永远睡眠在 kthread_create_list 上;kthread_create() 把创建请求放入列表唤醒 kthreadd,再由 kthreadd
kernel_thread() 实际 spawn;kthread_run() = 创建 + 启动一步到位。内核线程没有用户地址空间,永远只在内核态运行。
3. 结构图(系统"由什么组成")
3.1 部署图 — 内核态 vs 用户态分层
从 CPU 特权级出发,看清进程、内核线程、内核代码分别住在哪一层。
图 6:内核态 vs 用户态分层部署图
用例演示
# 判断 CPU 支持的特权级与当前模式(用户态 3) grep -m1 '^flags' /proc/cpuinfo | grep -q 'lm' && echo 'x86-64 ring 0/3 可用' # 进程在内核态的时间占比(含系统调用) top # 查看 %sys(内核态)、%us(用户态)、%wa(IO wait)
专有名词:ring 0 / ring 3 是 x86 的 CPU 特权级——ring 0 可执行特权指令(改页表、开中断、写 MSR),ring 3 必须通过 syscall 陷阱进内核;D-Bus 等是用户态服务,哪怕 systemd 用它与外部通信也改变不了 systemd 本身是用户态进程的事实。/proc 是内核暴露给用户态查看进程的唯一虚拟文件系统。
3.2 组件图 — 进程管理子系统
内核中负责"进程"的若干子系统如何协同。
图 7:进程管理子系统组件图
用例演示
# PID 分配器:查看当前 pid_max 与分配状态 cat /proc/sys/kernel/pid_max cat /proc/sys/kernel/ns_last_pid # 本 PID namespace 最后分配的 PID # 调度器:查看 CFS 组调度与优先级 cat /sys/kernel/debug/sched_features 2>/dev/null || true
专有名词:PID 分配器 维护全局 pidmap,分配后置位、回收后清零,按 pid_max 上限(默认 32768,64 位可调至 4194304);进程调度器(CFS/EEVDF)在运行队列中挑选下个进程;信号机制 负责进程间通知;cgroups/namespaces 负责资源隔离(容器基础)。
3.3 包图 — 内核源码目录
Linux 源码中与进程/PID 直接相关的核心文件。
图 8:进程相关内核源码包图
用例演示
# 对照源码快速定位(需先下载内核源码) # kernel/fork.c — copy_process()/_do_fork() # kernel/exit.c — do_exit()/wait() # kernel/pid.c — alloc_pid()/free_pid(),PID namespace 核心 # kernel/kthread.c — kthread_create()/kthreadd() # kernel/sched/core.c — 调度器主入口 # kernel/signal.c — 信号处理 # include/linux/sched.h — task_struct 定义
专有名词:task_struct 是进程描述符(俗称 PCB),每个进程/线程一份,是进程的"身份证";pid.c 里不仅有 PID,还有
struct pid 与 upid(支持 PID namespace 的嵌套);sched.h 定义调度相关字段与 CFS/EEVDF 的 vruntime。
3.4 类图 — task_struct 对象模型
进程在内核眼中的对象模型:字段很多,挑最重要的看。
图 9:task_struct 对象模型类图
用例演示
# 直接读真实 task_struct 的关键字段 cat /proc/$$/status | grep -E '^(Name|State|Pid|PPid|Tgid|NSpid|VmRSS|Threads):' # Name: bash # State: S (sleeping) # Pid: 1234 ← 本进程 PID # PPid: 1 ← 父进程 PID # Tgid: 1234 ← 线程组号(多线程下 = 主线程) # NSpid: 1234 ← 在各级 PID namespace 中的编号 # Threads: 1 ← 线程数
专有名词:mm 指向内存描述符 mm_struct,内核线程的 mm 为 NULL;parent / children / sibling 构成进程家族树;thread_group 把同线程组的所有线程串起来;pid / tgid 前者在用户态不可见,后者才是
getpid() 的值;NSpid 展示一个进程在多层 PID namespace 中的不同编号(容器里 PID 1,宿主机可能 45678)。
4. 实战速查手册
4.1 查看进程与 PID
| 命令 | 作用 |
|---|---|
ps -ef / ps aux | 全量进程,含 PID/PPID/STAT/COMMAND |
pstree -p | 进程树 + PID,直观看到 PID 1 为根 |
pgrep -l nginx | 按名查 PID |
pidof nginx | 查指定程序的 PID |
top / htop | 交互式监控,实时状态与 CPU/内存 |
cat /proc/<pid>/status | 单个进程详情感知(内核直接输出) |
echo $$ / echo $! | Shell 自身 PID / 最近后台任务 PID |
4.2 进程状态解读
| 现象 | STAT | 排查思路 |
|---|---|---|
| 大量 D 状态 | D | 磁盘/NFS 卡死,dmesg 查 IO 错误,检查 RAID、挂载点、NFS 服务器 |
| Z 僵尸堆积 | Z | 父进程没回收子进程,先查父进程(PPID)是否泄漏,kill 父进程由 PID 1 收割 |
| 进程 T 挂起 | T | kill -CONT <pid> 恢复,或 Ctrl+Z 后的 fg/bg |
| CPU 高但 R 少 | R | 可能在不可中断等待或内核软中断,用 pidstat/mpstat 细分 |
| 系统跑飞、swapper 占比极高 | — | 空闲状态,说明 CPU 没活干,正常 |
4.3 内核线程排查
# 列出所有内核线程 ps -eo pid,ppid,stat,comm | grep -E '\[.*\]' # 常见内核线程速查 # [kthreadd] PID 2,内核线程孵化器 # [kworker/N:x] 每 CPU 工作队列(处理延迟任务) # [ksoftirqd/N] 每 CPU 软中断线程 # [kswapd0] 内存回收 # [kblockd] 块层异步任务 # [jbd2/sda1-8] 日志文件系统提交线程 # [xfs_*] 文件系统后台线程 # 内核线程为什么 kill 不掉? kill -9 $(pgrep -f '\[kworker') # → 报错 "Operation not permitted":内核线程无用户态,用户无法向其发信号
专有名词:内核线程名字在
ps 里用方括号括起,表示它是内核内部实体、无用户态镜像;kworker 运行"工作队列",很多驱动后台任务委托给它;它们占 CPU 高时通常是驱动或 FS 出了问题。
4.4 PID 范围与调优
# 查看当前 PID 上限(默认 32768,即 2^15) cat /proc/sys/kernel/pid_max # 64 位系统可调高(最大 4194304 = 2^22) echo 4194304 | sudo tee /proc/sys/kernel/pid_max # 持久化 echo 'kernel.pid_max = 4194304' | sudo tee /etc/sysctl.d/99-pidmax.conf sudo sysctl -p /etc/sysctl.d/99-pidmax.conf
专有名词:pid_max 决定系统最多同时存在的进程/线程数上限;耗尽时
fork() 返回 EAGAIN("Resource temporarily unavailable")。容器场景每个 PID namespace 会重新从 1 编号,/proc/sys/kernel/ns_last_pid 可查看每个 namespace 的进度。
4.5 常见误区问答
| 误区 | 正解 |
|---|---|
| systemd 是 PID 1,所以它跑在内核态 | 错。systemd 是用户态进程(ring 3),只是权限最高、被内核特殊收养 |
| 内核程序有 PID | 内核代码无 PID;内核线程(kthreadd 等)有 PID |
| PID 0 是"第 0 个进程" | PID 0 = swapper/idle,是调度器的空闲任务,不参与正常进程调度 |
| kill 就是杀掉进程 | kill 是发信号,SIGTERM=优雅退出、SIGKILL=强制,其他信号各有用途 |
| 僵尸进程占用内存 | 僵尸只占一个 task_struct 表项(约 KB 级),不占内存/CPU,但占 PID |
| PID 永不重复 | PID 会回绕复用,但当前存活进程的 PID 全局唯一 |