大家好,这里是编程Cookbook。本文详细介绍操作系统中与进程和线程相关的核心知识点,包括它们的基本概念、区别与联系、调度策略、通信方式、同步机制、I/O 模型与锁、以及死锁问题的处理策略。
目录
- 概念与区别 
- 进程 
- 线程 
- 进程 VS 线程 
- 协程 
- 协程 VS 线程 
- 一个进程可创建的线程数量 
- 进程和线程的地址空间 
- 状态管理与调度 
- 进程状态 
- 调度算法 
- CFS 调度器(Linux) 
- 进程终止方式 
- 特殊进程 
- 僵尸进程(Zombie Process) 
- 孤儿进程(Orphan Process) 
- 切换与上下文 
- 进程的切换过程 
- 调度 vs 切换 
- 线程的切换过程 
- 上下文信息 
- 进程 vs 线程切换的区别 
- 通信(IPC)方式 
- 进程同步 
- 基本内容 
- 死锁 
- 概念 
- 死锁的四个必要条件(同时满足才可能发生死锁) 
- 死锁的处理策略 
进程与线程
概念与区别
进程
为了更好的描述和控制程序的并发,实现操作系统的并发性和共享性,引入了进程的概念,为每一个进程分配一个专门的数据结构:进程控制块PCB(常驻内存),保存运行期间进程的数据,PCB 是进程存在的唯一标志。
进程是操作系统中进行资源分配和调度的最小单位, 是正在运行的程序的实例。每个进程都有自己独立的地址空间、数据栈以及其他用于跟踪进程执行的辅助数据(如文件描述符、寄存器状态等)。
线程
线程是“轻量级进程”,它是基本的CPU执行单元,也是程序执行流的最小单元,由线程ID、程序寄存器、寄存器集合和堆栈组成(即 CPU 调度的基本单元)。线程控制块(Thread Control Block, TCB):保存运行期间线程的数据,TCB 是线程存在的唯一标志。
线程是 CPU 调度(程序执行)的最小单位,是进程中的一个执行流。一个进程可以包含多个线程,它们共享该进程的地址空间、文件描述符、堆等资源,但每个线程有独立的栈空间和寄存器状态。
进程 VS 线程
引入线程是为了提高程序的并发性能。相较于进程,线程更加轻量,切换开销更低,适合处理高并发、高频率的任务,如 Web 服务、爬虫、图像处理等。他们的区别如下:
进程和线程的关系
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
- 线程是进程内的一个执行单元,也是进程内的可调度实体。
线程与进程的区别
- 调度: 进程是资源分配和调度的一个独立单元;而线程是 CPU 调度的基本单元。 
- 并发性: 不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。线程并发执行过程中要进行同步和互斥,因为它们共享同一进程的所有资源。 
- 拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。并且线程共享整个进程的资源(寄存器、堆栈、上下文)。 
- 系统开销: 线程是轻量级的进程,它的创建和销毁所需要的时间比进程小很多,所以进程在切换和创建销毁时,耗费的资源较大,系统开销较大。 
- 健全性: 进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响;线程有自己的堆栈和局部变量,但没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮。 
- 创建和销毁: 进程的创建调用 fork 或者 vfork,而线程的创建调用 pthread_create;进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束。 
协程
协程是一种用户态的轻量级线程,是由用户控制的执行单元,拥有独立的执行上下文和栈。它的调度和上下文切换不依赖操作系统内核,而是由程序显式控制切换,可以在函数中间挂起和恢复执行,非常适合处理 I/O 密集型任务。
协程 VS 线程
协程的切换完全在用户态完成,性能远优于线程,非常适合大量 I/O 场景(如网络请求、数据库操作),可以极大地减少上下文切换开销。他们的主要区别如下:
- 协程本身不具备并行能力,但可以通过线程调度多个协程并发运行;
- 线程的调度是由内核完成,涉及从用户态到内核态的切换
一个进程可创建的线程数量
理论上,线程数量受限于操作系统和硬件资源(如内存)。Linux 中,线程本质上是轻量级进程,一个进程可以创建的线程数没有固定上限,但在 Linux 系统中,一般受限于:
进程和线程的地址空间
关于 进程和线程的地址空间,我们可以从操作系统的资源隔离和共享机制来理解它们的区别:
进程的地址空间
每个进程都有独立的地址空间,包括以下几部分:
|  |  | 
|---|
| 代码段(Text) |  | 
| 数据段(Data) |  | 
| BSS段 |  | 
| 堆(Heap) | 动态内存分配(如 malloc/new)使用的区域,向上增长 | 
| 栈(Stack) |  | 
| 共享库映射区 |  | 
| 内核空间映射区 | 操作系统为每个进程保留的内核映射区域(在某些架构中) | 
不同进程之间的地址空间互不影响,隔离性强,进程间通信(IPC) 需要特殊机制(管道、共享内存等)。
线程的地址空间
线程是进程中的“执行单元”,共享进程的地址空间,但每个线程有自己独立的一小块空间:
|  |  |  | 
|---|
| 代码段 |  |  | 
| 堆空间 |  |  | 
| 全局变量 |  |  | 
| 文件描述符表 |  |  | 
| 栈空间 | 不 | 每个线程都有自己的独立栈,存储私有局部变量、返回地址等 | 
| 线程局部存储(TLS) | 不 |  | 
总结对比表
每个进程拥有完整独立的地址空间,而线程是进程内的执行单元,共享进程的地址空间但拥有自己的栈和线程私有数据(TLS)。线程之间共享内存,因此通信成本低,但需注意同步与线程安全问题。
状态管理与调度
进程状态
操作系统中的进程通常具有以下几种状态:
- 就绪(Ready):进程已经获得除 CPU 之外的所有资源,等待 CPU 分配;
- 运行(Running):进程正在使用 CPU 执行;
- 阻塞(Blocked / Waiting):进程正在等待某个事件(如 I/O)完成,无法继续执行;
- 终止(Terminated / Exit):进程执行完毕或被中止;
- (有些系统还有:挂起状态,比如“就绪挂起”、“阻塞挂起”,用于内存调度)

