2.1 进程的基本概念

进程 (Process)
程序关于某个数据集合的一次执行过程,是系统进行资源分配和调度的基本单位。进程是动态的,有生命周期。

进程与程序的区别:

对比项程序进程
性质静态,存于磁盘的代码动态,运行于内存
资源不分配资源是资源分配的基本单位
对应关系一个程序可对应多个进程一个进程至少包含一个程序
生命周期永久存在有创建、调度、撤销

进程控制块 (PCB) 是进程存在的唯一标志,包含:

2.2 进程的状态与转换

进程的三种基本状态:

五种状态(加入创建和结束)的转换关系:

转换触发条件
就绪 → 运行调度程序选中该进程,分配 CPU
运行 → 就绪时间片用完 / 被高优先级进程抢占
运行 → 阻塞请求 I/O / 等待资源(主动行为)
阻塞 → 就绪I/O 完成 / 资源到达(被动唤醒)
重要:运行 → 阻塞是进程主动的(如 I/O 请求);阻塞 → 就绪是被动的(被中断处理程序唤醒)。阻塞态不能直接到运行态,就绪态不能直接到阻塞态。

2.3 进程控制

进程控制由 OS 内核的原语实现,保证原子性:

2.4 线程

线程 (Thread)
进程内的执行单元,是 CPU 调度的基本单位。线程自己不拥有系统资源,只拥有一点运行中必不可少的资源(寄存器、栈、PC),与其他线程共享所属进程的资源。
对比项进程线程
资源分配基本单位(拥有独立地址空间)不拥有资源,共享进程资源
调度单位传统 OS 中现代 OS 中 CPU 调度的基本单位
切换开销大(涉及地址空间切换)小(同一地址空间内)
通信需 IPC(管道、消息等)直接读写共享变量
并发性更高(线程间可并发)

线程分为用户级线程(由应用程序通过线程库管理,内核感知不到)和内核级线程(由内核管理调度)。多对一模型中,一个线程阻塞会导致整个进程阻塞。

2.5 进程调度

调度层次

调度算法评价指标

调度算法

算法类型特点缺点
FCFS 先来先服务非抢占公平,实现简单长作业有利,短作业不利(护航效应)
SJF 短作业优先非抢占/抢占平均等待时间最短(最优)长作业饥饿,需预知运行时间
RR 时间片轮转抢占响应快,适合分时系统时间片太小则切换开销大
优先级调度非抢占/抢占灵活,重要任务优先低优先级可能饥饿
多级反馈队列抢占兼顾各类作业,自适应实现复杂
注意:SJF 的平均等待时间在所有非抢占调度中最短。多级反馈队列中,新进程进入第一级队列,用完时间片后降级,最后一级采用时间片轮转;同一队列内按 FCFS。可通过"优先级提升"防止饥饿。

HRRN 高响应比优先调度

HRRN (Highest Response Ratio Next)
高响应比优先调度算法。每次选择响应比最高的进程分配 CPU。响应比公式:R = (等待时间 + 运行时间) / 运行时间 = 1 + 等待时间 / 运行时间

HRRN 是非抢占式调度算法,综合了 FCFS 和 SJF 的优点:

示例计算:设 P1 到达时间 0、服务时间 10;P2 到达时间 1、服务时间 2。在时刻 10(P1 刚完成时),P2 已等待 9 个单位时间,响应比 R₂ = (9+2)/2 = 5.5;若 P3 到达时间 2、服务时间 5,在时刻 10 时等待了 8,R₃ = (8+5)/5 = 2.6 → 选 P2。

HRRN 伪代码
// HRRN 高响应比优先
while(就绪队列非空){
  float maxR = -1;
  PCB best = null;
  for(each P in 就绪队列){
    float R = (P.waitTime + P.svcTime) / P.svcTime;
    if(R > maxR){ maxR = R; best = P; }
  }
  dispatch(best);
  while(best 未完成) best 运行;
}
注意:HRRN 既考虑了等待时间(防止饥饿),又考虑了运行时间(短作业优先),但每次调度需要计算所有就绪进程的响应比,开销较大。HRRN 不会产生饥饿问题。

调度算法伪代码

