进程的概念、组成、特征

  • 概念:进程是进程实体的运行过程,是系统进行==资源分配==和==调度==的一个独立单位

调度(Scheduling) 是操作系统管理计算机资源(如CPU、内存、I/O设备)的核心机制

  • 组成
    • PCB(进程控制块):但凡操作系统管理时所需要的信息,都会被放在PCB中。==是进程存在的唯一标志==
      • 进程描述信息
        • 进程标识符PID:==唯一的、不重复的==
        • 用户标识符UID
      • 进程控制和管理信息
        • CPU、磁盘、网络流量使用情况统计…
        • 进程当前状态:就绪态/阻塞态/运行态…
      • 资源分配清单
        • 正在使用哪些文件
        • 正在使用哪些内存区域
        • 正在使用哪些I/O设备
      • 处理机相关信息
        • 如PSW、PC等等各种寄存器的值(用于实现进程切换)
    • 程序段:程序的代码(指令序列)
    • 数据段:运行过程中产生的各种数据(如:程序中定义的变量)
  • 特征
    • 动态性
      • 进程是程序的一次执行过程,是动态地产生、变化和消亡的
      • 进程的==最基本特性==
    • 并发性
      • 内存中有很多进程实体,各进程可并发执行
    • 独立性
      • 进程是能独立运行、独立获得资源、独立接受调度的基本单位
    • 异步性
      • 各进程以不可预知的速度向前推进,可能导致运行结果的不确定性
      • 操作系统要提供“进程同步机制”来解决异步问题
    • 结构性
      • 每个进程都会配置一个PCB。从结构上看,进程是由PCB、程序段、数据段组成

进程的状态与转换

  • 状态

    • 运行状态:CPU✅其他所需资源✅
    • 就绪状态:CPU❌其他所需资源✅
    • 阻塞状态:CPU❌其他所需资源❌
    • 创建状态:操作系统为新进程分配资源、创建PCB
    • 终止状态:操作系统回收进程的资源、撤销PCB
  • 进程状态间的转换

    • 就绪态$\Longrightarrow$运行态:进程被调度

    • 运行态$\Longrightarrow$就绪态:时间片到,或CPU被其他高优先级的进程抢占

    • 运行态$\Longrightarrow$阻塞态:等待系统资源分配,或等待某事件发生(主动行为)

    • 阻塞态$\Longrightarrow$就绪态:资源分配到位,等待的时间发生(被动行为)

    • 创建态$\Longrightarrow$就绪态:系统完成创建进程相关的工作

    • 运行态$\Longrightarrow$终止态:进程运行结束,或运行过程中遇到不可修复的错误

进程控制

  • 基本概念

    • 进程控制就是要实现进程状态的转换

    进程状态转换

    • 进程控制用原语实现
      • 原语用==关/开中断==来实现
      • 原语是一种特殊的程序
      • 原语的执行必须==一气呵成、不可中断==
  • 相关原语

    • 进程的创建
      • 创建原语
        • 申请空白PCB
        • 为新进程分配所需资源(内存,空间)
        • 初始化PCB
        • 将PCB插入就绪队列
      • 引起进程创建的事件
        • 用户登录:分时系统中,用户登录成功,系统会为其建立一个新的进程
        • 作业调度:多道批处理系统中,有新的作业放入内存时,会为其建立一个新的进程
        • 提供服务:用户向操作系统提出某些请求时,会新建一个进程处理该请求
        • 应用请求:由用户进程主动请求创建一个子进程
    • 进程的终止
      • 撤销原语(就绪态/阻塞态/运行态$\Longrightarrow$ 终止态$\Longrightarrow$ 无)
        • 从PCB集合中找到终止进程PCB
        • 若进程正在运行,立即剥夺CPU,将CPU分配给其他进程
        • 终止其所有子进程
        • 将该进程拥有的所有资源归还给父进程或操作系统
        • 删除PCB
      • 引起进程终止的事件
        • 正常结束
        • 异常结束
        • 外界干预
    • 进程的阻塞
      • 阻塞原语(运行态$\Longrightarrow$ 阻塞态)
        • 找到要阻塞的进程对应的PCB
        • ==保护进程运行现场==,将PCB状态信息设置为“阻塞态”,暂时停止进程运行
        • 将PCB插入相应事件的等待队列
      • 引起进程阻塞的事件
        • 需要等待系统分配某种资源
        • 需要等待相互合作的其他进程完成工作
    • 进程的唤醒
      • 唤醒原语(阻塞态$\Longrightarrow$ 就绪态)
        • 在事件等待队列中找到PCB
        • 将PCB从等待队列移除,设置为就绪态
        • 将PCB插入就绪队列,等待被调度
      • 引起进程唤醒的事件
        • 等待的事件发生
    • 进程的切换
      • 切换原语(运行态$\Longrightarrow$ 就绪态, 就绪态$\Longrightarrow$ 运行态)
        • 将==运行环境信息==存入PCB
        • PCB移入相应队列
        • 选择另一个进程执行,并更新其PCB
        • 根据PCB恢复==新进程所需的运行环境==
      • 引起进程切换的事件
        • 当前进程时间片到
        • 有更高优先级的进程到达
        • 当前进程主动阻塞
        • 当前进程终止