进程在操作系统中会在不同状态之间转换,常见状态包括就绪、运行、阻塞三种,状态转移机制如下:
- 运行态 → 阻塞态:进程因等待 I/O 或锁等资源主动放弃 CPU(主动行为);
- 阻塞态 → 就绪态:等待的资源就绪,操作系统唤醒进程(被动行为);
- 运行态 → 就绪态:时间片用完或被调度器抢占,进程让出 CPU(被动行为);
- 就绪态 → 运行态:操作系统调度器分配 CPU 给该进程(被动行为)。
调度算法
调度分级
|  |  |  | 
|---|
| 作业调度 |  | 从外存中的后备作业中选择作业,分配资源并建立进程,使其获得竞争处理机的权利。 | 
| 内存调度 |  | 选择挂起状态的进程重新调入内存(即从外存加载到内存)。 | 
| 进程调度 |  |  | 
发生频率比较:高级调度 < 中级调度 < 低级调度
调度方式
常见调度算法
操作系统使用调度算法决定哪个进程/线程优先获得 CPU 时间。常见算法如下:
|  |  | 
|---|
| 先来先服务(FCFS) | 按照进程到达时间顺序执行,简单但可能导致部分进程“饥饿”。 | 
| 短作业优先(SJF) | 优先执行估计运行时间最短的作业,能显著降低平均等待时间,但可能导致长作业饥饿。 | 
| 高响应比优先(HRRF) | 响应比 = (等待时间 + 服务时间) / 服务时间,选择响应比最高的作业执行,兼顾等待时间与服务时间。 | 
| 时间片轮转(RR) | 每个进程分配一个时间片,时间用完即切换,提高系统响应性,适合分时系统。 | 
| 优先级调度 | 根据进程优先级高低进行调度,支持抢占/非抢占两种策略,可能出现低优先级进程饥饿问题。 | 
| 多级反馈队列(MLFQ) | 使用多个优先级就绪队列,进程可动态在队列间移动,结合了时间片轮转和优先级调度,适应性强。 | 
| 完全公平调度器(CFS) | Linux 的默认调度器,使用红黑树按“虚拟运行时间”调度进程,实现按权重“公平分配 CPU 时间”。 | 
CFS 调度器(Linux)
CFS(Completely Fair Scheduler)是 Linux 从 2.6.23 版本引入的默认进程调度器,核心目标是实现“尽可能公平”的调度:
- 每个进程维护一个“虚拟运行时间(vruntime)”,运行时间越多,优先级就越低;
- 所有就绪进程按 vruntime排序,使用红黑树(RBTree)存储,红黑树最左侧节点拥有最高调度优先级;
- 支持进程权重(如 nice 值),可实现公平但带权的调度。
CFS 支持平衡交互性与吞吐量,是目前 Linux 系统通用场景下的主力调度器。
进程终止方式
操作系统中,进程终止主要有以下几种方式:
- 正常终止(Normal exit):进程主动调用 exit()或返回主函数,正常结束;
- 错误终止(Error exit):进程由于运行错误(如段错误、非法操作)被终止;
- 被其他进程终止(Killed by another process):如通过 kill(pid, SIGTERM)信号强制结束;
- 被操作系统终止(Aborted by OS):如内存溢出、非法访问、资源限制等被系统强制终止。
特殊进程
僵尸进程(Zombie Process)
定义
当一个子进程执行完毕(终止)后,其退出信息(如退出码)还没被父进程回收(通过 wait() 或 waitpid()),该子进程就进入僵尸状态,成为 僵尸进程。 它在 进程表中保留一项记录,用于父进程获取信息。
带来的问题
僵尸进程已经终止,不会占用CPU和内存,但也会有其他的问题:
- 僵尸进程会占用系统的进程表项(PCB,用于保存退出状态),而进程表项数量是有限的;
- 如果大量僵尸进程堆积,可能会导致 系统无法创建新进程,引发系统资源耗尽;
解决方案
- 让父进程调用 wait()/waitpid()回收子进程资源;
- 如果父进程忘记回收,可以杀掉父进程,让子进程变成 孤儿进程,由 init(或systemd)接管后,init会负责回收;
- 使用信号机制(如 SIGCHLD)信号通知父进程子进程已退出,触发回收机制;
孤儿进程(Orphan Process)
定义
当一个子进程还在运行,而它的父进程已经退出,该子进程会成为孤儿进程。
操作系统会自动将其交给 PID 为 1 的 init/systemd 进程 进行接管和管理。
带来的问题
- 孤儿进程不会像僵尸进程那样占用系统资源,因为 - init/systemd会自动回收它们的状态;
 