// FCFS 先来先服务
while(就绪队列非空){
  P = 就绪队列队首;
  dispatch(P);                     // 分配 CPU
  while(P 未完成) P 运行;
  if(P 完成) 撤销 P;
  else P 回到就绪队列队尾;
}

// SJF 短作业优先(非抢占)
while(就绪队列非空){
  // 选择服务时间最短的进程
  P = argmin(ready, p => p.svcTime);
  dispatch(P);
  while(P 未完成) P 运行;
}

// RR 时间片轮转
while(就绪队列非空 || 有进程运行){
  P = 就绪队列队首;
  dispatch(P, q);                // 最多运行 q 时间
  if(P 完成) 撤销 P;
  else 就绪队列.入队(P);
  if(新进程到达) 就绪队列.入队(新进程);
}

// 多级反馈队列 MLFQ
for 每个新进程 P: Q1.入队(P);
while(true){
  if(Q1 非空): RR_dispatch(Q1, q1);
  else if(Q2 非空): RR_dispatch(Q2, q2);
  else if(Q3 非空): FCFS_dispatch(Q3);
}
// 规则:用完时间片未完成 → 降级;完成 → 撤销;
//       在高优先级队列等待过久 → 优先级提升

2.6 同步与互斥

临界资源
一次仅允许一个进程使用的资源,如打印机、共享变量。各进程对临界资源的访问必须互斥进行。

临界区互斥的四条准则:

进程同步基本实现方法

实现进程互斥的基本方法主要有软件方法和硬件方法。其中Peterson 算法是最经典的软件实现方案。

Peterson 算法(两进程互斥)

Peterson 算法使用两个共享变量来实现两个进程的严格互斥:

bool flag[2] = {false, false};
int turn = 0;

// 进程 i(i=0 或 i=1)
void Pi(int i){
  int j = 1 - i;
  while(true){
    flag[i] = true;              // ① 表示想进入临界区
    turn = j;                     // ② 谦让对方,让对方先走
    while(flag[j] && turn == j);   // ③ 若对方想进且对方被谦让,则等待
    临界区;
    flag[i] = false;              // ④ 离开临界区
    其余区;
  }
}

Peterson 算法满足临界区的三条准则

软件方法的局限:Peterson 算法仅适用于两个进程,进程数增多时算法复杂度大幅增加。实际系统中更常用硬件方法(如关中断、Test-and-Set 指令、Swap/Exchange 指令)来实现互斥。现代 OS 使用信号量、锁等高级同步原语。

信号量机制与 PV 操作

信号量 (Semaphore)
由 P 操作(wait)和 V 操作(signal)控制的整型变量 S,用于实现进程同步与互斥。
P(S):S=S−1;若 S<0,进程阻塞并进入 S 的等待队列。
V(S):S=S+1;若 S≤0,唤醒等待队列中的一个进程。

信号量的应用:

经典问题:生产者-消费者

一组生产者向缓冲区放入产品,一组消费者从缓冲区取出产品。缓冲区大小为 N。需三个信号量:

// 信号量定义
semaphore mutex = 1;   // 互斥访问缓冲区
semaphore empty  = N;   // 空缓冲区数量(同步)
semaphore full   = 0;   // 满缓冲区数量(同步)

producer(){
  while(1){
    生产一个产品;
    P(empty);      // 申请空位,无空位则阻塞
    P(mutex);      // 互斥访问缓冲区
    放入产品;
    V(mutex);      // 释放缓冲区访问权
    V(full);       // 增加产品计数,唤醒消费者
  }
}
consumer(){
  while(1){
    P(full);       // 申请产品,无产品则阻塞
    P(mutex);      // 互斥访问缓冲区
    取出产品;
    V(mutex);      // 释放缓冲区访问权
    V(empty);      // 增加空位,唤醒生产者
    消费产品;
  }
}
关键:实现互斥的 P 操作必须在实现同步的 P 操作之后,否则可能死锁(如先 P(mutex) 再 P(empty),缓冲区满时生产者持 mutex 等 empty,消费者持 full 无法获 mutex)。V 操作顺序不影响正确性,但通常先 V(mutex)。

交互式动画 · 生产者-消费者问题