进程通信

  • 共享存储
    • 设置一个共享内存区域,并映射到进程的虚拟地址空间
    • 要==互斥地访问==共享空间(由通信进程自己负责实现互斥)
    • 两种方式
      • 基于数据结构(低级):如共享空间里只能放一个长度为10的数组。是一种==低级通信==方式
      • 基于存储区的共享(高级):操作系统在内存中划出一块共享存储区,数据的形式、存放位置都由通信进程控制,而不是操作系统。这种共享方式速度很快,是一种==高级通信==方式
  • 消息传递
    • 进程间的数据交换以==格式化的消息==为单位
    • 进程通过操作系统提供的“发送消息/接收消息”两个==原语==进行数据交换
    • 两种方式
      • 直接通信方式:消息发送进程要指明接收进程的ID
      • 间接通信方式:通过“信箱”间接地通信。因此又称“==信箱通信方式==”
  • 管道通信
    • 只能采用==半双工通信==
    • 如果要实现==双向同时通信==,则需要设置==两个管道==
    • 各进程要==互斥==地访问管道
    • 当==管道写满==时,==写进程==将==阻塞==,直到读进程将管道中的数据取走,即可唤醒写进程
    • 当==管道读空==时,==读进程==将==阻塞==,直到写进程往管道中写入数据,即可环境读进程
    • 管道中的数据一旦被读出,就彻底消失
    • 一个管道允许多个写进程,一个读进程

线程概念,多线程模型

  • 线程的概念
    • 线程是一个==基本的CPU执行单位==,也是==程序执行流的最小单位==
    • 引入线程之后,进程内的==各线程之间==也可以并发,从而进一步==提升了系统的并发度==,使得一个进程内也可以并发处理各种任务
    • 引入线程之后,==进程==只作为==除CPU之外的系统资源的分配单元==
    • 线程则作为==处理机的分配单元==
  • 引入线程机制后,带来的变化
    • 资源分配、调度
      • 传统进程机制中,进程是资源分配、调度的基本单位
      • 引入线程后,进程是资源分配的基本单位,线程是==调度的基本单位==
    • 并发性
      • 传统进程机制中,只能进程间并发
      • 引入线程后,各线程间也能并发,提升了并发度
    • 系统开销
      • 传统的进程间并发,需要切换进程的运行环境,系统开销很大
      • 线程间并发,如果是同一进程内的线程切换,则不需要切换进程环境,系统开销小
      • 引入线程后,并发所带来的系统开销减小
  • 线程的属性
    • 线程是处理机调度的单位
    • 多CPU计算机中,各个线程可占用不同的CPU
    • 每个线程都有一个线程ID、线程控制块TCB
    • 线程也有就绪、阻塞、运行三种基本状态
    • 线程几乎不拥有系统资源
    • 同一进程的不同线程间共享进程的资源
    • 由于共享内存地址空间,同一进程间的线程通信甚至无需系统干预
    • 同一进程中的线程切换,不会引起进程切换
    • 不同进程中的线程切换,会引起进程切换
    • 切换同进程内的线程,系统开销很小
    • 切换进程,系统开销较大