- 但如果这些孤儿进程本身是无意义的或无限循环,可能会导致: 
解决方案
- 定期检查系统中 PPID=1的进程(ps -ef | awk '$3==1');
- 如果是异常程序导致的,应修复程序逻辑或添加守护策略;
$3 表示第三列,也就是 PPID(父进程 ID),当该值为 1 时,表示其父进程是 init(PID 为 1)。
切换与上下文
进程的切换过程
进程切换是指 CPU 从当前正在运行的进程切换到另一个进程的过程,导致运行环境发生实质性变化。
过程步骤解释如下:
- 保存处理机上下文 保存当前进程的 CPU 寄存器状态(包括 PC、SP、通用寄存器等)到该进程的 PCB 中。 
- 更新 PCB 信息 修改当前进程的状态(如从“运行”改为“就绪”或“阻塞”),并记录相关信息。 
- 调用调度算法(如 CFS、RR)从就绪队列选中下一个进程。
- 切换页表(Page Table Base Register)以映射新进程的虚拟内存空间。
- TLB(Translation Lookaside Buffer)失效,需要刷新。
调度 vs 切换
|  |  | 
|---|
| 调度(scheduling) | 是“决定哪个进程获得 CPU”的策略决策行为。调度器根据优先级、时间片等信息选择合适进程。 | 
| 切换(switching) | 是“将 CPU 交给被选中的进程”的过程,即保存和恢复上下文、切换地址空间等操作。 | 
| 关系 | 调度决定“谁来上”,切换决定“怎么上”;调度在前,切换在后。 | 
线程的切换过程
线程切换本质是上下文的切换,但比进程切换更轻量,不涉及内存空间的切换。它主要通过保存和恢复 TCB 中的寄存器等执行上下文实现。如果两个线程属于不同进程,还涉及进程的切换;但如果属于同一进程,则只切换线程上下文。
线程切换是指 CPU 从一个线程切换到另一个线程的过程,它涉及的主要是:
- 保存当前线程的执行上下文(如寄存器、程序计数器 PC、栈指针 SP)
上下文信息
线程切换涉及上下文保存,主要包括:
|  |  |  | 
|---|
| 程序计数器(PC) |  |  | 
| 栈指针(SP) |  |  | 
| 通用寄存器(如 RAX, RBX) |  |  | 
| 程序状态字/标志位 |  |  | 
| 浮点寄存器(FPU 状态) |  |  | 
| 线程私有变量 / TLS |  |  | 
| 地址空间(页表等) |  |  | 
进程 vs 线程切换的区别
进程/线程通信与同步
通信(IPC)方式
进程间通信方式
|  |  |  |  |  | 
|---|
|  |  | 半双工,父子进程常用。匿名管道只能用于有亲缘关系的进程。 |  |  | 
|  |  |  |  |  | 
|  |  |  |  |  | 
|  |  | 将一段物理内存映射到多个进程虚拟空间中。需结合信号量或锁保证同步。 |  |  | 
|  |  | 本质是同步机制,用于控制多个进程对共享资源的访问。 |  |  | 
|  |  | 操作系统向进程发送异步通知,如 SIGINT等;可自定义处理函数。 |  |  | 
|  |  | 支持本地进程间或网络通信,基于 TCP/UDP,可用于不同主机之间。 |  |  | 
|  |  | 将文件映射到多个进程地址空间中,通过读写文件实现通信。 |  |  | 
线程间通信方式
线程属于同一进程,天然共享内存空间,因此通信本质上是「对共享数据的读写」+「同步机制」的组合。
|  |  |  |  | 
|---|
|  | 最常见方式,直接访问全局变量或堆上的共享数据,需使用互斥锁、条件变量等控制并发访问。 |  |  | 
|  | 与互斥锁配合,支持线程在某个条件不满足时阻塞等待,满足后被唤醒。 |  |  | 
|  | 支持多线程并发资源控制与同步,POSIX 信号量在线程间也适用。 |  |  | 
|  | 多用于 Windows 编程,线程可等待事件触发。 |  |  | 
|  | 多线程通信框架中常用,如生产者线程写入,消费者线程读取(结合锁)。Python queue.Queue就是线程安全的。 |  |  | 
|  | 如 Go 的 channel、Rust 的 mpsc,或 C++ 的消息总线。 |  |  | 
总结对比
进程同步
临界资源
一次仅允许一个进程使用的资源。
同步遵循的准则
- 空闲让进:临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区。
- 忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
- 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区。
- 让权等待:当进程不能进入临界区时,应立即释放处理器,防止进程忙等待。
实现临界区互斥的基本方法
- 进程同步(Process Synchronization):指的是在多个进程共享资源(如临界资源)时,为了避免竞态条件(Race Condition),需要通过同步机制来协调进程的执行顺序。 
- 互斥(Mutual Exclusion):同步的一个重要目标,确保一次只能有一个进程进入临界区访问共享资源。 
|  |  | 
|---|
| 软件方法 | 单标志法、双标志法、皮特森算法(Peterson)、Dekker 等 | 
| 硬件方法 | 中断屏蔽、TestAndSet、Swap、Compare-And-Swap 等 | 
| 操作系统原语 | 信号量(Semaphore)、互斥锁(Mutex)、条件变量等 | 
| 高级同步机制 | 管程(Monitor)、消息传递(Message Passing)、信号机制等 | 
软件实现方法
|  |  |  | 
|---|
| 单标志法 | 设置一个公用整型变量 turn,表示允许进入临界区的进程编号。如:
 turn=0则允许P0进入。 | 只能交替使用临界资源,不遵循“空闲让进”,但能保证临界区互斥。 | 