可视化物品在有界缓冲区中流动,观察 empty/full/mutex 三个信号量的变化。

经典同步问题

1. 读者-写者问题

一组读者进程只读共享数据,一组写者进程可修改。规则:① 允许多个读者同时读;② 写者必须互斥且独占;③ 读者与写者必须互斥。

需要三个信号量:rwmutex(写者互斥,初值 1)、rmutex(读者计数互斥,初值 1)、writeblock(写者阻塞读者,初值 1,解决"写饥饿")。

// 信号量
semaphore rwmutex = 1;    // 写者互斥
semaphore rmutex  = 1;    // 读者计数互斥
semaphore writeblock = 1; // 写饥饿解决
int readcount = 0;

reader(){
  while(1){
    P(writeblock);          // 无写者请求时才进入
    P(rmutex);              // 互斥更新 readcount
    readcount++;
    if(readcount==1) P(rwmutex);   // 第一个读者阻止写者
    V(rmutex);
    V(writeblock);
    读数据;
    P(rmutex);
    readcount--;
    if(readcount==0) V(rwmutex);   // 最后一个读者放行写者
    V(rmutex);
  }
}
writer(){
  while(1){
    P(writeblock);          // 请求写入,阻塞后续读者
    P(rwmutex);             // 互斥其他写者
    写数据;
    V(rwmutex);
    V(writeblock);
  }
}

交互式动画 · 读者-写者问题

按钮触发读者进入/离开、写者请求/完成,观察读者共享与写者互斥。

2. 哲学家就餐问题

5 位哲学家围坐一圈,每人需同时获得左右两支筷子才能就餐。用 5 个互斥信号量 chopstick[i] 表示每支筷子。

semaphore chopstick[5] = {1,1,1,1,1};

philosopher(int i){
  while(1){
    思考;
    P(chopstick[i]);              // 取左
    P(chopstick[(i+1)%5]);        // 取右
    进餐;
    V(chopstick[(i+1)%5]);
    V(chopstick[i]);
  }
}
避免死锁的几种策略:① 最多 4 人竞争(限流器);② 偶数号哲学家先取左、奇数号先取右(有序分配);③ 用一个互斥信号量确保同时取两支筷。

交互式动画 · 哲学家就餐问题

5位哲学家围坐圆桌,观察死锁形成与预防策略效果。切换策略对比差异。

锁与条件变量 [22新增]

锁 (Lock)
最基本的同步原语,提供两种原子操作:acquire()(获取锁)release()(释放锁)。同一时刻只有一个线程/进程能持有锁,其他尝试获取的线程会被阻塞,直到锁被释放。

自旋锁 vs 阻塞锁

对比项自旋锁 (Spin Lock)阻塞锁 (Blocking Lock / Mutex)
等待方式忙等待(循环检查,不放弃 CPU)让出 CPU,进入睡眠/阻塞状态
CPU 开销等待期间持续占用 CPU等待期间不占用 CPU
上下文切换有(进入等待和唤醒时)
适用场景临界区极短(几微秒),多核环境临界区较长,I/O 等待等
典型实现硬件 TAS/CAS 指令 + 忙循环操作系统内核提供的 mutex

条件变量 (Condition Variable)

条件变量
一种同步原语,允许线程在某个条件不满足时等待(wait),当条件满足时由另一个线程通知唤醒(signal/broadcast)。条件变量必须与锁配合使用

条件变量的基本操作:

// 条件变量典型用法(生产者-消费者)
lock mutex;
cond  notFull, notEmpty;

producer(){
  lock_acquire(mutex);
  while(缓冲区满) cond_wait(notFull, mutex);
  放入产品;
  cond_signal(notEmpty);
  lock_release(mutex);
}

consumer(){
  lock_acquire(mutex);
  while(缓冲区空) cond_wait(notEmpty, mutex);
  取出产品;
  cond_signal(notFull);
  lock_release(mutex);
}

与信号量的对比

