一:进程的基本概念
1. 进程的定义
一个具有独立功能的程序集合在一个数据集合上的一次动态执行过程。
2. 进程和程序
联系:
- 程序是产生进程的基础,进程是程序的功能实现
- 通过调用关系,一个进程可以包含多个程序。
- 通过多次执行,一个程序可以对应多个进程。
区别:
- 程序是一段静态代码
- 进程是一次动态执行过程
- 程序是永久的,可以长久保存
- 进程是暂时的,是动态变化的过程
- 进程包含了程序,数据,进程控制块
3. 进程的组成
程序的状态信息
- 程序的代码
- 程序处理的数据
- 程序计数器:下一条指令
- 一组通用寄存器的当前值, 堆、栈
- 一组系统资源(内存资源,文件资源,网络资源等)
4. 进程的特点
- 动态性:动态创建和结束
- 并发性:可以被独立调度并运行
- 独立性:互不影响(进程运行的地址空间独立)
- 制约性:访问共享资源或进程间同步
5. 进程的控制结构——进程控制块PCB
(1)定义:
操作系统管理控制进程运行所用的信息集合
(2)作用:
用来描述进程的基本情况以及运行变化过程
- 创建进程:生成PCB
- 终止进程:回收PCB
- 管理进程:管理PCB
(3)特点:是进程存在的唯一标志
(4)结构:
进程标识信息:
标识进程自身,进程的产生者,进程的用户等
进程状态信息(处理机CPU状态信息保存区):保存进程的运行现场信息
- 用户可见寄存器:数据、地址等寄存器
- 控制、状态寄存器:程序计数器PC、程序状态字PSW
- 栈指针:过程调用、系统调用、中断处理
进程控制信息:
- 调度和状态信息
- 进程间通信信息(接收方PCB)
- 存储管理信息:本进程映像存储空间的数据结构
- 进程所用资源(文件等)
- 有关数据结构的连接信息:进程可以连接到PCB链或其他进程
(5)组织方式
- 链表(动态插入删除):同一状态的进程PCB组成同一条链表如就绪链表和阻塞链表等
- 索引表(个数固定):同一状态的进程归入同一个哈希表如就绪索引表和阻塞索引表等,以PCB为键
二:进程的生命周期
1. 进程的生命期
(1)进程创建
通过构建PCB自动完成初始化并形成新的进程
- 系统初始化:创建第一个进程(init进程)
- 用户请求创建一个新进程
- 正在运行的进程执行一个系统调用创建进程
(2)进程运行
内核选择一个就绪进程,占用CPU处理机执行
(3)进程等待(自身)
- 请求并等待系统服务完成
- 启动某种操作或等待其他进程完成
- 等待数据到达
(4)进程唤醒(其他进程或操作系统)
- 资源满足
- 等待的事件到达
- PCB插入就绪队列
(5)进程结束
- 正常结束
- 错误结束
- 操作系统强制结束
- 被其他进程强制结束
2. 进程的状态变化模型
(1)三种基本状态:
- 就绪状态:已经具备全部资源
- 运行状态:正在运行
- 等待状态:等待资源或事件
(2) 进程的挂起模型
进程处在挂起状态,进程映像在磁盘上。
- 内存空间过小
- 系统出现故障
- 用户调试程序
挂起与激活:
- 阻塞挂起状态:
- 活跃阻塞:进程在主存(内存)中等待事件后,可以进入活跃就绪状态
- 静止阻塞:进程在辅存(磁盘)中被挂起,等待事件后可以进入静止就绪状态
- 就绪挂起状态:
- 活跃就绪:进程在主存(内存)中,可被调度
- 静止就绪:进程在辅存(磁盘)中被挂起,无法调度 ,直到没有活跃就绪进程
(3)状态队列:
每个进程的PCB都根据状态进入相应队列,状态发送变化后,出队并进入另一个队列
- 就绪队列:多种优先级队列
- 阻塞队列:多种事件队列
三:进程与线程
1:引入
问题:多进程可以减少开销,但是会有并发性
解决:操作系统以线程为基本单位
2:定义:进程执行的一条流程
进程:
- 资源部分:进程管理全部资源
- 执行部分:进程的执行分为多个线程。
线程:可以共享进程的资源,独立执行自己的程序段,拥有自己的独占资源如堆栈和寄存器等
3:结构——线程控制块TCB
- 程序计数器PC
- 堆栈SP
- 寄存器
4:特点
优点:
- 一个进程可以有多个线程
- 线程可以并发执行
- 线程之间可以共享相同的地址空间和文件等资源
缺点:
- 无法保障安全,会影响所有线程
5:例子
MS-DOS:单进程单线程
UNIX:多进程单线程
WINDOWS,LINUX:多进程多线程
6:区别和联系
进程是资源分配单位,线程是CPU调度单位
进程有完整的资源平台,线程独享必要资源
线程可以减少开销
7:实现
(1)用户线程(用户空间):应用程序管理的线程
操作系统只能够管理进程
由用户级的线程函数完成对线程的创建、终止、同步、调度等管理,维护进程的TCB列表,完成线程间的切换(速度快),每个线程可以有自定义调度算法
特点:
- 可用于不支持线程技术的多进程操作系统
- 线程发起阻塞时整个进程都会阻塞
- 线程需要主动让出CPU,否则其他线程无法进行(操作系统无法中断)
- 线程得到的时间片少,执行较慢
(2)内核线程(内核)WINDOWS:操作系统管理的线程
由进程管理资源
由操作系统的内核(PCB)完成对线程的创建、终止、同步、调度等管理,维护进程的TCB列表,完成线程间的切换(速度慢)
特点
- 开销较大
- 线程阻塞不会影响其他线程
- 线程得到的时间片多,执行较快
(3)轻量级进程(内核)LINUX
一个进程可以有多个轻量级进程,每个轻量级进程有内核线程支持
四:进程的切换和控制
1. 进程的上下文切换
由于各个进程共享CPU资源,需要切换让各个进程运行。停止当前运行的进程,调度其他进程进入运行状态。
- 上下文的保存——PCB保存(寄存器(程序计数器,栈指针),CPU状态)
- 上下文的恢复——PCB恢复
2. 进程的控制
(1)进程的创建:fork
定义:把一个进程复制成两个进程。创建一个继承的子进程,它会复制父进程的所有地址空间(代码,数据,堆栈,但是PID不同)
返回值:子进程返回0,父进程返回子进程的PID
过程:
- 子进程分配资源,初始化PCB,复制父进程的内存和cpu寄存器
- 子进程开始运行
- 初始化内核线程(内核堆栈),设置共享的地址空间
性能:开销很大
- 省略复制父进程到子进程的过程
- 轻量级fork :不用创建一个同样的内存映像,只复制了一小部分父进程的内容,子进程应该几乎立即调用exec
- COW技术(只复制了父进程的页表,指向的是同一地址空间,对某个地址单元进行写操作时触发异常才需要复制)
(2)进程的加载:exec
把一个新的程序加载在内存中,重写子进程的地址空间(代码,数据,堆栈,会保留PID)
(3)进程的等待:wait
父进程必须接受并处理子进程向父进程返回的一个值。
- 父进程睡眠等待
- 子进程运行结束——exit():释放大部分资源,检查父进程是否活着
- 如果活着,子进程进入僵尸状态,返回exit()的结果
- 父进程帮助子进程释放PCB,清理所有僵尸进程
- 如果父进程死亡,祖先进程代替父进程清理
(4)进程的终止:exit
释放进程拥有的资源、内存、操作系统的数据结构
五:进程的调度
1. 调度的概念
作业的三级调度
- 高级/作业调度/长调度:决定处于输入池中的哪个后备作业可以调入主系统做好运行的准备,成为就绪进程
- 中级/对换调度:决定处于交换区中的哪个就绪进程可以调入内存,在内存紧张时把阻塞进程调入交换区
- 低级调度/进程调度/短调度:决定处于内存中的哪个就绪进程可以占用CPU
2. 调度的条件
- 进程从运行到等待
- 进程的终结
3. 调度的策略
- 用户级不可抢占式:程序必须等待时机结束
- 用户级抢占式:运行的进程可以被唤出进行中断处理(优先级改变,时间片结束,)
- 内核级不可抢占:用户进程执行系统调用进入内核态,此时内核中的用户进程仍然会继续运行,不会发生状态变化
- 内核级可抢占:用户进程执行系统调用进入内核态,此时内核中的用户进程需要切换到另一个进程,主动放弃cpu
4. 调度的原则
高带宽低延迟:
- CPU利用率
- 吞吐量(高):单位时间内的进程数量,也就是带宽
- 周转时间(短):进程从初始化到结束的时间
- 等待时间(短):进程在就绪队列的时间
- 响应时间(短):进程从请求到响应的时间,也就是延迟
公平:增加平均响应时间
5. 调度的算法
(1)基本操作系统
先来先服务FCFS
按照进程到达顺序进入队列,如果队列中的进程阻塞,则队列中的下一个进程会被调度。
优点:
- 简单
- 适合长作业、IO密集型作业
缺点:
- 平均等待时间波动大(处理时间少的进程可能在队列后等待)
- 可能导致IO和CPU的重叠处理(CPU密集型进程会导致IO设备闲置,IO密集型进程也会等待)
短进程优先 / 短作业优先 / 短剩余时间优先
进程执行时间决定优先级,按照进程执行时间在队列中排序,如果队列中的进程阻塞,则队列中的下一个进程会被调度。
如果有优先级更高的进程进入:
- 非抢占(短进程/短作业):不会打断当前进程,进入就绪队列
- 抢占(短剩余时间):当前进程时间片用完并且剩余时间比较长,则选择剩余时间更短的进程
优点
- 平均等待时间最少
缺点
- 连续短进程会导致长进程饥饿
- 需要预知执行时间(询问用户或者通过历史预估):根据上一个时刻的执行时间预估这一时刻的执行时间
最高响应比优先
响应比R = (等待时间+执行时间) / 执行时间
综合考虑等待时间和执行时间,进程响应比决定优先级,按照进程的响应比在队列中排序,如果队列中的进程阻塞,则队列中的下一个进程会被调度。
特点
- 不可抢占
- 需要预估时间
- 可以缓解饥饿事件
时间片轮转法
通过时间片轮转提供资源利用率和进程并发性
- 固定时间片:每个进程使用相同的时间片,在时间片结束切换下一个进程,等待所有进程执行一次后,没有执行完的进程继续循环执行
- 公平,一般使用时间片维持切换上下文的开销小于%1
- 若时间片过大,则等待时间过长,最长情况会变成FCFS算法
- 若时间片过小,则反应快,切换的开销大
- 可变时间片:根据进程的不同要求动态修改时间片大小
优先级调度
让每一个进程拥有一个优先数,数值大的进程优先级高,优先调度。
- 静态优先级:进程的优先级在创建时就确定,不会改变
- 动态优先级:在进程运行过程中可以动态改变优先级
多级反馈队列
就绪队列根据高低优先级划分,高优先级队列——侧重交互,低优先级队列——侧重数据处理,每个队列选择不同的算法
- 固定优先级:先高后低
- 导致饥饿队列
- 动态优先级:时间片随优先级的增加而增加,高优先级时间片大于低优先级
进程先进入最高优先级队列按FCFS算法调度,若在一个时间片内无法执行完,则进入下一优先级队列。若在最后一个优先级队列按轮询算法调度。
如果有高优先级进程进入抢占CPU,则当前进程进入该队列末尾- CPU密集型(计算时间长):优先级下降快,提高吞吐量,避免多次调度
- IO密集型(执行时间短):停留在高优先级,减少切换阻塞的开销,缩短响应时间
公平共享调度
未使用的资源按每个用户组分配的资源比例来分配,没有达到资源使用率目标的组获得更高优先级
特点:避免不重要的用户组垄断资源
算法 | 优点 | 缺点 |
---|---|---|
FCFS | 简单 | 不公平,平均等待时间较差 |
SPN/SRT | 平均等待时间最小 | 不公平,可能导致饥饿,需要预估时间 |
HRRN | 缓解饥饿 | 不公平,不可抢占,需要预估时间 |
RR | 公平 | 平均等待时间较差 |
MLFQ | 平均等待时间较好 | |
公平共享调度 | 公平 |
(2)嵌入式实时系统(工厂)
定义
正确性依赖于时间和功能的操作系统
特点
时间确定,注重正确性,可预测性
分类
- 强实时系统:任务必须在时间内完成
- 若实时系统:尽量完成任务,要求重要的进程的优先级更高
工作时间
- 硬时限(绝对期限):必须保证任务完成
- 软时限(相对期限):尽最大努力完成任务
工作单元
任务:一次计算或文件读取等工作
属性:
- 获取资源
- 定时参数
过程:
- 任务就绪
- 任务执行(相对期限)
- 任务结束(不能超出绝对期限)
周期任务:一系列相似的任务
特点:任务有规律的重复
属性:
- 周期:也就是任务结束的期限
- 执行时间(在周期之间)
- 使用率:执行时间 / 周期
过程:任务循环执行
调度算法
静态优先级调度:事先确定优先级
速率单调调度算法RM:一开始就确定执行周期和执行时间。周期越短优先级别越高
动态优先级调度:优先级不断变化
最早期限调度算法EDF:任务离期限越短优先级越高
(3)多核处理器(家用)
多个CPU组成一个多核处理器:负载共享
对称多处理器算法SMP:
- 每个处理器运行自己的调度程序
- 需要在调度程序中同步
六:进程的通信
1. 进程通信的概念
进程之间在保持独立性的同时,能够有效的沟通
1:建立通讯链路:
- 物理:共享内存,硬件总线
- 逻辑:逻辑属性
2:使用通讯机制:发送消息、接收消息
- 低级原语:PV操作、管程
- 高级原语:
- 共享内存
- 消息队列
- 信号
- 管道
2. 进程通信的特点——竞态条件:不确定、不可重现
独立的进程
- 没有共享资源
- 确定性:输入决定输出
- 可重现
- 顺序不重要
合作的进程
- 共享状态
- 不确定
- 不可重现
合作的重要性:
- 共享资源
- 提高效率,并行执行程序
- 模块化,易于扩展系统
3. 多进程通信的互斥——锁机制
(1):没有互斥引发的问题
临界区(代码区域):只允许一个进程执行临界区的代码,进行读写操作。
进程间互斥:当一个进程位于临界区并且访问共享资源时,其他进程不会处于临界区,也不会访问任何相同的共享资源。(多线程共享公共数据的协调执行)
原子操作:一次不存在中断或者失败的执行,执行要么成功结束,要么没有执行(如果进程不是原子操作,执行过程中会被切换,则执行结果可能会不一致)
(2):加锁操作的问题
可以通过对临界区加锁,实现互斥
引发的问题:
- 进程间死锁:两个或以上的进程,在相互等待特定事件,无法将自身任务进行下去
- 饥饿进程:一个可执行的进程被调度器持续忽略,以至于虽然处于可执行状态却不会执行。
- 忙状态:一个没有继续执行的进程一直占用CPU
(3)目标:
- 线程间互斥:同一时间临界区最多存在一个线程
- 线程可前进:不会产生死锁
- 有限等待:不会产生饥饿
- 不会处于忙状态
(4)解决方案:
1:基于硬件禁用中断(单处理器)
通过指令,在进入临界区时屏蔽中断,进入临界区后恢复中断(硬件将中断延迟)
原因:没有中断就没有上下文切换,就没有并发
缺点:
- 该线程无法停止,导致其他线程处于饥饿状态
- 如果临界区过大,会无法限制响应中断所需的时间
- 多CPU状态下无法解决问题
2:基于软件实现互斥(复杂)
线程之间通过共享变量进行同步:
- 用一个保护区保护临界区
- 线程通过共享变量得到信息后进入保护区
- 线程离开保护区后,改变共享变量提醒其他线程
使用一个共享变量的缺点:
- 标记允许进入临界区的线程(可能导致其他线程饥饿)
- 标记准备进入临界区的线程(可能导致线程间无法互斥或死锁)
Dekker算法
同时使用两个共享变量:
- 线程A做好准备,等待CPU的允许,避免线程B也做好准备而死锁
- 当另一个进程B做好准备并且CPU允许调用时,执行B
- 如果B没做好准备,或者CPU允许A执行,则执行A
- 进程A或进程B执行后,标记自己对下一次执行还没准备好,让进程先执行另外一个,避免饥饿
扩展Eisenberg算法——N个进程(进程循环)
当线程i准备好进入临界区,需要等待前面i-1个线程
当后面的i+1个进程准备好进入临界区,也需要等待i
Bakery算法(N个进程的临界区)
- 进入临界区之前,进程接收一个数字
- 得到数字最小的进程进入临界区
- 如果数字相同的话,那么PID较小的先进入
3:基于硬件的抽象算法(多处理器)
硬件提供高级的原语如锁,信号量等,可以从高级原语构建。
锁:抽象的数据结构,表示临界内存单元
具有一个二进制状态:是否锁定
具有两种方法:
- Acquire:等待并得到锁
- Release:释放锁并唤醒
特殊原子操作的指令
- 通过特殊的内存访问电路
- 针对单处理器和多处理器
Test-And-Set :测试、置位
- 从内存中读取值
- 测试值是否为1
- 设置内存值为1
exchange:交换
- 交换内存中的两个值
使用特殊指令实现锁:
1:Test-And-Set:
进程进入临界区——获取锁
- 输入锁的内存值lock=0
- 使用test-and-set:
- 如果锁被释放,那么lock值由0设为1(锁空闲并且正在占用,可以继续执行)
- 如果锁被占用,那么lock值仍然为1(锁已经被占用了并且需要…)
- 临界区较小:忙等待,避免切换上下文开销
- 临界区较大:加入等待队列,然后阻塞,避免忙等待
进程退出临界区——释放锁
- 重新初始锁的内存值lock=0
- 如果有等待队列,把线程移出队列,唤醒正在睡眠的进程
2:exchange
使用一个标志位key=1
进程进入临界区——获取锁
- key初始化为1,进行exchange
- 如果lock值为0,那么key=0,lock=1(证明锁空闲,可以占用,可以进入临界区)
- 如果lock值为1,那么key=1,lock=1(证明锁已经被占用了,不能进入临界区)
- 忙等待
- 进入等待队列
进程退出临界区——释放锁
- 重新初始锁的内存值lock=0
- 如果有等待队列,把线程移出队列,唤醒正在睡眠的进程
优点:
- 适用于单处理器或者共享主存的多处理器中任意数量的进程
- 简单
- 适用于多临界区
缺点:
- 忙等待
- 可能导致饥饿
- 死锁
4. 进程通信的条件同步——信号量机制
(1):信号量的数据类型
一个整形sem+两个原子操作
- P操作:sem- -,如果sem<0,则等待,可以引起阻塞
- V操作:sem++,如果sem<=0,则唤醒等待的进程,不会引起阻塞
(2):信号量的特点
- 信号量是整数
- 信号量是被保护的,唯一改变的是P、V操作
- 信号量是公平的:先进先出(回旋锁是随机的)
(3):信号量的类型
- 二进制信号量:0或1(两个进程)
- 计数信号量:非负值(多进程)
(4):信号量的使用
进程间的互斥:一个临界区只允许一个进程存在
进程间的条件同步:一个进程必须等待另一个进程完成特定操作才能继续执行
1:一般的互斥操作
- 初始化一个二进制信号量为临界区允许的进程个数(例如1)表示互斥(目标进程进入临界区时seg=1-1=0,如果其他进程进入临界区时seg=0-1=-1,需要等待)
- 进入临界区——P操作
- 退出临界区——V操作
2:一般的同步操作
-
初始化两个二进制信号量表示同步,A为0表示消费进程的可用资源为0,B为N表示生成进程的生产容量为N
- 一个消费进程进行某个操作获取资源,A=0-1=-1,需要等待,如果另一个进程完成操作可以提供资源,A=-1+1=0,唤醒等待的进程
- 另一个生成进程需要空间来生成资源,B=N-1=…=0-1=-1,需要等待,如果另一个进程完成操作提取了资源,B=0+1=…=N-1+1=N,唤醒等待的进程
-
等待另一个进程——P操作
-
另一个进程执行完成——V操作
3:条件同步(生产者消费者模型)
特点:
- 互斥:在任何一个时间只能有一个线程操作缓冲区
- 同步:缓冲区为空时消费者等待,缓冲区满时生产者等待
注意点:
- 使用的信号量已经被占用
- 没有释放信号量
- 多个信号量阻塞后造成死锁
过程
-
初始化互斥信号量为1(缓冲区只能有一个进程),初始化同步信号量——缓冲区已有个数为0,缓冲区空闲个数为N(可以有N个生产者生产数据)
-
互斥:添加和取出数据时,使用P、V操作互斥信号量,保证消费者和生产者只有一个操作缓冲区
-
同步:
生产者- 对空闲信号量进行P操作(减少空闲个数,有N个生产者最多减少N次,如果缓冲区已经满了那就需要等待消费者)
- 添加数据
- 对占用信号量进行V操作(增加占用个数,最多可以占用N个,提醒消费者有数据了)
消费者
- 对占用信号量进行P操作(减少占用个数,最多可以使用N个,如果缓冲区是空的那就需要等待生产者)
- 取出数据
- 对空闲信号量进行V操作(增加空闲个数,最多可以增加N个,提醒生产者可以继续了)
(5):信号量的实现
P操作:
- seg–
- 如果seg<0,当前线程加入等待队列并睡眠
V操作:
- seg++
- 如果seg<=0,从等待队列取出线程并唤醒
5. 管程机制
(1)管程的定义
采用资源集中管理的方法,将资源用某种数据结构抽象表示,从而对共享资源的申请和释放就可以通过过程在数据结构上的操作来实现。
组成:
- 共享数据
- 变量
- 一个锁:指定临界区,保证互斥
- 多个条件变量:等待或通知信号量,保证同步
- 过程
(2)管程的函数
锁:
- acquire 获取锁:等待锁并抢占
- release 释放锁:唤醒等待者
条件变量
- wait 等待:挂起线程,释放锁并睡眠,选择下一个线程并执行,直到获取锁
- signal 唤醒:如果有线程在等待,取出并唤醒一个线程(可以自身马上睡眠,让唤醒的线程执行,此时无需判断,因为只有1个线程被唤醒;或者自身先释放锁,再让唤醒的线程执行,此时需要判断是不是有多个线程被唤醒抢先执行)
(3)生产者消费者模型
-
初始化管程
1. 锁
2. 条件变量:空缓存区条件、满缓存区条件
3. 缓冲区的数据个数 -
互斥:在生成数据和取出数据的时候,使用锁保证只有一个线程在管程里面
-
同步:
生产者:- 如果缓存区满了,那么满缓存区的这一个条件变量睡眠,释放锁,等待消费者取数据
- 如果缓冲区个数还没有满,那么可以添加数据,同时缓存区个数增加,唤醒消费者
消费者:
- 如果缓存区为空,那么空缓存区的这一个条件变量睡眠,释放锁。等待生产者生成数据
- 如果缓存区不为空,那么可以取出数据,同时缓存区的个数减少,唤醒生产者
6. 经典的同步问题
(1)读者和写者的问题
问题的描述
目的:访问共享数据
约束:
- 读者:不需要修改数据,可以同时有多个读者
- 写者:需要读取和修改数据,只能有一个写者,写者优先(当有一个写者进程正在执行时,如果后面也有写者进程,它可以跳过读者继续读取)
- 读者和写者只能有一个能访问数据,读者优先(当有一个读者进程正在执行时,如果后面也有读者进程,它可以跳过写者继续读取)
信号量实现
1:设计共享变量
- 数据集:data
- 同步变量
- 读者个数:Rcount=0
- 写者个数:1
- 互斥变量
- 互斥读者个数:countMutex=1
- 互斥写者与读者:writeAndReadMutex=1
2:设计函数
- 互斥:
1. 写者:只有一个写者,写者和读者是互斥的。(在写入数据时需要保护)
2. 读者:可以有多个读者,但是读者数量是共享变量,是互斥的。(在修改读者数量时需要保护,在读取的时候不用保护) - 同步:
1. 由于读者优先,所以写者肯定会等待读者,不需要同步
2. 由于读者优先,第一个读者需要阻塞写者,后来的读者不需要同步
3. 如果是最后一个读者,在读取数据后,需要唤醒写者写入数据
3:实现
写者:
- 对writeAndReadMutex进行P操作
- 写入数据
- 对writeAndReadMutex进行V操作
读者:
- 对countMutex进行P操作
- 如果没有读者,对writeAndReadMutex进行P操作,保证只有自己一个读者。如果有读者,那就直接继续读
- 读者数量增加
- 对countMutex进行V操作
- 读取数据
- 对countMutex进行P操作
- 读者数量减少
- 如果没有读者,对writeMutex进行V操作,提醒写者可以继续写了
- 对countMutex进行V操作
管程实现
1:设计变量
- 状态变量
- 正在读的读者个数:AR=0
- 正在写的写者个数:AW=0
- 等待写的读者个数:WR=0
- 等待写的写者个数:WW=0
- 条件变量
- 可以读 okRead,可以写 okWrite
- 互斥锁变量
- Lock:函数互斥
2:设计函数
读者
1 读取前
- 加锁
- 如果有写者正在读(AW+WW>0),那么等待读的WR增加,可以读的条件变量okRead睡眠。 被唤醒后WR减少
- 正在读的AR增加
- 释放锁
2 读取数据
3 读取后
- 加锁
- 正在读的AR减少
- 如果没有读者(AR=0)了,并且有等待的写者(WW>0),唤醒写者,可以写的条件变量okWrite唤醒
- 释放锁
写者
写入前
- 加锁
- 当前有读者或者有写者(AW+AR>0),那么等待写的WW增加,可以写的条件变量okWrite睡眠。 被唤醒后WW减少
- 正在写的个数AW增加
- 释放锁
写入数据
写入后
- 加锁
- 正在读的AW减少
- 如果有等待的写者WW>0,可以写的条件变量okWrite唤醒。如果有等待的读者WR>0,可以写的条件变量okRead全部唤醒
- 释放锁
(2)哲学家就餐问题
问题的描述
五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。
科学家的角度:自己吃饭优先
- 如果左邻居或右邻居正在用餐,等待
- 拿起两把叉子吃面条
- 放下左边和右边的叉子
- 重新循环
计算机的角度:大家吃饭优先
- 如果左邻居或右邻居正在用餐,阻塞
- 拿起两把叉子吃面条
- 放下左边的叉子,看看左邻居能否进餐(饥饿+两把叉子),是则唤醒。
- 放下右边的叉子,看看右邻居能否进餐(饥饿+两把叉子),是则唤醒。
- 重新循环
设计变量
使用叉子作为临界资源:会造成死锁
使用就餐作为临界资源:一次只允许一个人吃饭
使用科学家的状态作为临界资源(思考、饥饿、吃饭)
- 状态变量
- 哲学家个数:N
- 左邻居:i
- 右邻居:(i+1)%N
- 思考 THINKING=0,饥饿HUNGRY=1,吃饭EATING=2
- 记录状态:state[N]
- 互斥变量
- 对状态的访问时互斥:mutex=1
- 同步变量
- 哲学家之间需要同步:s[N]=0
设计函数
1:思考
- 对mutex使用P操作,保证互斥
- 设置状态(state[i]=THINKING)
- 对mutex使用V操作
2:拿叉子(要么拿到叉子吃饭,要么阻塞)
- 对mutex使用P操作,保证互斥
- 设置状态(state[i]=HUNGRY)
- 尝试拿叉子
1. 判断我是饿的,左边的和右边的科学家都不在吃饭
2. 设置状态(state[i]=EATING)
3. 通过V操作提醒自己吃饭,防止后面P操作阻塞吃不了饭 - 对mutex使用V操作
- 使用P操作阻塞自己:如果拿不到叉子则先等其他人吃好,如果拿到叉子吃了饭则表示吃饱了
3:吃饭——临界区
4:放叉子(放下叉子,可以唤醒其他邻居)
- 对mutex使用P操作
- 设置状态(state[i]=THINKING)
- 判断左邻居和右邻接能不能进餐
1. 判断邻居是饿的,邻居左边的和右边的科学家都不在吃饭
2. 设置状态(state[i]=EATING)
3. 通过V操作提醒邻居吃饭 - 对mutex使用V操作
7. 死锁
(1)死锁的概念
一组阻塞的进程,持有一种资源,需要等待获取另一个进程占用的资源
(2)系统模型
需求方:进程
需求资源:CPU,内存单元,IO设备、文件、数据库
访问资源的过程:
- 对需求资源进行申请:空闲资源
- 获取并使用资源:已被占用资源(其他资源无法使用)
- 释放资源:恢复空闲资源
资源的特征——可重复使用
- 在一段时间只能被一个进程使用,进程不能被kill
- 进程获得资源后必须释放给其他进程使用
- 如果进程获得资源后还想继续请求其他资源,有可能发生死锁
资源分配图
顶点:进程、资源(个数)
边:请求资源、分配资源
环:如果每个资源有多个实例,可能死锁
(3)死锁的特征(必要不充分条件)
- 互斥:在一个时间只能有一个进程使用资源
- 请求保持:进程持有至少一个资源,正在等待其他进程持有的资源
- 不可剥夺:资源只能被进程主动释放
- 循环等待
(4)死锁的处理
- 避免和预防死锁
- 如果检测到死锁,需要进行死锁恢复
- 忽略死锁
1:死锁预防——进程请求前(最强)
限制申请方式
- 进程间不互斥:执行会出现不确定性
- 进程持有所有资源,或者等待:资源利用率低,可能发生饥饿
- 可抢占:把正常执行的互斥进程杀死
- 预先静态分配法:资源利用率低,可能发生饥饿
- 资源有序分配法:对所有资源类型排序,并要求每个进程按照资源类型进行申请(开销大,限制了进程请求)
2:死锁避免——进程请求时
当一个进程请求可用资源时判断立即分配是否能使系统处于安全状态
需要的信息:
- 每个进程声明需要的每个资源的最大数量
- 限制提供与分配的资源数量
- 动态检查,确保不会出现环形等待
安全状态:
针对所有进程,存在安全序列<P1,P2…Pn>
(Pi可用资源 = (P0~Pi-1)释放的资源+当前系统可用的资源)
资源分配图:
- 虚线:进程需要的资源 / 进程释放的资源
- 实线:进程请求的资源
银行家算法
以银行借贷系统的分配策略为基础,通过寻找允许每个进程获得最大资源并结束的进程请求的安全序列,判断并保证系统的安全运行
前提条件:
- 有多个进程实例
- 每个进程最大限度利用资源
- 进程获得所有资源,必须释放他们
- 进程请求不到资源,需要等待
数据结构:
- 进程数量:N
- 资源类型:M
- 总需求量:一个N×M的矩阵(Max[i,j] = k表示进程i总共需要资源j的个数k)
- 已分配量:一个N×M的矩阵(Allocation[i,j] = k表示进程i已经分配资源j的个数k)
- 未来需求量:一个N×M的矩阵(Need[i,j] = k表示进程i还需要分配资源j的个数k)
- 剩余空闲量:长度为M的向量(Avalilable[j] = k表示资源j有k个可以用)
- 当前的空闲资源:长度为M的向量 (work = Avalilable)
- 标记当前进程是否执行:长度为N的向量(Finish[j] = true表示进程i正常执行结束)
- 当前进程所需的资源: 长度为M的向量(Request[j] = k表示进程需要k个资源j)
资源 进程 |
Avaliable 可用资源 |
Max 总需求 |
Allocation 已分配资源 |
Need 需求资源 总需求-已分配 |
Work 当前可以资源 可用+已分配 |
能否执行 |
---|---|---|---|---|---|---|
P0 | 1,1,0 | 2,2,1 | 1,2,1 | 1,0,0 | 2,3,1 | True |
P1 | 2,3,1 | 6,4,2 | 2,1,1 | 0,1,1 | 4,4,2 | True |
P2 | 4,4,2 | 2,2,2 | 1,1,1 | 5,3,1 | False |
过程:
- 初始化,如果进程有提出请求,判断进程分配资源后是否安全
- 如果进程的Requesti>Needi,则不能分配,已经超过最大要求
- 如果进程的Requesti>Avaliablei,则需要等待,因为现在的资源不能满足
- 通过修改状态,假定已经分配请求资源并且释放了(Avaliable=Avaliable-Request,Allocationi=Allocationi+Request,Needi=Needi-Request)
- 执行下面的判断,如果不安全则进程等待,恢复刚才的状态,否则可以分配资源
- 寻找目标进程
- 进程没有结束:Finish[i]=false
- 进程请求的资源满足:Needi<Work
- 寻找成功
- 进程可以正常结束:Finish[i]=true
- 进程释放资源:Work=Work+Allocationi
- 继续寻找
- 寻找失败
- 所有进程都可以正常执行,返回安全
- 不能分配资源给这个进程,返回不安全
3:死锁检测——进程运行时
通过进程等待图判断:
进程等待图:
- 顶点:进程
- 边:如果进程请求的资源被另一个进程拥有,则指向另一个进程
- 环:可能死锁
通过定期调用检测算法搜索图中是否存在环
通过死检测算法判断:
数据结构:
- 进程数量:N
- 资源类型:M
- 剩余空闲量:长度为M的向量(Avalilable[j] = k表示资源j有k个可以用)
- 已分配量:一个N×M的矩阵(Allocation[i,j] = k表示进程i已经分配资源j的个数k)
- 当前进程所需的资源: 一个N×M的矩阵(Request[i,j] = k,表示进程i需要k个资源j)
- 标记当前进程是否执行:长度为N的向量(Finish[j] = true表示进程i正常执行结束)
- 当前进程所需的资源: 长度为M的向量(Request[j] = k表示进程需要k个资源j)
过程:
- 初始化
- 寻找目标进程
- 进程没有结束:Finish[i]=false
- 进程请求的资源满足:Requesti<Work
- 寻找成功
- 进程可以正常结束:Finish[i]=true
- 进程释放资源:Work=Work+Allocationi
- 继续寻找
- 寻找失败
- 所有进程都可以正常执行,安全
- 死锁状态
缺点
- 无法确定检测频率
- 无法确定回滚或者杀死的进程
- 多次检测可能是资源图的多个循环
4:死锁恢复(最弱)
- 资源剥夺法:从一些进程强行剥夺足够的资源分配给死锁程序
- 撤销进程法:在一个时间内终止一个进程直到没有死锁
- 按以下顺序终止进程
- 进程的优先级
- 进程的运行时间
- 进程需要的运行时间
- 进程占用的资源
- 进程需要的资源
- 终止的进程个数
- 进程是交互的,批处理的
- 回滚:重启进程到安全状态
8:进程通信的其他机制
(1)信号signal
定义:
软件中断,通知事件处理
比如sigfpe,sigkill,sigusr1,sigstop,sigcont
处理方式:
1:catch 调用指定处理函数
2:ignore 操作系统默认操作(进程停止运行)
3:mask 闭塞信号,不会传送
特点:
- 效率高,适合通知
- 无法传输要交换的数据
实现
- 进程注册信号处理器(Signal handles),将这个系统调用发送给操作系统
- 操作系统根据信号,在从内核态切换到用户态时,修改堆栈的入口为对应的信号处理函数。再把后面的指令地址作为栈针的返回地址,执行后面的其他操作。
(2) 管道 type(内存中的缓冲区)
进程之间通过管道输出或输入数据。
- 父进程创建管道
- 父进程分别创建子进程A和B
- 子进程A将数据输入管道,子进程B将数据输出管道
特点:
- 两个进程具有父子关系
- 数据是字节流
(3)消息队列
- 进程A发送消息,将数据输入消息队列
- 进程B接收消息,将数据从消息队列取出
特点:
- 消息队列按照FIFO来管理消息
- 可以有多个进程传递消息
- 进程之间可以没有关系
- 消息是一个字节序列
分类
(1)直接通信
- 建立通讯链路
- 自动建立链路
- 一条链路只对应一对通信进程
- 每对进程之间只有一条链接存在
- 链接通常是双向的
- 进程正确的命名对方,使用send、receive
(2)间接通信
- 建立通讯链路
- 只有进程共享共同的消息队列,才建立链路
- 链接可以与许多进程相关联
- 每对进程可以共享多个通信链路
- 连接可以是单向或者双向的
- 建立消息队列
- 通过消息队列发送和接收消息
- 销毁消息队列
(3)阻塞同步通信
发送消息并阻塞,直到发送成功再继续执行
(4)非阻塞异步通信
发送消息并继续执行
缓存消息——队列的消息被附加到链路
1:0容量(同步通信)
发送方必须等待接收方
2:有限容量
如果队列满了发送方必须等待
3:无限容量
发送方无需等待
如果队列为空,接收方等待发送方或者返回错误信息
(4)共享内存 share memory
每个进程都有私有地址空间,在每个地址空间明确设置了共享内存
实现:
同一个物理地址,映射到不同的地址空间
特点:
- 快速,方便,高效
- 没有系统调用,数据复制
- 必须同步数据访问