线程的实现方式,多线程模型

  • 线程的实现方式

    • 用户级线程
      • 从用户视角能看到的线程,由线程库实现
      • 优点:线程管理的系统开销小,效率高
      • 缺点:并发度不高。多个线程不可在多核处理机上并行运行

    用户级线程

    • 内核级线程
      • 从操作系统视角看到的线程,由操作系统实现。内核级线程才是处理机分配的单位
      • 优点:并发能力强。多线程可在多核处理机上并行执行
      • 缺点:线程管理的成本高,开销大

    内核级线程

    • 组合方式:上述两种方式的结合
  • 多线程模型

    • 一对一模型

      一对一模型

      • 一个用户级线程映射到一个内核级线程
      • 优点:各个线程可分配到多核处理机并行执行,并发度高
      • 缺点:线程管理都需要操作系统支持,开销大
    • 多对一模型

      多对一模型

      • 多个用户级线程映射到一个内核级线程
      • 优点:线程管理开销小,效率高
      • 缺点:一个线程阻塞会导致整个进程都被阻塞(并发度低)
    • 多对多模型

      多对多模型

      • n个用户级线程映射到m个内核级线程($n\ge m$)
      • 集二者之所长

    注意:操作系统只“看得见”内核级线程,因此==只有内核级线程才是处理机分配的单位==

调度的概念、层次

  • 基本概念
    • 按某种算法选择一个进程将处理机分配给它
  • 三个层次
    • 高级调度(作业调度)
      • 按照某种规则,从后备队列中选择合适的作业将其调入内存,并为其创建进程
      • 简单理解:好几个程序需要启动,到底先启动哪个
      • 每个作业只调入一次,调出一次
      • 作业调入时会创建PCB,调出时才撤销PCB
    • 中级调度(内存调度)
      • 按照某种规则,从挂起队列中选择合适的进程将其数据调回内存
      • 暂时调度外存等待的进程状态为==挂起状态==,被挂起的进程PCB会被组织成==挂起队列==
    • 低级调度(进程调度/处理机调度)
      • 按照某种规则,从就绪队列中选择一个进程为其分配处理机
      • 操作系统中==最基本的一种调度==
  • 三层调度的联系、对比
    • 高级调度
      • 外存$\Longrightarrow$内存(面向作业)
      • 发生频率:最低
    • 中级调度
      • 外存$\Longrightarrow$内存(面向进程)
      • 发生频率:中等
    • 低级调度
      • 内存$\Longrightarrow$CPU
      • 发生频率:最高
  • 补充知识
    • 为减轻系统负载,提高资源利用率,暂时不执行的进程会被调到外存从而变为“挂起态”
    • 七状态模型:在五状态的基础上加入了“就绪挂起”和“阻塞挂起”两种状态

七状态模型

进程调度的时机,切换与过程,调度方式

  • 时机
    • 什么时候需要进程调度
      • 主动放弃
        • 进程正常终止
        • 运行过程中发生异常而终止
        • 主动阻塞(如 等待I/O)
      • 被动放弃
        • 分给进程的时间片用完
        • 有更紧急的事情需要处理(如 I/O中断)
        • 有更高优先级的进程进入就绪队列
    • 什么时候不能进行进程调度
      • 在处理中断的过程中
      • 进程在操作系统内核程序临界区中
      • 原子操作过程中(原语)
    • 临界资源:一个时间段内只允许一个进程使用的资源。各进程需要==互斥地==访问临界资源
    • 临界区:访问临界资源的那段代码
  • 切换与过程
    • 狭义的“调度”和“切换”的区别
      • 狭义的进程调度指的是从就绪队列中==选中一个要运行的程序==(这个进程可以是刚刚被暂停执行的进程,也可能是==另一个进程==,后一种情况就需要==进程切换==)
      • 进程切换是指一个进程让出处理机,由另一个进程占用处理机的过程
      • 广义的进程调度包含了选择一个进程和进程切换两个步骤
    • 切换过程
      • 对原来运行进程各种数据的保存
      • 对新的进程各种数据的恢复
      • 重要结论:进程调度、切换是有代价的,并不是调度越频繁,并发度就越高
  • 方式
    • 非剥夺调度方式(非抢占式)
      • 只能由当前运行的进程主动放弃CPU
    • 剥夺调度方式(抢占式)
      • 可由操作系统剥夺当前进程的CPU使用权

