目录
[0. 状态总览](#0. 状态总览)
[1. 运行态(Running / TASK_RUNNING)](#1. 运行态(Running / TASK_RUNNING))
[2. 就绪态(Ready)](#2. 就绪态(Ready))
[3. 阻塞态(Blocked / Sleeping)](#3. 阻塞态(Blocked / Sleeping))
[4. 僵尸态(Zombie / EXIT_ZOMBIE)](#4. 僵尸态(Zombie / EXIT_ZOMBIE))
[5. 孤儿态(Orphan)](#5. 孤儿态(Orphan))
面试三层级方法论
• 第一层(定义级):能说出是什么、有什么用 ------ 及格线
• 第二层(原理级):能讲清楚底层怎么实现、为什么这么设计 ------ 加分项
• 第三层(对比级):能横向对比同类方案、讲 trade-off、结合项目说
一、进程概念(Process)
第一层・定义级
是什么: 进程是 程序的一次运行实例 ,是操作系统进行 资源分配和调度的基本单位。
- 程序 = 存放在磁盘上的二进制文件(静态的、死的)
- 进程 = 程序被加载到内存中运行后的实体(动态的、活的)
有什么用:
- 资源隔离:每个进程有独立的地址空间,一个进程崩了不影响其他进程
- 并发执行:OS 通过切换进程让多个程序 "同时" 运行
- 权限管理:每个进程有 uid/gid,系统据此控制资源访问权限
第二层・原理级
底层怎么实现: Linux 内核用一个叫 task_struct(进程描述符)的巨型结构体来描述一个进程,存在内核栈底部 / 单独的 slab 缓存中。
核心字段:
cpp
struct task_struct {
volatile long state; // 进程状态
void *stack; // 进程内核栈指针
pid_t pid; // 进程ID
pid_t tgid; // 线程组ID
struct mm_struct *mm; // 用户态地址空间
struct files_struct *files;// 打开的文件表
struct signal_struct *signal; // 信号处理
struct sched_entity se; // 调度实体(CFS调度用)
struct task_struct *parent;// 父进程指针
// ... 几百个字段
};
为什么这么设计:
- 独立地址空间(mm_struct):通过页表实现虚拟内存隔离,安全性高,代价是进程切换要刷 TLB,开销大
- task_struct 放内核态:用户态无法直接修改,保证内核数据结构安全
- 父子关系树形结构:所有进程以 init 进程为根形成进程树,便于资源回收和信号传递
第三层・对比级
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 资源粒度 | 资源分配基本单位,独立地址空间 | 调度基本单位,共享进程地址空间 | 用户态调度,共享线程栈空间 |
| 切换开销 | 大(页表、TLB、文件表全换) | 小(只换寄存器和栈) | 极小(用户态完成,无内核陷入) |
| 通信方式 | IPC(管道、消息队列、共享内存) | 共享变量 + 锁 | 直接函数调用式协作 |
| 崩溃影响 | 单个进程崩溃不影响其他 | 一个线程崩溃 → 整个进程挂 | 同线程内协程一起挂 |
| 适用场景 | 需要强隔离的独立程序 | CPU 密集型并发、IO 多路复用 | 高并发 IO、轻量任务调度 |
二、进程状态详解
0. 状态总览
Linux 内核中 state 字段的核心宏定义:
cpp
#define TASK_RUNNING 0x0000 // 运行/就绪态(Linux合并了)
#define TASK_INTERRUPTIBLE 0x0001 // 可中断阻塞
#define TASK_UNINTERRUPTIBLE 0x0002 // 不可中断阻塞
#define __TASK_STOPPED 0x0004 // 暂停态
#define EXIT_ZOMBIE 0x0020 // 僵尸态
#define EXIT_DEAD 0x0040 // 死亡态
1. 运行态(Running / TASK_RUNNING)
第一层・定义级
是什么: 进程正在 CPU 上执行 ,或者已经准备好随时可以被调度执行。
⚠️ 注意:Linux 把 "运行中" 和 "就绪" 合并成了同一个状态
TASK_RUNNING,这是和教科书最大的区别!
有什么用: 标记这个进程是调度器的候选对象,会被放进运行队列(runqueue)里等待 CPU 时间片。
第二层・原理级
底层实现:
- 每个 CPU 有一个 CFS(完全公平调度器)运行队列
cfs_rq - 所有
TASK_RUNNING状态的进程按vruntime(虚拟运行时间)挂在红黑树上 - 调度器每次挑 vruntime 最小的进程上 CPU 运行
第三层・对比级
- 运行中 vs 就绪:Linux 内核层面不分,但从 "是否占用 CPU" 的角度区分 ------ 占用 CPU 的叫 running,在队列里排队的叫 ready
- 时间片耗尽:进程用完时间片 → 重新入队,状态不变(还是 TASK_RUNNING),只是从 CPU 上下来了
2. 就绪态(Ready)
第一层・定义级
是什么: 进程万事俱备,只等 CPU。所有资源都分配好了,就差被调度器选中上 CPU 执行。
有什么用: 区分 "能跑但没轮到" 和 "根本跑不了(阻塞)" 的进程,调度器只从就绪队列里挑进程。
第二层・原理级
底层实现: 在 Linux 里就是 TASK_RUNNING 状态但当前不在 CPU 上的那些进程,挂在每个 CPU 的 runqueue 红黑树上。
进入就绪态的时机:
fork()创建完新进程,初始化完成后- 阻塞的进程等到了事件(IO 完成、信号到来)
- 被抢占的进程(高优先级进程来了)
- 时间片用完的进
第三层・对比级
| 状态 | 等待的资源 | 能否被调度 |
|---|---|---|
| 就绪 | 只缺 CPU | ✅ 在 runqueue 里,随时可调度 |
| 阻塞 | 缺 IO / 锁 / 事件 | ❌ 不在 runqueue,调度器看不到 |
3. 阻塞态(Blocked / Sleeping)
第一层・定义级
是什么: 进程因为等待某个事件发生而主动放弃 CPU,暂停执行。事件到来之前,给它 CPU 也跑不了。
两种阻塞:
- 可中断阻塞(TASK_INTERRUPTIBLE) :等事件 + 收到信号也会唤醒(比如
sleep()、wait()) - 不可中断阻塞(TASK_UNINTERRUPTIBLE):只能等事件,信号都不响应(比如磁盘 IO 进行中)
有什么用: 避免忙等(busy waiting)浪费 CPU,让 CPU 去跑别的进程,事件来了再唤醒。
第二层・原理级
底层怎么实现:
- 进程把自己从 runqueue 中移除
- 把自己挂到某个**等待队列(wait_queue)**上
- 修改 state 为
TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE - 调用
schedule()主动触发调度,让出 CPU
唤醒时反向操作:事件触发后,等待队列上的进程被唤醒,state 改回 TASK_RUNNING,重新加入 runqueue。
为什么这么设计:
- 等待队列是 "生产者 - 消费者" 模型:事件生产者(如中断处理函数)唤醒等待者
- 不可中断阻塞存在的意义:防止 IO 过程中被信号打断导致数据不一致(比如写到一半的磁盘)
第三层・对比级
| 阻塞类型 | 唤醒条件 | 典型场景 | ps 显示 |
|---|---|---|---|
| 可中断 | 事件发生 + 任意信号 | sleep、wait、读终端 | S |
| 不可中断 | 只能是事件发生 | 磁盘 IO、锁 Semaphore | D |
Demo:阻塞态观察
cpp
// demo_block.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
printf("PID: %d, 我马上进入 sleep(可中断阻塞)...\n", getpid());
printf("另开终端执行: ps -o pid,stat,cmd -p %d\n", getpid());
sleep(30); // 睡眠30秒,状态为 S(可中断睡眠)
printf("睡醒了,结束\n");
return 0;
}
bash
gcc demo_block.c -o demo_block && ./demo_block
# 另一个终端:ps -o pid,stat,cmd -p <pid>
# 会看到 STAT 是 S+(前台进程的可中断睡眠)
4. 僵尸态(Zombie / EXIT_ZOMBIE)
第一层・定义级
是什么: 进程已经执行结束退出了,但它的父进程还没有调用 wait()/waitpid() 来收尸 ,导致 task_struct 还没被释放。
有什么用: 保留退出状态码和资源使用统计,等父进程来 "收尸" 读取。僵尸进程本身几乎不占资源,只占一个 PID 和一个 task_struct 结构体。
第二层・原理级
底层实现:
- 进程调用
exit()→ 释放用户态资源(地址空间、文件、内存) - 状态设为
EXIT_ZOMBIE - 给父进程发
SIGCHLD信号 - 父进程调用
wait()读取退出状态后,内核才释放 task_struct
为什么这么设计:
- 父进程可能需要知道子进程是正常退出还是异常崩溃、退出码是多少、用了多少 CPU 时间
- 这些信息必须在进程死后保留一段时间,所以设计了 "僵尸" 这个中间状态
第三层・对比级
| 维度 | 僵尸进程 | 孤儿进程 |
|---|---|---|
| 谁死了 | 子进程死了,父进程活着 | 父进程死了,子进程活着 |
| 资源占用 | 用户态资源已释放,只剩 task_struct | 正常运行,占完整资源 |
| 危害 | 大量僵尸占满 PID 号,导致无法创建新进程 | 无危害,被 init 收养 |
| 怎么解决 | 父进程 wait,或 kill 父进程让 init 接管 | 不需要解决,正常现象 |
怎么杀僵尸进程? → 杀不了,它已经死了。只能 kill 它的父进程,让它变成孤儿,然后被 init 进程收养并回收。
Demo:僵尸进程制造与观察
bash
// demo_zombie.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
exit(1);
} else if (pid == 0) {
// 子进程:立刻退出,变成僵尸
printf("子进程 PID=%d 即将退出,变成僵尸...\n", getpid());
exit(0);
} else {
// 父进程:不 wait,休眠30秒让你观察
printf("父进程 PID=%d,子进程 PID=%d\n", getpid(), pid);
printf("另开终端执行: ps -o pid,stat,cmd -p %d\n", pid);
sleep(30);
printf("父进程结束\n");
}
return 0;
}
bash
gcc demo_zombie.c -o demo_zombie && ./demo_zombie
# 另一个终端:ps aux | grep Z 或 ps -o pid,stat <子pid>
# 会看到 STAT 是 Z+(僵尸状态)
5. 孤儿态(Orphan)
第一层・定义级
是什么: 父进程先退出了,子进程还在运行,这个子进程就成了孤儿。
有什么用: 不是故意设计的状态,是一种现象。Linux 会自动把孤儿进程过继给 init 进程(PID=1),由 init 来负责回收它的尸体,保证不会永远僵尸。
第二层・原理级
底层实现:
- 父进程 exit 时,遍历自己的所有子进程
- 如果子进程还活着(非 EXIT_DEAD),就把它的父进程指针
p->parent指向child_reaper(通常是 init) - 同时给新的父进程(init)发 SIGCHLD
为什么这么设计:
- 必须有一个兜底机制,否则父进程意外死亡后,子进程退出时永远没人收尸,PID 会泄漏
- init 进程天生自带 wait 循环,专门收尸
第三层・对比级
| 对比项 | 孤儿进程 | 僵尸进程 |
|---|---|---|
| 进程是否活着 | ✅ 活着,正常运行 | ❌ 已死,只剩壳 |
| 占用资源 | 完整的内存、文件、CPU 时间 | 几乎不占,只占 PID |
| 回收方式 | 退出时由 init 自动回收 | 必须父进程 wait 或父死被 init 收 |
| 算不算问题 | 不算,系统自动处理 | 大量累积才是问题(PID 耗尽) |
bash
// demo_orphan.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid < 0) {
perror("fork失败");
exit(1);
} else if (pid == 0) {
// 子进程
printf("子进程: PID=%d, 父进程PPID=%d\n", getpid(), getppid());
sleep(3); // 等父进程先死
printf("子进程: 现在我的PPID=%d(应该是1或systemd)\n", getppid());
printf("子进程: 我是孤儿,被init收养了\n");
sleep(10);
printf("子进程退出\n");
} else {
// 父进程:立刻退出
printf("父进程: PID=%d,我先走一步\n", getpid());
exit(0);
}
return 0;
}
bash
gcc demo_orphan.c -o demo_orphan && ./demo_orphan
# 观察:子进程打印的 ppid 从父进程PID变成了1(或systemd的PID)