进程的概念、组成、特征与状态转换全析
进程的概念、组成、特征与状态转换
引言
打开任务管理器,你会看到几十个正在运行的程序。微信在收发消息,浏览器在渲染页面,音乐播放器在解码音频。问题是:只有一个 CPU(或几个核心),怎么同时跑这么多程序?
答案就是进程。进程是操作系统分配资源、调度运行的基本单位。理解了进程,你就理解了操作系统最核心的设计思想。这篇文章是第二章"进程与线程"系列的第一篇,聚焦进程的概念、组成、特征和状态转换——这是 408 考研中每年都会涉及的高频考点。
📌 核心要点
- PCB(进程控制块)是进程存在的唯一标志,包含 PID、状态、资源清单等关键信息
- 进程实体 = PCB + 程序段 + 数据段;进程是动态运行过程,进程实体是某一时刻的静态快照
- 五状态模型是核心考点:运行态、就绪态、阻塞态、创建态、终止态,转换条件必须烂熟于心
- 进程切换通过原语实现,原语执行期间关中断,保证操作的原子性
- 线程是 CPU 调度的最小单位,同一进程内线程切换的开销远小于进程切换
目录
- 什么是进程?——从程序到进程的蜕变
- 进程的三块积木 —— PCB、程序段与数据段
- 进程的五个基本特征是什么?
- 进程的一生 —— 五状态模型与转换规则
- 操作系统如何组织成百上千个进程?
- 原语如何控制进程?
- 线程——比进程更轻量的执行单元
- 进程 vs 线程 —— 一张表看清本质区别
- FAQ
- 总结与系列导航
什么是进程?——从程序到进程的蜕变
程序是菜谱,进程是炒菜
程序是静态的,安静地躺在磁盘里。
比如 QQ.exe,它就是一堆指令和数据,存储在 D:\Tencent\QQ\Bin\ 目录下。
你不双击它,它什么都不是。
当你双击 QQ.exe,操作系统把它从磁盘加载到内存,
分配一块地址空间,创建一个进程控制块(PCB),
然后让 CPU 开始执行它的指令。这一连串动作完成后,进程就诞生了。
所以:进程是程序的一次执行过程,是系统进行资源分配和调度的独立单位。
[IMAGE] 程序与进程的关系示意图:左侧为磁盘中的可执行文件(静态),右侧为内存中的进程实体(动态),用箭头标注"加载→创建进程"的过程。Alt text: 程序到进程的转换——从磁盘静态文件到内存动态实体的过程。
一个程序可以对应多个进程。比如你打开了三个 Chrome 窗口——表面上是三个窗口,
底层是三个不同的进程,各自有独立的 PCB 和数据段,
但它们共享同一份程序段(Chrome 的可执行代码)。
考研选择题喜欢在这里设陷阱:同一程序多次运行,
PCB 不同、数据段不同、程序段可以相同。
进程实体与进程的区别
这是一个考研中的精细考点。进程实体(也叫进程映像)包含三部分:
PCB、程序段、数据段。它是进程某一时刻的快照,是静态的。
进程则是进程实体的运行过程,是动态的。
两者的关系就像"一张照片"与"照片中的人物正在进行的活动"——照片是静态的,
活动是动态的。
🔍 考点速记:进程是动态的,进程实体是静态的。选择题里说"进程就是进程实体"——这是错的。
[CHART] 进程实体的三层结构示意图:顶部为 PCB(进程标识与控制信息),中部为程序段(可执行代码),底部为数据段(全局变量、堆、栈等运行时数据),三个模块组成一个完整的进程映像。
为什么需要"进程"这个抽象?
在早期的单道批处理系统中,内存里一次只放一个程序。
CPU 执行完了,换下一个。这很简单,但效率极低——当程序等待 I/O 时,CPU 就闲着。
操作系统工程师们面临一个问题:如何让多个程序"看起来同时"运行?
答案就是引入进程抽象。每个程序成为一个独立运行的实体,
操作系统在这些实体之间快速切换,让 CPU 始终有活干。
[PERSONAL EXPERIENCE] 我在第一次理解进程概念时犯过一个认知错误:
以为"进程 = 程序 + 运行状态"。这个理解漏掉了最关键的一环——资源分配的视角。
进程不只是"正在运行的程序",它是操作系统向一个运行实体分配 CPU 时间、
内存空间、文件句柄等资源的"账户"。
这个区分在后续学习线程时会更加重要——线程共享进程的"账户",但没有自己独立的资源"账户"。
进程的三块积木 —— PCB、程序段与数据段
每个进程在内存中的实体由三部分组成。
理解这三部分及其关系,是后续学习进程调度和内存管理的基础。
PCB(进程控制块)—— 进程的"身份证"
PCB 是进程存在的唯一标志。
操作系统通过 PCB 感知进程的存在,也通过 PCB 管理进程。
创建一个进程,本质上是分配一个 PCB 并填入信息;
撤销一个进程,本质上是回收其 PCB。
PCB 中记录的信息可以归纳为四类:
| 信息类别 | 具体内容 | 用途 |
|---|---|---|
| 进程标识 | PID(进程 ID)、UID(用户 ID)、父进程 ID | 区分不同进程 |
| 处理机状态 | 通用寄存器、PC(程序计数器)、PSW(程序状态字) | 进程切换时保存/恢复现场 |
| 进程调度 | 进程状态、优先级、调度队列指针、事件标识 | 供调度程序决策 |
| 资源清单 | 内存地址空间、打开文件列表、I/O 设备分配情况 | 资源管理 |
当一个进程被剥夺 CPU 时,操作系统会把它的寄存器值、PC 等"现场信息"
保存到它的 PCB 中。等它再次获得 CPU 时,操作系统从 PCB 恢复这些值,
进程就能从被打断的地方继续执行。这个过程叫上下文切换。
[UNIQUE INSIGHT] 很多教材讲到 PCB 时只罗列它的字段,但最关键的设计意图在于:
PCB 把"管理一个进程所需的全部元数据"封装到了一个数据结构中。
操作系统不需要翻遍内存去找进程的信息——所有它需要的都在 PCB 里。
这是一种"元数据驱动设计",Linux 内核的 task_struct 结构体就是 PCB 的经典实现。
如果你想直观地感受 PCB,可以在 Linux 终端执行:
# 查看当前 shell 进程的信息
cat /proc/$$/status | head -20
输出中 Pid、State、VmRSS 等字段就是内核从该进程的 task_struct 中读取并暴露给用户的。
[IMAGE] Linux /proc 文件系统中进程信息的目录结构截图,展示 /proc/[PID]/ 目录下的 status、maps、fd 等虚拟文件。Alt text: Linux proc 文件系统中进程 PCB 信息的可视化——每个目录对应一个进程,目录中的文件对应 PCB 的各个字段。
程序段 —— 要执行的指令
程序段存放进程要执行的代码。对于同一个程序(如 QQ.exe)的多个进程实例,
它们的程序段内容相同——都是同一份可执行代码。
所以操作系统可以允许多个进程共享程序段(只读映射),节省内存。
数据段 —— 运行时数据
数据段存放进程运行过程中产生和使用的数据,包括:
全局变量、局部变量(栈区)、动态分配的内存(堆区)。
即使是同一个程序的多个进程,它们的数据段也各不相同。
你在两个 QQ 窗口中输入的是不同的聊天内容,这些内容就存储在各进程独立的数据段中。
🔍 考研高频陷阱:“同一程序多次执行,程序段相同、数据段相同”——这是错误的。程序段可以相同(共享),但数据段各自独立。
一段代码,理解 PCB 与 fork
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main() {
pid_t pid;
int x = 100; // 数据段中的局部变量
pid = fork(); // 创建子进程,底层为新进程分配 PCB + 数据段副本
if (pid == 0) {
// 子进程:拥有独立的 PCB 和数据段
// 程序段与父进程相同,但 x 独立存储
x = 200;
printf("[子进程] PID=%d, x=%d (地址: %p)\n",
getpid(), x, (void*)&x);
} else if (pid > 0) {
// 父进程:x 不受子进程修改的影响
sleep(1); // 等待子进程先执行
printf("[父进程] PID=%d, 子PID=%d, x=%d (地址: %p)\n",
getpid(), pid, x, (void*)&x);
}
return 0;
}
运行这段代码,你会看到父进程和子进程的 x 分别为 100 和 200,
但它们的虚拟地址相同——这是因为操作系统为每个进程维护了独立的地址空间。
同一虚拟地址映射到不同的物理内存。
说明:
fork()创建子进程时,内核为新进程分配新的 PID、PCB,
并复制父进程的数据段(写时拷贝优化)。程序段由于只读,父子共享。
这段代码验证了"同一程序两次执行,PCB 和数据段不同,程序段相同"。
进程的五个基本特征是什么?
进程有五个基本特征。考试中常以选择题形式出现,判断哪个是"最基本的特征"。
特征一:动态性(最基本特征)
进程是程序的一次执行过程。它有创建、运行、消亡的生命周期。
动态性是进程最根本的特征。 程序是静态的,进程是动态的——这是两者最本质的区别。
特征二:并发性
多个进程可以在同一时间间隔内"同时"运行。
宏观上你看到微信和浏览器都在工作,微观上它们在 CPU 上交替执行。
并发性正是引入进程机制的目的。
特征三:独立性
每个进程拥有独立的 PCB、独立的地址空间。
在未引入线程时,进程既是资源分配的独立单位,也是调度的独立单位。
一个进程崩溃通常不会直接影响其他进程——这是独立性的价值。
特征四:异步性
各进程以不可预知的速度向前推进。
你无法精确预测进程 A 的下一条指令在哪个时刻执行完毕——因为它随时可能被中断。
操作系统需要保证无论进程以何种顺序执行,最终结果都正确。
这正是后续要学习的"进程同步"要解决的问题。
特征五:结构性
每个进程内部都有清晰的结构:PCB + 程序段 + 数据段。
这使操作系统能够以统一的方式管理所有进程。
🔍 考点速记:动态性是最基本的特征。并发性是引入进程的目的。
独立性是资源分配角度的特征。这三点常常混在一起考查。
进程的一生 —— 五状态模型与转换规则
进程从创建到消亡,经历若干状态。
状态的转换规则是每年 408 考研的必考点,必须做到看图就能口述每条箭头。
三种基本状态
| 状态 | 含义 | 关键判断标准 |
|---|---|---|
| 运行态(Running) | 进程正在 CPU 上执行 | 占有 CPU |
| 就绪态(Ready) | 万事俱备,只欠 CPU | 具备运行条件,但没有 CPU |
| 阻塞态(Blocked/Waiting) | 等待某个事件发生 | 即使有 CPU 也不能运行 |
单 CPU 系统下,同一时刻最多只有一个进程处于运行态。
多 CPU 系统下,同一时刻最多有N 个(N 为 CPU 核心数)进程处于运行态。
[CHART] 进程五状态转换图:用五个节点(创建态、就绪态、运行态、阻塞态、终止态)和六条有向边表示状态转换,每条边标注转换条件(如"被调度"、“时间片到”、“等待I/O”、"I/O完成"等)。使用 Mermaid 流程图或 SVG 示意图。Alt text: 进程五状态模型状态转换图——展示了五种状态之间所有可能的转换路径及触发条件。
五种状态的完整模型
考研中需要掌握的完整五状态模型:
创建态、就绪态、运行态、阻塞态、终止态。
创建态(New/Created):进程正在被创建。
操作系统正在为它分配 PCB、分配内存、初始化资源。
此时进程还不具备运行条件。
终止态(Terminated):进程已经结束运行。
操作系统正在回收它的资源、撤销它的 PCB。
状态转换——六条关键箭头
下面这张表完整覆盖所有可能的转换。每条都要记住方向和触发条件。
| 转换 | 触发条件 | 408考查频率 |
|---|---|---|
| 创建态 → 就绪态 | 系统完成进程创建工作,进程被插入就绪队列 | 中 |
| 就绪态 → 运行态 | 进程被调度程序选中,获得 CPU | 极高 |
| 运行态 → 就绪态 | 时间片用完,或被更高优先级的进程抢占 | 极高 |
| 运行态 → 阻塞态 | 进程请求系统资源(如 I/O)而得不到满足 | 极高 |
| 阻塞态 → 就绪态 | 等待的事件发生(I/O 完成、资源可用) | 极高 |
| 运行态 → 终止态 | 进程执行完毕或调用 exit() |
中 |
🔍 高频陷阱:没有"阻塞态 → 运行态"的直接转换!
阻塞态只能先转换为就绪态,再被调度为运行态。
这和"阻塞态 → 就绪态 → 运行态"两步走是很多同学的选择题失分点。
为什么没有"就绪态 → 阻塞态"?
[UNIQUE INSIGHT] 这个问题值得停下来想一想。就绪态意味着进程已经准备好运行,
它还没有获得 CPU,也就还没机会去请求 I/O。
所以"就绪态 → 阻塞态"这个转换在逻辑上不可能发生——
进程必须先在 CPU 上运行、发出系统调用(如 read()),才会因为等待数据而阻塞。
理解这条逻辑,你就不需要死记硬背状态转换表。
Linux 中的进程状态
Linux 内核在 task_struct 的 state 字段中记录了进程状态。
与理论模型的对应关系如下:
| Linux 状态常量 | 含义 | 对应理论状态 |
|---|---|---|
TASK_RUNNING |
正在运行或可运行 | 运行态 + 就绪态 |
TASK_INTERRUPTIBLE |
可中断睡眠 | 阻塞态(可被信号唤醒) |
TASK_UNINTERRUPTIBLE |
不可中断睡眠 | 阻塞态(不响应信号,常用于磁盘 I/O) |
TASK_STOPPED |
暂停执行 | 暂停状态 |
EXIT_ZOMBIE |
已结束但 PCB 未回收 | 终止态(僵尸进程) |
TASK_RUNNING 把运行态和就绪态合并了——Linux 的设计者认为,
在调度的视角里,运行和就绪都是"可被调度的状态"。
用 ps 命令看到 STAT 列显示 R、S、D、Z,对应的就是这些状态。
C 代码观察进程状态转换
下面这段代码创建子进程,展示从运行态到阻塞态再到就绪态的完整过程:
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程:运行态 → 阻塞态 → 就绪态 → 运行态 → 终止态
printf("[子进程 %d] 运行态: 正在执行\n", getpid());
// 主动 sleep,子进程进入阻塞态(等待定时器事件)
sleep(2);
// 定时器事件发生 → 子进程回到就绪态 → 被调度后恢复运行态
printf("[子进程 %d] 运行态: sleep 结束, 即将退出\n", getpid());
// exit → 进入终止态
return 0;
} else {
// 父进程:观察子进程状态
printf("[父进程 %d] 创建子进程 %d 后继续执行\n",
getpid(), pid);
printf("[父进程 %d] 子进程此时可能正处于阻塞态(sleep中)\n",
getpid());
// 父进程等待子进程退出
wait(NULL);
printf("[父进程 %d] 子进程已终止(终止态), 父进程回收其PCB\n",
getpid());
}
return 0;
}
这段代码追踪了一个进程完整的状态转换路径:
创建态(fork)→ 就绪态 → 运行态 → 阻塞态(sleep)→ 就绪态 → 运行态 → 终止态(exit)。
实际的状态切换由调度器控制,在 sleep() 调用期间由内核自动管理。
操作系统如何组织成百上千个进程?
当系统中有几百个进程时,操作系统需要高效地找到"哪些进程处于就绪态"
“哪些进程正在等待磁盘 I/O”。组织方式主要有两种。
链接方式
为每个状态维护一个队列。PCB 中包含一个指针字段,
把同状态的 PCB 串联成链表。就绪队列、阻塞队列各自成链。
就绪队列: [PCB_A] → [PCB_B] → [PCB_C] → NULL
阻塞队列: [PCB_D] → [PCB_E] → NULL
优点是简单直观,缺点是遍历时需要逐个访问,效率为 O(n)。
索引方式
为每个状态建立一个索引表(本质是一个 PCB 指针数组)。
操作系统通过索引表直接定位到任意 PCB,效率更高。
就绪索引表: [0]→PCB_A [1]→PCB_B [2]→PCB_C
阻塞索引表: [0]→PCB_D [1]→PCB_E
现代操作系统多数采用索引方式或其变体。Linux 使用红黑树组织就绪进程的调度实体,
复杂度为 O(log n),比两种基础方式都更高效。
原语如何控制进程?
进程的创建、终止、阻塞和唤醒不是普通的函数调用,
而是通过原语来实现的。
什么是原语?
原语是一段程序,其执行过程不允许被中断。
它通过"关中断"指令开始、通过"开中断"指令结束。
关中断期间,即使外部设备发来中断信号,CPU 也不响应。
为什么需要关中断?试想:进程创建过程中,PCB 的数据结构正处于"半成品"状态。
如果此时发生时钟中断、调度器开始运行并访问了这个半成品 PCB,
后果是数据损坏甚至系统崩溃。原语的原子性就是针对这种场景设计的。
四个核心原语
| 原语 | 操作 | 关键步骤 |
|---|---|---|
| 进程创建 | 创建 → 就绪 | 分配 PID→分配 PCB→初始化→插入就绪队列 |
| 进程终止 | 运行 → 终止 | 回收资源→撤销 PCB→通知父进程 |
| 进程阻塞 | 运行 → 阻塞 | 保存现场→修改 PCB 状态→移入阻塞队列→触发调度 |
| 进程唤醒 | 阻塞 → 就绪 | 修改 PCB 状态→从阻塞队列移出→插入就绪队列 |
🔍 考点:阻塞和唤醒必须成对出现。如果一个进程阻塞了自己,
必须有另一个进程(通常是 I/O 中断处理程序)来唤醒它。
考题经常问"进程自己能否从阻塞态变为就绪态"——不能,必须由外部事件唤醒。
需要注意的是,进程切换也是一项通过原语实现的操作。
进程切换原语将当前运行进程的 CPU 上下文保存到其 PCB 中,
然后从就绪队列中选择下一个进程,加载其上下文。
整个过程在关中断条件下完成,确保现场保存与恢复的完整性。
进程上下文切换的开销
进程切换是有代价的。每次切换需要:
- 保存当前进程的寄存器、PC、栈指针到 PCB
- 刷新 TLB(Translation Lookaside Buffer,页表缓存)
- 加载下一个进程的上下文
- 可能导致 CPU 缓存失效
在 Linux 中,一次进程切换大约消耗 1-5 微秒(视硬件和缓存命中率而定)。
这个数字看似很小,但对每秒处理几十万请求的服务器而言,累积开销不可忽视。
这也是为什么引入了更轻量的线程——线程切换只需要保存少量寄存器,
不涉及地址空间切换。
线程——比进程更轻量的执行单元
引入线程之前的问题
在没有线程的年代,一个进程只能串行执行。
如果你想写一个同时处理用户输入和网络数据的程序,
只能靠多进程——开销很大。进程间通信(IPC)复杂,切换成本高。
线程的引入解决了这个问题:一个进程内部可以有多个线程并发执行。
比如 Chrome 浏览器的渲染进程里,有负责解析 HTML 的线程、
有负责下载图片的线程、有负责执行 JavaScript 的线程。
它们共享同一个地址空间,协作效率远高于多进程。
线程的定义
线程是程序执行的最小单位,是 CPU 调度的基本单位。
引入线程后,进程的地位发生了变化:进程仍然是资源分配的基本单位,
但不再是 CPU 调度的基本单位——CPU 调度的是线程。
一句话记住二者的角色变化:
引入线程前:进程 = 资源分配单位 + 调度单位。
引入线程后:进程 = 资源分配单位(除 CPU 外),线程 = CPU 调度单位。
一个经典的类比
进程就像一栋图书馆,线程就像图书馆里的阅览座位。
一个图书馆(进程)里可以有多个座位(线程)。
每个座位都可以有读者(执行指令),读者之间共享图书馆的藏书(进程资源)。
换一个座位远比换一栋图书馆容易——这对应了线程切换远快于进程切换。
[IMAGE] 图书馆类比示意图:一栋图书馆建筑标注为"进程"(拥有藏书、设施等资源),内部多个阅览座位标注为"线程"(共享资源、独立执行),箭头表示切换座位的便捷性。Alt text: 进程与线程的图书馆类比——进程是拥有资源的图书馆,线程是共享资源但独立执行的阅览座位。
线程的属性
| 属性 | 说明 |
|---|---|
| 轻型实体 | 线程基本不拥有系统资源(仅需少量必需的:PC、寄存器、栈) |
| 独立调度的基本单位 | CPU 切换的是线程,不是进程 |
| 可并发执行 | 同一进程内的多个线程可以并发运行 |
| 共享进程资源 | 线程共享进程的地址空间、文件描述符、信号处理等 |
线程的实现方式 🔍
线程的实现方式分为两大类,这是选择题和高频考点。
用户级线程(ULT,User-Level Thread):线程管理在用户空间完成,内核感知不到线程的存在。
线程切换不需要内核态切换,速度快。但缺点致命——如果一个线程阻塞(比如等待磁盘读取),
内核会把整个进程挂起,导致同一进程中的其他线程也无法运行。
内核级线程(KLT,Kernel-Level Thread):线程由内核管理,每个线程对内核可见。
一个线程阻塞不会影响其他线程。但线程切换需要从用户态陷入内核态,开销较大。
多线程模型:
- 多对一模型:多个用户线程映射到一个内核线程。一个线程阻塞,全局阻塞。
- 一对一模型:每个用户线程对应一个内核线程。并发性好,但内核线程数量有限。
- 多对多模型:M 个用户线程映射到 N 个内核线程(M >= N)。兼顾并发性和系统开销,最灵活。
Linux 的 pthread 采用的是一对一模型(每个 pthread 对应一个内核任务)。
在 Linux 中,线程本质上是一种特殊的进程——共享地址空间的"轻量级进程(LWP)"。
所以 Linux 调度器实际调度的对象始终是 task_struct,
"进程"和"线程"的区别仅在于资源共享的程度。
C 代码演示多线程
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// 全局变量——线程间共享(属于进程的数据段)
int shared_counter = 0;
void* thread_work(void* arg) {
int id = *(int*)arg;
for (int i = 0; i < 5; i++) {
shared_counter++; // 多个线程修改同一变量
printf("[线程 %d] shared_counter = %d\n", id, shared_counter);
usleep(100000); // 0.1 秒,模拟工作负载
}
return NULL;
}
int main() {
pthread_t t1, t2;
int id1 = 1, id2 = 2;
// 创建两个线程——它们共享 shared_counter 和进程资源
pthread_create(&t1, NULL, thread_work, &id1);
pthread_create(&t2, NULL, thread_work, &id2);
// 等待线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("[主线程] 最终 shared_counter = %d\n", shared_counter);
// 注意:由于没有同步机制(如互斥锁),最终结果可能不是 10
// 这正是后续文章要讨论的"线程同步"问题
return 0;
}
编译命令:gcc -pthread -o thread_demo thread_demo.c
关键观察:两个线程同时修改
shared_counter时出现了竞态条件。
最终值可能不是 10——这与进程的"独立数据段"形成鲜明对比。
进程之间不会出现这种问题(各自数据隔离),但线程共享数据带来了协作效率,
也带来了同步的复杂性。这就是本系列第三篇文章《进程同步与互斥》要解决的核心问题。
进程 vs 线程 —— 一张表看清本质区别
| 维度 | 进程 | 线程 |
|---|---|---|
| 定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
| 拥有资源 | 独立的地址空间、文件描述符、PCB | 仅拥有栈、寄存器、PC 等少量上下文 |
| 地址空间 | 不同进程地址空间隔离 | 同一进程的线程共享地址空间 |
| 通信方式 | IPC(管道、消息队列、共享内存、信号) | 直接读写共享变量(需同步保护) |
| 切换开销 | 大(切换地址空间、保存/恢复全部上下文) | 小(无需切换地址空间,仅保存少量寄存器) |
| 创建开销 | 大(需分配 PCB、地址空间等全套资源) | 小(仅分配栈和 TCB) |
| 独立性 | 强(一个进程崩溃一般不波及其他进程) | 弱(一个线程崩溃可能导致整个进程终止) |
| 系统调用 | fork()、exec() |
pthread_create()、pthread_join() |
| 并发粒度 | 粗 | 细 |
[CHART] 进程切换 vs 线程切换开销对比柱状图(使用 SVG 或 Mermaid):横轴列出"上下文保存"、“地址空间切换”、“TLB刷新”、"总开销"四项,每项两根柱子(进程切换/线程切换),展示线程切换在各维度上的开销优势。Alt text: 进程切换与线程切换开销的定量对比——线程切换在各项指标上均显著低于进程切换。
FAQ
进程创建后,为什么不是直接进入运行态,而是先进入就绪态?
进程创建完成时,CPU 可能正被其他进程占用。操作系统不会立即抢占当前运行进程——而是把新进程放入就绪队列,等待调度器的下一次选择。所以"创建态 → 就绪态"是标准流程,跳级进运行态在设计上不合理,会破坏调度的公平性。
阻塞态和就绪态的核心区别是什么?
就绪态进程"只缺 CPU"——给它 CPU 就能执行;阻塞态进程"缺的不是 CPU"——它等待的是某个外部事件(如磁盘数据就绪、用户键盘输入)。即使 CPU 空闲,阻塞态进程也不能运行。这是判断状态的唯一标准。
进程和线程到底怎么记才不会混淆?
记住一句话:进程管资源,线程管执行。 进程是操作系统中"资源的容器",拥有地址空间和文件描述符。线程是"在容器中干活的人",共享容器里的资源,但每个线程有自己的栈。创建进程 = 建一个新容器,创建线程 = 在当前容器里多派个人。
原语为什么一定要关中断?用锁不行吗?
锁只能保护用户空间的临界区,原语的代码在内核空间执行,涉及全局数据结构(就绪队列、阻塞队列、PCB 链表)的一致性。如果在修改就绪队列中途被调度器中断,调度器会看到一个损坏的队列——系统随后崩溃。关中断是内核中实现原子性最直接也最可靠的手段。
僵尸进程是什么?对系统有什么影响?
僵尸进程是已经终止(exit),但其 PCB 尚未被父进程回收的进程。它在 Linux 中处于 EXIT_ZOMBIE 状态。僵尸进程不占用内存和 CPU,但占用 PID 资源。如果大量僵尸进程堆积,可能导致系统无法分配新 PID。解决方案是父进程及时调用 wait() 或 waitpid() 来回收。
总结与系列导航
进程是操作系统中最核心的抽象之一。
本文按照 408 考纲的顺序,从进程的概念、组成、特征,讲到状态转换、进程控制和线程引入。
理解这些基础概念后,后续三篇文章将逐层深入:
-
《进程调度 —— 从 FCFS 到多级反馈队列,调度算法全景解析》
覆盖调度层次、调度时机、经典调度算法、调度算法评价指标。 -
《进程同步与互斥 —— 信号量、管程与 PV 操作实战》
覆盖临界区问题、信号量机制、经典同步问题(生产者-消费者、读者-写者、哲学家就餐)。 -
《死锁 —— 四个必要条件、银行家算法与死锁检测》
覆盖死锁成因、预防与避免策略、银行家算法、死锁检测与恢复。
这四篇文章构成"进程与线程"的完整知识体系。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)