调度器、闲逛进程

  • 什么事件会触发调度程序
    • 创建新进程
    • 进程退出
    • 运行进程堵塞
    • I/O中断发生
    • 非抢占式调度策略:只有运行进程阻塞或退出才触发调度程序工作
    • 抢占式调度策略:每个时钟中断或k个时钟中断会触发调度程序工作
  • 闲逛进程:没有其他就绪进程时,运行闲逛进程(idle)

调度的目标

  • CPU利用率

    • $利用率=\frac{\text{忙碌的时间}}{\text{总时间}}$
  • 系统吞吐量

    • $系统吞吐量 = \frac{\text{总共完成了多少道作业}}{\text{总共花了多少时间}}$
  • 周转时间

    • 周转时间 = 作业完成时间-作业提交时间
    • $平均周转时间=\frac{\text{各作业周转时间之和}}{\text{作业数}}$
    • $带权周转时间=\frac{\text{作业周转时间}}{\text{作业实际运行时间}}$
    • $平均带权周转时间=\frac{\text{各作业带权周转之和}}{\text{作业数}}$
  • 等待时间

    • 进程/作业等待被服务的时间之和
    • 平均等待时间即各个进程/作业等待时间的平均值
  • 响应时间

    • 从用户提交请求到首次产生响应所用的时间

调度算法

算法 可抢占? 优点 缺点 考虑到等待时间&运行时间? 会导致饥饿?
先来先服务FCFS 非抢占式 公平;实现简单 对短作业不利 等待时间✅运行时间❌ 不会
短作业优先SJF/SPF 默认为非抢占式,也有抢占式版本SRTN “最短的”平均等待/周转时间 对长作业不利,可能导致饥饿;难以做到真正的短作业优先 等待时间❌运行时间✅
高响应比优先HRRN 非抢占式 上述两种算法的权衡折中,综合考虑的等待时间和运行时间 等待时间✅运行时间✅ 不会
时间片轮转 抢占式 公平,适用于分时系统 频繁切换有开销,不区分优先级 不会
优先级调度 有抢占式,也有非抢占式 区分优先级,适用于实时系统 可能导致饥饿
多级反馈队列 抢占式 平衡优秀 可能导致饥饿

$响应比=\frac{\text{等待时间+要求服务时间}}{\text{要求服务时间}}$

同步与互斥的基本概念

  • 进程同步
    • 并发性带来了异步性,有时需要通过进程同步解决这种异步问题
    • 有的进程之间需要相互配合地完成工作,各进程的工作推进需要遵循一定的先后顺序
  • 进程互斥
    • 对临界资源的访问,需要互斥的进行,即同一时间段内只能允许一个进程访问资源
    • 四个部分
      • 进入区
        • 检查是否可进入临界区,若可进入,需要“上锁”
      • 临界区
        • 访问临界资源的那段代码
      • 退出区
        • 负责“解锁”
      • 剩余区
        • 其余代码部分
    • 需要遵循的原则
      • 空闲让进
        • 临界区空闲时,应允许一个进程访问
      • 忙则等待
        • 临界区正在被访问时,其他试图访问的进程需要等待
      • 有限等待
        • 要在有限时间内进入临界区,保证不会饥饿
      • 让权等待
        • 进不了临界区的进程,要释放处理机,防止忙等

进程互斥的软件实现方法

  • 单标志法

    • 在进入区只做“检查”,不“上锁”
    • 在退出区把临界区的使用权转交给另一个进程
    • 主要问题:不遵循“空闲让进”原则
  • 双标志先检查

    • 在进入区先“检查”后“上锁”,退出区“解锁”
    • 主要问题:不遵循“忙则等待”原则
  • 双标志后检查

    • 在进入区先“加锁”后“检查”,退出区“解锁”
    • 主要问题:不遵循“空闲让进,有限等待”原则,可能导致“饥饿”
  • Peterson算法

    • 在进入去“主动争取-主动谦让-检查对方是否想进、己方是否谦让”
    • 主要问题:不遵循“让权等待”原则,会发生“忙等”