对比项锁 + 条件变量信号量 (Semaphore)
同步粒度锁保护临界区;条件变量管理等待条件信号量本身兼具互斥和同步功能
状态记忆条件变量无记忆(signal 时若无等待者则丢失)信号量有记忆(V 操作使 S+1,后续 P 可直接通过)
使用约束必须与锁配合,wait 必须在持有锁时调用P/V 操作独立使用即可
可扩展性适合复杂同步场景,支持 broadcast适合简单的计数型同步
使用建议:简单互斥用信号量(初值=1 的互斥信号量);复杂条件等待用锁+条件变量(如生产者-消费者中需要 while 检查条件)。现代 OS 中用锁+条件变量构建的管程是主要的高级同步机制。

管程 (Monitor)

管程
一种编程语言级同步机制。管程由共享数据、操作过程、初始化语句组成,保证过程互斥执行。条件变量 (condition) 用于实现阻塞/唤醒。

管程的特征:① 模块化封装数据与操作;② 过程互斥调用;③ 通过条件变量 (wait/signal) 实现同步。典型应用:用管程实现 bounded buffer、读者-写者。

信号 (Signal) IPC [25新增]

信号 (Signal)
一种异步通知机制,用于向进程发送一个简短的通知,告知某个事件已经发生。信号是 Linux/Unix 系统中最古老的 IPC 方式之一,实现轻量级进程间通信。

常见信号类型

信号编号含义默认动作触发方式
SIGINT2终端中断(Ctrl+C)终止进程用户从键盘发送
SIGKILL9强制杀死进程终止进程(不可捕获/忽略)kill -9 PID
SIGTERM15终止进程(可捕获)终止进程kill PID(默认)
SIGCHLD17子进程状态改变(终止/暂停)忽略子进程结束时内核发送给父进程
SIGALRM14定时器超时(alarm() 到期)终止进程alarm() / setitimer()
SIGSEGV11段错误(非法访存)终止进程+core dump内核检测到内存访问违规

信号处理方式

进程收到信号后可选择三种处理方式之一:

发送信号

信号 vs 其他 IPC:信号是异步的通知机制(传递控制信息而非数据),适合简单事件通知(如子进程终止、超时、用户中断)。对于大量数据传输,应使用管道、消息队列、共享内存等 IPC 方式。信号是可中断的,信号处理函数执行期间可能被更高优先级信号打断。

进程通信 IPC

2.7 死锁

死锁 (Deadlock)
多个进程因竞争资源而造成的一种僵局,若无外力干预,这些进程都将无法继续推进。

死锁产生的四个必要条件

注意:四个条件必须同时满足才会死锁。破坏任意一个即可预防死锁。

死锁处理策略

银行家算法

是最著名的死锁避免算法。在进程申请资源时,先试探性分配,再执行安全性算法:能否找到一个安全序列,使所有进程都能顺利完成。若安全则真正分配,否则让进程等待。

数据结构
Available(可用资源向量)、Max(最大需求矩阵)、Allocation(已分配矩阵)、Need = Max − Allocation(还需矩阵)。安全序列:找到一个 Need≤Available 的进程,假设它完成并释放资源,Available += Allocation,标记完成,重复直至所有进程完成。

银行家算法伪代码

// 数据结构
int Available[m];     // 可用资源向量
int Max[n][m];        // 最大需求矩阵
int Allocation[n][m]; // 已分配矩阵
int Need[n][m];       // 需求矩阵 = Max - Allocation

bool SafetyCheck(){
  vector<int> Work = Available;
  bool Finish[n] = {false};
  for(int i=0; iint k = -1;
    for(int j=0; jif(!Finish[j] && Need[j] <= Work){ k=j; break; }
    if(k==-1) return false;   // 不安全
    Finish[k] = true;
    Work += Allocation[k];          // 释放资源
  }
  return true;   // 安全
}

bool Request(int i, int Request[m]){
  if(Request > Need[i]) return false;
  if(Request > Available) return false;
  // 试探分配
  Available -= Request;
  Allocation[i] += Request;
  Need[i] -= Request;
  if(SafetyCheck()) return true;
  else { // 回滚
    Available += Request;
    Allocation[i] -= Request;
    Need[i] += Request;
    return false;
  }
}

交互式动画 · 死锁检测(资源分配图化简)

通过资源分配图化简法判断死锁,步进观察化简过程。