| 双标志法先检查 | 每个进程访问临界资源前,先查看对方是否在访问(通过 flag[i])。
 flag[i] = TRUE表示进程Pi正在访问临界区。 | 不遵循“忙则等待”,容易发生两个进程同时进入临界区。 | 
| 双标志法后检查 | 先设置自己的 flag[i]=TRUE,再检测对方的flag[j]。若为TRUE则等待。 | 不遵循“空闲让进”和“有限等待”,可能导致活锁或饥饿。 | 
| 皮特森算法(Peterson's Algorithm) | 设置 flag[i]=TRUE后再设置turn=j,表示自己让权给对方。之后检测对方状态与 turn 标志来决定是否等待。
 | 解决互斥与有限等待问题,但不遵循“让权等待”,仍可能忙等。 | 
锁
基本内容
锁是并发控制机制,用于控制多个线程/进程对共享资源的访问,防止出现竞态条件、数据不一致等问题。
常见锁
- 互斥锁(Mutex):只能一个线程持有,用于互斥访问资源。
- 读写锁(RWLock):支持多个读线程并发访问,但写时独占。
- 自旋锁(Spinlock):忙等待,不释放 CPU,适合锁时间短的场景。它是一种非阻塞的互斥锁,当一个线程尝试获取锁时,如果锁已被其他线程持有,它不会挂起(阻塞)当前线程,而是会在原地不断循环(“自旋”)检查锁是否被释放。
锁的优缺点
好处
|  |  | 
|---|
| 保证数据一致性 | 多线程访问共享资源时,锁可以防止数据竞争,确保只允许一个线程修改数据。 | 
| 线程同步机制 | 锁提供一种通用、直观的方式来管理线程间的同步逻辑。 | 
| 编程简单 | 相比无锁编程,使用锁结构清晰、容易理解,便于开发和维护。 | 
| 多种类型适配不同场景 | 如互斥锁(mutex)、读写锁、自旋锁、递归锁等可根据性能/粒度选用。 | 
坏处
|  |  | 
|---|
| 容易造成死锁 | 多线程竞争多个锁,可能形成互相等待,导致系统僵死。 | 
| 线程阻塞降低性能 | 阻塞等待会导致线程上下文切换开销增大,浪费 CPU 时间。 | 
| 可能出现优先级反转 | 高优先级线程被低优先级线程持有锁而阻塞,影响响应性。 | 
| 降低并发性 | 锁粒度过大会使多个线程被串行化,不能充分发挥多核性能。 | 
| 编程复杂 | 需要开发者精细设计锁的粒度、范围和顺序,容易出错。 | 
悲观锁 vs 乐观锁
死锁
概念
死锁(Deadlock) 是指多个进程/线程因争夺资源而造成的一种互相等待的现象,即每个进程都在等待其他进程释放它自己需要的资源,从而导致所有进程都无法继续执行。
导致进程/线程出现错误或者异常的场景除了死锁还有:
- 饥饿:由长期得不到想要的资源,某进程无法向前推进的现象。
- 死循环:某进程执行过程中一直跳不出某个循环的现象。
相同点
- 都是进程无法顺利向前推进的现象(故意设计的死循环除外)。
区别
- 死锁和饥饿是管理者(操作系统)的问题,死循环是被管理者(程序逻辑错误,是程序员)的问题
死锁的四个必要条件(同时满足才可能发生死锁)
|  |  | 
|---|
| 互斥 |  | 
| 不剥夺 |  | 
| 请求并保持 | 进程已获得部分资源,又请求其他资源但不释放已有资源。 | 
| 循环等待 | 存在进程组成的资源等待环,每个进程等待下一个进程所持有的资源。 | 
四个条件必须同时成立才会产生死锁,破坏其中任一即可避免死锁。
死锁的处理策略
|  |  |  |  | 
|---|
| 预防 | 破坏死锁产生的条件之一 |  |  | 
| 避免 | 动态检测资源分配是否会进入不安全状态 | 银行家算法(Banker's Algorithm) |  | 
| 检测与解除 | 允许死锁发生 |  |  | 
预防死锁
通过破坏死锁的四个必要条件之一或多个来从根本上避免死锁的发生。
- 尽量使用可共享资源(如只读文件、缓存等),多个进程可同时访问。
- 但实际很多资源本身无法共享(如写文件、打印机),因此此方法适用范围有限。
- 如果一个进程请求新资源失败,则主动释放已占资源并等待重试。
- 系统需支持资源状态保存与恢复,适用于可中断型资源(如内存页、CPU 时间片)。
- 进程在启动时一次性申请所需全部资源,若不能满足则等待所有资源就绪再执行。
- 降低灵活性,但能有效防止因边申请边持有造成的资源环。
- 为所有资源统一编号,进程按照编号升序申请资源,禁止逆序申请。
- 常用于多线程加锁场景(锁 A → 锁 B,避免锁 B → 锁 A 形成死锁链)。
避免死锁
- 进程在申请新资源前必须一次性申请完,或先释放已有资源后再申请。
- 模拟每次资源分配,若发现分配后系统可能进入死锁状态,则拒绝当前请求。
解除死锁
- 终止部分或全部死锁相关进程,释放资源,打破循环等待。
- 强制从部分进程中回收资源,使其他进程得以继续执行。
- 将某些进程恢复到以前的某个安全状态,重新执行避免死锁。
阅读原文:https://mp.weixin.qq.com/s/jPXJwAoAt2lUx3IcItecOg
该文章在 2025/6/19 17:17:24 编辑过