进程互斥的硬件实现方法

  • 中断屏蔽方法
    • 优点:简单高效
    • 缺点
      • 只适用于单处理机
      • 只适用于操作系统内核程序
  • TestAndSet(TS指令/TSL指令)
    • 是用硬件实现的,执行的过程不允许被中断,只能一气呵成
    • 优点
      • 实现简单
      • 适用于多处理机环境
    • 缺点:不满足“让权等待”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bool变量lock表示当前临界区是否被加锁
// true表示已加锁,false表示未加锁
bool TestAndSet (bool *lock) {
bool old;
old = *lock;
*lock = true;
return old;
}

// TSL实现互斥
while (TestAndSet (&lock)); // "上锁"并"检查"
// 临界区代码段
critical_area();
lock = false;
// 剩余区代码段
redundancy_zone()
  • Swap指令(XCHG指令)
    • 是用硬件实现的,执行的过程不允许被中断,只能一气呵成
    • 逻辑上和TS指令类似
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Swap指令的作用是交换两个变量的值
Swap (bool *a, bool *b) {
bool temp;
temp = *a;
*a = * b;
*b = temp;
}

// 用Swap实现互斥的算法逻辑
// lock表示当前临界区是否被加锁
bool old = true;
while (old == true) Swap (&lock, &old);
// 临界区代码段
critical_area();
lock = false;
// 剩余区代码段
redundancy_zone()

信号量机制

  • 整型信号量
    • 用一个整数型变量作为信号量,数值表示某种资源数
    • 整型信号量与普通整型变量的区别:对信号量只能执行初始化PV三种操作
    • 整型信号量存在的问题:不满足让权等待原则
  • 记录型信号量
    • S.value表示某种资源数,S.L指向等待该资源的队列
    • P操作中,一定是先S.value--,之后可能需要执行block原语
    • V操作中,一定是先S.value++,之后可能需要执行wakeup原语
    • 可以用记录型信号量实现系统资源的“申请”和“释放”
    • 可以用记录型信号量实现进程互斥、进程同步

用信号量机制实现进程互斥、同步、前驱关系

  • 实现进程互斥
    • 分析问题,确定临界区
    • 设置互斥信号量,初值为1
    • 临界区之前对信号量执行P操作
    • 临界区之后对信号量执行V操作
  • 实现进程同步
    • 分析问题,找出哪里需要实现“一前一后”的同步关系
    • 设置同步信号量,初始值为0
    • 在“前操作”之后执行V操作
    • 在“后操作”之前执行P操作
  • 实现进程的前驱关系
    • 分析问题,画出前驱图,把每一对前驱关系都看成一个同步问题
    • 为每一对前驱关系设置同步信号量,初值为0
    • 在每个“前操作”之后执行V操作
    • 在每个“后操作”之前执行P操作

生产者、消费者问题

问题描述

系统中由一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用

只有缓冲区没满时,生产者才能把产品放入缓冲区,否则必须等待

只有缓冲区不空时,消费者才能从中取出产品,否则必须等待

缓冲区是临界资源,各进程必须互斥地访问

问题分析

  1. 设置信号量
1
2
3
semaphore mutex = 1; // 互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; // 同步信号量,表示空闲缓冲区的数量
semaphore full = 0; // 同步信号量,表示产品的数量,也即非空缓冲区的数量
  1. 生产者
1
2
3
4
5
6
7
8
9
10
producer () {
while(1) {
生成一个产品;
P (empty);
P (mutex);
把产品放入缓冲区;
V (mutex);
V (full);
}
}
  1. 消费者
1
2
3
4
5
6
7
8
9
10
consumer () {
while (1) {
P (full);
P (mutex);
从缓冲区取出一个产品;
V (mutex);
V (empty);
使用产品;
}
}

注意

  • 实现互斥的P操作一定要在实现同步的P操作之后

  • 两个V操作顺序可以交换

多生产者-多消费者

问题描述

桌子上有一只盘子,每次只能向其中放入一个水果。爸爸专向盘子中放苹果,妈妈专向盘子中放橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子空时,爸爸或妈妈才可向盘子中放一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。用PV操作实现上述过程

多生产者-多消费者问题描述

问题分析

  • 互斥关系:(mutex = 1)
    • 对缓冲区(盘子)的访问要互斥地进行
  • 同步关系(一前一后)
    • 父亲将苹果放入盘子后,女儿才能取苹果
    • 母亲将橘子放入盘子后,儿子才能取橘子
    • 只有盘子为空时,父亲或母亲才能放入水果

问题分析

  1. 信号量
1
2
3
4
semaphore metux = 1; // 实现互斥访问盘子
semaphore apple = 0; // 盘子中有几个苹果
semaphore orange = 0; // 盘子中有几个橘子
semaphore plate = 1; // 盘子中还可以放多少个水果
  1. 父亲
1
2
3
4
5
6
7
8
9
10
dad () {
while (1) {
准备一个苹果;
P(plate);
P(mutex);
把苹果放入盘子;
V(mutex);
V(apple);
}
}
  1. 母亲
1
2
3
4
5
6
7
8
9
10
mom () {
while (1) {
准备一个橘子;
P(plate);
P(mutex);
放入一个橘子;
V(mutex);
V(orange);
}
}
  1. 女儿
1
2
3
4
5
6
7
8
9
10
daughter () {
while (1) {
P(apple);
P(mutex);
从盘中拿走苹果;
V(mutex);
V(plate);
吃掉苹果;
}
}
  1. 儿子
1
2
3
4
5
6
7
8
9
10
son() {
while (1) {
P(orange);
P(mutex);
从盘中拿走橘子;
V(mutex);
V(plate);
吃掉橘子;
}
}

注意

本题缓冲区大小为1,在任何时刻,orange、apple、plate三个同步信号量中最多只有一个为1,因此可以不用semaphore metux = 1;

如果盘子容量为2,不可删除semaphore metux = 1;

也即,如果缓冲区大小大于1,就必须专门设置一个互斥信号量mutex来保证互斥访问缓冲区

读者-写者问题

问题描述

有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:①允许多个读者可以同时对文件进行读操作;②只允许一个写者往文件中写信息;③任一写者在完成写操作之前不允许其他读者或写者工作;④写者执行写操作前,应让已有的读者和写者全部退出

问题描述

问题分析

  • 两类进程:写进程、读进程
  • 互斥关系:写进程-写进程、写进程-读进程
  1. 信号量
1
2
3
4
semaphore rw = 1; // 用于实现对共享文件的互斥访问
int count = 1; // 记录当前有几个读进程在访问文件
semaphore mutex = 1; // 用于保证count变量的互斥访问
semaphore w = 1; // 用于实现"写优先"
  1. 写者
1
2
3
4
5
6
7
8
9
writer () {
while (1) {
P(w);
P(rw);
写文件;
V(rw);
V(w);
}
}
  1. 读者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
reader () {
while (1) {
P(w);
P(mutex);
if (count == 0)
P(rw);
count++;
V(mutex);
V(w);
读文件...;
P(mutex);
count--;
if (coutn == 0)
V(rw);
V(mutex);
}
}

哲学家进餐问题

问题描述

一张圆桌上坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子的中间是一碗米饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根地拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才可以开始进餐,当进餐完毕后,放下子继续思考。

哲学家进餐问题

问题分析

  1. 可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐
  2. 要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反
  3. 仅当一个哲学家左右两支筷子都可用时才允许他抓起筷子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
semaphore chopstick[5] = {1, 1, 1, 1, 1};
semaphore mutex = 1; // 互斥地取筷子
Pi () {
while (1) {
P(mutex);
P(chopstick[i]); // 取左
P(chopstick[(i + 1) % 5]); // 取右
V(mutex);
吃饭;
V(chopstick[i]);
V(chopstick[(i + 1) % 5]);
思考;
}
}

管程

  • 为什么要引入管程
    • 解决信号量机制编程麻烦、易出错的问题
  • 组成
    • 共享数据结构
    • 对数据结构初始化的语句
    • 一组用来访问数据结构的过程(函数)
  • 基本特征
    • 各外部进程/线程只能通过管程提供的特定“入口”才能访问共享数据
    • 每次仅允许一个进程在管城内执行某个内部过程
  • 补充
    • 各进程必须互斥访问管程的特性是由编译器实现的
    • 可在管程中设置条件变量及等待/唤醒操作以解决同步问题

死锁的概念

  • 什么是死锁
    • 各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进
  • 死锁、饥饿、死循环的区别
    • 死锁:至少是两个进程一起死锁,死锁进程处于阻塞态
    • 饥饿:可以只有一个进程饥饿,饥饿进程可能阻塞也可能就绪
    • 死循环:可能只有一个进程发生死循环,死循环的进程可上处理机
    • 死锁和饥饿是==操作系统==要解决的问题,死循环是==程序员==要解决的问题
  • 死锁产生的必要条件
    • 互斥条件
      • 对必须互斥使用的资源的争夺才会导致死锁
    • 不剥夺条件
      • 进程保持的资源只能主动释放,不可强行剥夺
    • 请求和保持条件
      • 保持着某些资源不放的同时,请求别的资源
    • 循环等待条件
      • 存在一种进程资源的循环等待链
      • 循环等待未必死锁,死锁一定有循环等待
  • 什么时候会发生死锁
    • 对不可剥夺资源的不合理分配,可能导致死锁
  • 死锁的处理策略
    • 预防死锁:破坏死锁产生的四个必要条件
    • 避免死锁:避免系统进入不安全状态(银行家算法)
    • 死锁的检测和解除:允许死锁发生,系统负责检测出死锁并解除

死锁的处理策略——预防死锁

  • 破坏互斥条件
    • 将临界资源改造为可共享使用的资源(如SPOOLing技术)
    • 缺点:可行性不高,很多时候无法破坏互斥条件
  • 破坏不剥夺条件
    • 方案一:申请的资源得不到满足时,立即释放拥有的所有资源
    • 方案二:申请的资源被其他进程占用时,由操作系统协助剥夺(考虑优先级)
    • 缺点
      • 实现复杂
      • 剥夺资源可能导致部分工作失效
      • 反复申请和释放导致系统开销大
      • 可能导致饥饿
  • 破坏请求和保持条件
    • 运行前分配好所有需要的资源,之后一直保持
    • 缺点
      • 资源利用率低
      • 可能导致饥饿
  • 破坏循环等待条件
    • 给资源编号,必须按编号从大到小的顺序申请资源
    • 缺点
      • 不方便增加新设备
      • 会导致资源浪费
      • 用户编程麻烦

死锁的处理策略——避免死锁

什么是安全序列

  • 安全序列,指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要找到一个安全序列,系统就是==安全状态==

  • 安全序列可能有多个

什么是系统的不安全状态,与死锁有何联系

  • 如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了==不安全状态==
  • 如果系统处于安全状态,就==一定不会发生死锁==
  • 如果系统处于不安全状态,就==可能发生死锁==

如何避免系统进入不安全状态——银行家算法

核心思想:在进程提出资源申请时,先预判此次分配是否会导致系统进入不安全状态。如果会进入不安全状态,就暂时不答应这次请求,让该进程先阻塞等待

死锁的处理策略——死锁的检测

  • 如何检测

    • 数据结构:资源分配图
      • 两种结点
        • 进程结点(图中蓝色结点)
        • 资源结点(图中绿色结点)
      • 两种边
        • $进程结点\Longrightarrow 资源结点$(请求边,图中蓝色边)
        • $资源结点\Longrightarrow 进程结点$(分配边,图中绿色边)

    资源分配图

    • 死锁检测算法
      • 依次消除与不阻塞边进程相连的边,直到无边可消
      • 死锁定理:若资源分配图是不可完全简化的,说明发生了死锁
    • 如何解除
      • 资源剥夺法。挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。
      • 撤销进程法(或称终止进程法)。强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是==实现简单==,但所付出的==代价可能会很大==。因为有些进程可能已经运行了很长时间,已经接近结束了,旦被终止可谓功亏一篑,以后还得从头再来。
      • 进程回退法。让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。