第一章 计算机系统概述
计算机系统概述
计算机系统视图
程序员的视图
软件开发的不同层次
- 计算机硬件系统:机器语言
- 操作系统之资源管理:机器语言+广义指令(扩充了硬件资源管理)
- 操作系统之文件系统:机器语言+系统调用(扩充了信息资源管理)
- 数据库管理系统:数据库语言(扩充了功能更强的信息资源管理)
- 语言处理程序:面向问题的语言
计算机程序的执行过程
操作系统概述
操作系统类型
批处理操作系统
- 成批处理作业
- 作业控制语言与作业说明书
- 脱机工作方式
- 追求系统效率与吞吐量
分时操作系统
- 用户通过终端直接控制程序执行
- 交互式工作方式
- 交互型、友善性、快速响应
- 今天最常见的计算机操作方式
实时操作系统
- 事件驱动,有较高时间要求
- 实时操作系统的分类
- 过程控制系统的处理步骤:数据采集、加工处理、操作控制、反馈处理
系统调用的实现过程
操作系统结构分类
- 单体式结构
- 层次式结构
- 虚拟机结构
- 微内核结构
- 客户/服务器结构
特权指令和处理器状态
- 从资源管理和控制程序执行的角度出发,必须设置特权指令,提供给操作系统的核心程序使用
- 处理器状态
- 管理状态(特权状态、系统模式、特态或管态):处理器可以执行全部指令,使用所有资源,并具有改变处理器状态的能力
- 用户态(目标状态、用户模式、常态或目态):处理器只能执行非特权指令
中断与指令周期
中断装置与中断响应
自愿性中断事件处理
中断控制流程
第二章 处理器管理
状态模型
三状态模型及其转换
含两个挂起态(七状态模型)
虚拟内存中的用户进程
多线程技术
单线程结构进程给并发程序设计效率带来的问题
- 进程切换开销大
- 进程通信开销大
- 限制了进程并发的粒度
- 不适合并行计算的要求
线程的概念
解决问题的基本思路
- 把进程的两项功能——“独立分配资源”与“被调度分派执行”分离开
- 进程作为系统资源分配和保护的独立单位,不需要频繁地切换和保护资源
- 线程作为系统调度和分派的基本单位,能轻装运行,会被频繁地调度和切换,在这种知道思想下,产生了线程的概念
引入进程与线程的目的对比
- 操作系统中引入进程的目的是为了使多个进程并发执行,以改善资源使用率和提高系统效率
- 操作系统中再引入线程,则是为了减少程序并发执行时所付出的时空开销,使得并发粒度更细、并发性更好
多线程结构的进程
- 线程是进程的组成部分,每个进程内允许包含多个并发执行的实体(控制流)
- 线程作为处理器调度和分派的一级单位
- 线程的状态(运行态、就绪态、阻塞态)
线程组成
- 线程唯一标识符及线程状态信息
- 未运行时保存线程的上下文,可把线程看成是进程中一个独立的程序计数器在操作
- 核心栈,核心态下工作时,保存参数
- 用于存放线程局部变量及用户栈的私有存储区
并发多线程程序设计的优点
- 快速线程切换
- 减少(系统)管理开销
- (线程)通信易于实现
- 便于共享资源
- 并行程度提高
用户级线程 vs. 内核级线程
处理器调度
处理器调度算法衡量指标
- 吞吐量:每单位时间完成的进程数目
- 周转时间TT(Turnaround Time):每个进程从提出请求到运行完成时间
- **响应时间(Response Time):**从提出请求到第一次回应的时间
- **等待时间(Waiting Time):**每个进程在就绪队列中等待的时间
处理器调度算法
FCFS(先来先服务)
就像字面意思
RR(时间片轮转)
时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法。每个进程被分配一时间段,称作它的时间片,即该进程允许运行的时间
SPN(最短进程优先)
预计执行时间最短的进程优先
SRT(最短剩余时间优先)
预计最短剩余执行时间优先
HHRF(最高相应比优先)
响应比=(等待时间+执行时间)/执行时间
Feedback(多级反馈调度)
传统Unix系统的调度(例)
- 多级反馈队列,每个优先级队列使用时间片轮转
- 每秒重新计算每个进程的优先级
- 给每个进程赋予最基本优先级的目的是把所有进程划分成固定的优先级区
- 可控调节因子
Unix SVR4调度算法(例)
- 多级反馈队列,每一个优先数都对应一个就绪进程队列
- 实时优先级层次:优先级和时间片都是固定的,在抢占点时执行抢占
- 分时优先级层次:优先数和时间片时可变的,从0优先数的100ms到59优先数的10ms
第三章 存储管理
寻址、分段与分页
寻址
进程在寻址方面的需求
重定位
地址转换与存储保护
程序的编译、链接、装入和执行
分段
模块化程序设计的分段结构
程序的用户视图
分段(例)
分段的重定位
分页
- 主存被划分为大小固定相等的块,且块相对比较小,每个进程也被分成同样大小的小块
- 进程中称为页(page)的块可以指定到内存中称为页框(frame)或者页框的可用块
- 仅有一个简单的基址寄存器是不够的,操作系统需要为每个进程维护一个页表(page table)
- 页表给出了该进程的每一页对应的页框的位置
- 每个逻辑地址包括一个页号和在页中的偏移量
指定进程页到空闲页框
分页的重定位
分段与分页的比较
- 分段是信息的逻辑单位,由源程序的逻辑结构所决定,用户可见
- 段长可根据用户需要来规定,段起始地址可从任何主存地址开始
- 分段方式中,源程序(段号、段内位移)经连结装配后地址仍保持二维结构
- 分页是信息的物理单位,与源程序的逻辑结构无关,用户不可见
- 页长由系统确定,页面只能以页大小的整倍数地地址开始
- 分页方式中,源程序(页号、页内位移)经连结装配后地址变成了一维结构
虚拟存储管理
虚拟存储概念
请求分页虚存地址转换过程
缺页中断的处理流程
页尺寸
多级页表
背景
现代计算机普遍支持$2^{32}$~$2^{64}$容量的逻辑地址空间,采用分页存储管理时,页表相当大。以Windows为例,其运行的Intel x86平台具有32位地址,规定页面4KB($2^{12}$)时,那么4GB的逻辑地址空间由$2^{20}$个页组成,若每个页表项占用4个字节,则需要占用4MB连续主存空间存放页表。系统中有许多进程,因此页表存储开销很大
概念
- 系统为每个进程建一张页目录表,它的每个表项对应一个页表页,而页表页的每个表项给出了页面和页框的对应关系,页目录表是一级页表,页表页是二级页表
- 逻辑地址由三部分组成:页目录、页表页和位移
两级页表
多级页表地址转换过程
反置页表
- 页表设计的一个重要缺陷是页表的大小与虚拟地址空间的大小成正比
- 在反向页表方法中,虚拟地址的页号部分使用一个简单散列函数映射到哈希表中。哈希表包含一个指向反向表的指针,而反向表中含有页表项
- 通过这个结构,哈希表和反向表中只有一项对应于一个实存页(面向实存),而不是虚拟页(面向虚存)
反置页表的结构
- 页号:虚拟地址页号部分
- 进程标志符:使用该页的进程。页号和进程标志符结合起来标志一个特定的进程的虚拟地址空间的一页
- 控制位:该域包含一些标记,比如有效、访问和修改,以及保护和锁定的信息
- 链指针:如果某个项没有链项,则该域为空(允许用一个单独的位来表示)
反置页表地址转换过程
逻辑地址给出进程标识和页号,用它们去比较IPT,若整个反置页表中未能找到匹配的页表项,说明该页不在主存,产生缺页中断,请求系统调入;否则,该表项的序号便是页框号,块号加上位移,便形成物理地址
虚拟分页的操作系统软件
- 读取策略
- 放置策略
- 替换策略
- 驻留集策略
- 清除策略
页面替换算法
最佳页面置换算法 OPT算法
最佳页面置换算法是Belady于1966年提出的一种理论上的算法。是一种保证最少的缺页率的理想化算法。
算法描述
输入页面号引用串:
- 如果页框中的某个页面P以后永不使用,则该页面为淘汰页面Pt。
- 如果每个P都会再次被访问,那么其中最长未来时间内不再被访问的页面为淘汰页面Pt。
先进先出页面置换算法 FIFO算法
算法描述
优先淘汰最早进入内存的页面,亦即在内存中驻留时间最久的页面。
Belady异常
所谓Belady现象是指:采用FIFO算法时,如果对一个进程未分配它所要求的全部页面,有时就会出现分配的页面数增多但缺页率反而提高的异常现象。
第二次机会算法 SCR算法
第二次机会算法的基本思想是与FIFO相同的,但是有所改进,避免把经常使用的页面置换出去。
算法描述
- 设置一个访问位
- 当淘汰一个页面时,要检查其访问位:若访问位是1,给它第二次机会,选择下一个FIFO页面,并将其访问位置为0;若访问位是0,则淘汰它
- 另外,访问到访问位为0的页面,将其访问位重新置为1
Clock页面置换算法
算法描述
- 当某一页首次装入内存中时,则将该页框的使用位设置为1;当该页随后被访问到时(在访问产生缺页中断之后),它的使用位也会被设置为1
- 置换条件是当前使用位为0;当一页被置换时,该指针被设置成指向缓冲区中的下一页框(不修改下一页框的使用位)
- 当需要置换一页时,操作系统扫描缓冲区,以查找使用位被置为0的一页框。每当遇到一个使用位为1的页框时,操作系统就将该位重新置为0
- 如果在这个过程开始时,缓冲区中所有页框的使用位均为0时,则选择遇到的第一个页框置换
- 如果所有页框的使用位均为1时,则指针在缓冲区中完整地循环一周,把所有使用位都置为0,并且停留在最初的位置上,置换该页框中的页
其他博主见解
每一次进行替换指针的位置就从替换数移到下一个位置;每一次进行访问时,则指针保持不动 (原文链接: https://blog.csdn.net/springtostring/article/details/85331177 )
概括说就是,替换指针后移一位(循环),不替换指针不动。
流程图
注(个人理解,不喜勿喷,欢迎指教hhh)
Clock算法看上去和SCR算法很像,但它们还是有区别的,其中一点区别在于:clock算法做得是循环扫描,淘汰的是下一个遇到的使用位为0的页;而SCR算法是FIFO算法的改进版,淘汰的是最先进入的访问位为0的页。
改进型的Clock算法
改进型的Clock算法需要综合考虑某一内存页面的访问位和修改位来判断是否置换该页面 设访问位为A、修改位为M
1类(A=0,M=0):表示该页最近既未被访问、又未被修改,是最佳淘汰页。
2类(A=0,M=1):表示该页最近未被访问,但已被修改,并不是很好的淘汰页。
3类(A=1,M=0):最近已被访问,但未被修改,该页有可能再被访问。
4类(A=1,M=1):最近已被访问且被修改,该页有可能再被访问。
算法描述
- 从指针所指示的当前位置开始,扫描循环队列,寻找A=0且M=0的第一类页面,将所遇到的第一个页面作为所选中的淘汰页。在第一次扫描期间不改变访问位A。
- 如果第一步失败,即查找一周后未遇到第一类页面,则开始第二轮扫描,寻找A=0且M=1的第二类页面,将所遇到的第一个这类页面作为淘汰页。在第二轮扫描期间,将所有经过的页面的访问位置0。
- 如果第二步也失败,即未找到第二类页面,则将指针返回到开始的位置,并将所有的访问位复0。(此时已无3类、4类页框)然后,重复第一步,如果仍失败,必要时再重复第二步,此时就一定能够找到被淘汰的页。
流程图
最近最少使用算法 LRU算法
算法概述
思路是,发生缺页中断时,选择未使用时间最长的页面置换出去
局部最佳页面替换算法 MIN算法
算法描述
- 滑动窗口(τ):间隔(t,t + τ)即从当前页,到第τ个页,被称为滑动窗口(实际长度为τ+1)
- 无论发生缺页与否,算法在每一步都要考虑引用串:如果(驻留集中的)此页面在时间间隔(t,t + τ)(t为当前时刻)内未被再次引用,那么就将该页面移出;否则保留该页面到再次被引用
工作集置换算法 WS算法
P.J.Denning提出工作集(Working Set replacement, WS)模型,用来对局部最佳页面替换算法进行模拟实现,也使用滑动窗口概念,但并不向前查看页面引用串,而是基于程序局部性原理向后看,在任何给定时刻,一个进程不久的将来所需内存页框数可通过考察其最近时间内的内存需求做出估计。
算法描述
- 进程工作集:在某一段时间间隔内进程运行所需访问的页面集合
- 简单说就是与MIN算法相反,滑动窗口为(t - τ,t);如果(工作集中的)此页面在时间间隔(t - τ,t)(t为当前时刻)未被使用过,那么就将该页面移出;否则保留该页面到再次被引用或下次移出
基本替换算法的比较
第四章 设备管理
I/O控制方式
- 程序控制I/O(轮询方式)
- 处理器代表给I/O模块发送一个I/O命令,该进程进入忙式等待(busy-waiting),等待操作的完成,然后才可以继续操作
- 中断驱动I/O
- 处理器代表进程向I/O模块发出一个I/O命令,然后继续执行后续指令,当I/O模块完成工作后,处理器被该模块中断
- 如果该进程不需要等待I/O模块完成,则后续指令可以仍是该进程的指令;否则,该进程在这个中断上挂起,处理器执行其他工作
- 直接存储器访问(DMA)
- 一个DMA模块控制主存和I/O模块之间的数据交换。为传送一块数据,处理器给DMA模块发请求,只有当整个数据块传送结束后,处理器才被中断
磁盘
磁盘结构
- 一个磁道分为固定多个扇区
- 磁盘物理块地址:(柱面号、磁头号、扇区号)
- 磁盘读写方式:移臂$\rightarrow$旋转$\rightarrow$读写
磁盘调度策略
扫描(SCAN)
- 要求磁头臂仅仅沿一个方向移动,并在途中满足所有未完成的请求,直到它达到这个方向上的最后一个磁道或者在这个方向上没有其他请求为止(后一种改进有时候称为LOOK策略)
- 接着反转服务反向,沿着相反方向扫描,同样按顺序完成所有请求
虚拟设备
- 使用一类物理设备模拟另一类物理设备的技术
- 通常是使用共享型外围设备模拟独占型外围设备
- 脱机同时外围设备操作
SPOOLing
- “井”是用作缓冲的存储区域,采用井的技术能调节供求之间的矛盾,消除人工干预带来的损失
- “预输入程序”
- 操作系统将一批作业从输入设备上预先输入到磁盘的输入缓冲区中暂时保存,这称为“预输入”,此后,由作业调度程序调度作业执行,作业使用数据时不必再启动输入设备,只要从磁盘的输入缓冲区中读入
- “缓输出程序”
- 作业执行中不必直接启动输出设备,只要将作业的输出数据暂时保存到磁盘的输出缓冲区,当作业执行完毕后,由操作系统组织信息成批输出
- “井管理程序”
作业调度与进程调度的关系
I/O系统各层软件及其功能
第五章 文件管理
文件管理的要素
索引
例题
文件系统内部结构
目录项、inode和数据块的关系
文件系统调用
文件的创建
// C语言格式
int fd, mode;
char *filenamep;
fd = create(filename, mode)
文件创建执行过程
- 为新文件分配索引节点和活动索引节点,并把索引节点编号与文件分量名组成新目录项,记到目录中
- 在新文件所对应的活动索引节点中置初值,如置存取权限i_mode,连接计数i_link等
- 分配用户打开文件表项和系统打开文件表项,表项初值、读写位移f_offset清“0”
- 把各表项及文件对应的活动索引节点用指针连接起来,把文件描述字返回给调用者
文件的删除
- 删除把指定文件从所在的目录文件中除去
- 如果没有连接用户(i_link为“1”),还要把文件占用的存储空间释放。删除系统调用形式为:unlink(filenamep)
文件的打开
// C语言格式
int fd, mode;
char *filenamep;
fd = open(filenamep, mode);
文件打开执行过程
- 检索目录,把它的外存索引节点复制到活动索引节点表
- 根据参数mode核对权限,如果非法,则这次打开失败
- 当“打开”合法时,为文件分配用户打开文件表项和系统打开文件表项,并为表项设置初值。通过指针建立这些表项与活动索引节点间的联系。把文件描述字,即用户打开文件表中相应文件表项的序号返回给调用者。
文件的关闭
// C语言格式
int fd;
close(fd);
文件关闭执行过程
- 根据fd找到用户打开文件表项,再找到系统打开文件表项。释放用户打开文件表项
- 把对应系统打开文件表项中的f_count减“1”,如果非“0”,说明还有进程共享这一表项,不用释放直接返回;否则释放表项
- 把**活动索引节点中的i_count减“1”,若不为“0”,则表明还有用户进程正在使用该文件,不用释放而直接返回;否则把该活动索引节点中的内容复制回文件卷上的相应索引节点中后,释放该活动索引节点。
f_count与i_count
f_count与i_count分别反映进程动态地共享一个文件的两种方式
- f_count反映不同进程通过同一个系统打开文件表项共享一个文件地情况
- i_count反映不同进程通过不同系统打开文件表项共享一个文件的情况
- 通过两种方式,进程之间即可用相同的位移指针f_offset也可用不同位移指针共享同一个文件
文件读取
// C语言格式
int nr, fd, count;
char buf[];
nr = read(fd, buf, count);
读文件执行过程
- 系统根据f_flag中的信息,检查读操作合法性
- 再根据当前位移量f_offset值,求读出的字节数及活动索引节点中i_addr指出的文件物理块存放地址,把相应的物理块读到缓冲区中,然后再送到bufp指向的用户主存区中
读操作时文件数据结构的关系
文件写入
// C语言格式
nw = write(fd, buf, count);
// buf是信息传送的源地址,即把buf所指向的用户主存区中的信息,写入到文件中
文件的随机存取
若文件初次"打开"时,文件的位移量f_offset清空为零,以后的文件读写操作总是根据offset的当前值,顺序地读写文件。
为了支持文件的随机访问,提供系统调用lseek,它允许用户在读、写文件前,实现改变f_offset的指向
// C语言形式
long lseek;
long offset;
int whence, fd;
lseek(fd, offset, whence);
文件描述字fd必须指向一个用读或写方式打开的文件,
- 当whence是“0”时,则f_offset被置为offset
- 当whence是“1”时,则f_offset被置为文件当前位置加上offset
文件的动态共享
使用相同的位移指针
使用不同的位移指针
Linux虚拟文件系统
第六章 并发程序设计
进程的交互:竞争与协作
进程之间存在两种基本关系:竞争关系和协作关系
竞争关系
一个进程的执行可能影响到同其竞争资源的其他进程,如果两个进程要访问同一资源,那么,一个进程通过操作系统分配得到该资源,另一个将不得不等待
进程的互斥
**进程的互斥(mutual exclusion)**是解决进程间竞争关系(间接制约关系)的手段。进程互斥指若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源
协作关系
某些进程为完成同一任务需要分工协作,由于合作的每一个进程都是独立地以不可预知地速度推进,这就需要相互协作的进程在某些协调点上协调各自的工作。
当合作进程中的一个到达协调点后,在尚未得到其伙伴进程发来的消息或信号之前应阻塞自己,直到其他合作进程发来协调信号或消息后方被唤醒继续执行
进程的同步
**进程的同步(Synchronization)**是解决进程间协作关系(直接制约关系)的手段。进程同步指两个以上进程基于某个条件来协调它们的活动。一个进程的执行依赖于另一个协作进程的消息或信号,当一个进程没有得到来自于另一个进程的消息或信号时则需等待,直到消息或信号达到才被唤醒
进程的交互: 竞争与协作
进程互斥关系是一种特殊的进程同步关系, 即逐次使用互斥共享资源,是对进程使用 资源次序上的一种协调
信号量与PV操作
问题的提出
- TS或swap指令管理临界区,采用忙式轮询,效率低
- 关开中断管理临界区,不便交给用户程序使用
信号量的构思
一种可动态定义的软件资源:信号量
-
核心数据结构:等待进程队列
-
信号量声明:资源报到,建立队列
-
申请资源的原语:若申请不得,调用进程入队等待
-
归还资源的原语:若队列中有等待进程,需释放
-
信号量撤销:资源注销,撤销队列
记录型信号量的定义
记录型信号量:一种带数值的软资源
经典问题
PV操作解决进程互斥问题框架
semaphore s;
s = 1;
cobegin
process Pi {
···
P(s);
临界区;
V(s);
···
}
coend;
例:PV操作解决机票问题
int A[m];
semaphore s;
s = 1; // 只有相同航班的票数才是相关的临界资源,所以用一个信号量处理全部机票会影响进程并发度
cobegin
Process Pi {
int Xi;
Li:按旅客订票要求找到A[j];
P(s);
Xi = A[j];
if (Xi >= 1) {
Xi = Xi - 1;
A[j] = Xi;
V(s);
输出一张票;
}
else {
V(s);
输出票已售完;
}
goto Li; // 无条件跳转至Li标记处
}
coend;
int A[m];
semaphore s[m];
for(int j = 0; j < m; j++){
s[j] = 1;
}
cobegin
Process Pi {
int Xi;
Li:按旅客订票要求找到A[j];
P(s[j]);
Xi = A[j];
if (Xi >= 1) {
Xi = Xi - 1;
A[j] = Xi;
V(s[j]);
输出一张票;
}
else {
V(s[j]);
输出票已售完;
}
goto Li; // 无条件跳转至Li标记处
}
coend;
哲学家就餐问题
有五个哲学家围坐在一圆桌旁,桌中央有一盘通心面,每人面前有一只空盘子,每两人之间放一把叉子。每个哲学家思考、饥饿、然后吃通心面。为了吃面,每个哲学家必须获得两把叉子,且每人只能直接从自己左边或右边去取叉子
semaphore fork[5];
for (int i=0;i<5;i++)
fork[i]=1;
cobegin
process philosopher_i( ) { // i = 0,1,2,3,4
while(true) {
think( );
P(fork[i]); // 先取右手的叉子
P(fork[(i+1)%5]); // 再取左手的叉子
eat( );
V(fork[i]);
V(fork[(i+1)%5]);
}
}
coend
上述解法可能出现永远等待,有若干种办法可避免死锁:
- 至多允许四个哲学家同时取叉子 (C. A. R. Hoare方案)
- 奇数号先取左手边的叉子,偶数号先取右手边的叉子
- 每个哲学家取到手边的两把叉子才吃,否则一把叉子也不 取
PV操作解决进程同步问题
进程同步:并发进程为完成共同任务基于某个条件来协调执行先后关系而产生的写作制约关系(一个进程的执行等待来自于其他进程的消息)
解决的基本思路
- 定义一个信号量:其数值代表可用消息数
- 等待消息进程:执行P,无消息则等待
- 发出消息进程:执行V,有进程等待则释放
例:1生产者1消费者1缓冲区问题
- 生产者和消费者共享缓冲区
- 缓冲区有空位时,生产者可放入产品,否则等待
- 缓冲区有产品时,消费者可取出产品,否则等待
程序框架
解决思路
同步关系1:消费者一开始在等待产品到来,考虑设置一个信号量(等待产品);一开始无产品,初值为0
同步关系2:生产者则在等待缓冲区中有空位,也可设置一个信号量(等待缓冲区);一开始缓冲区有空位,初值为1
例:1生产者1消费者N缓冲区问题
例:N生产者N消费者N缓冲区问题
苹果橘子问题
A、B分别产生苹果、橘子,C、D分别消费苹果、橘子,共享同一缓冲区
同步关系1:有苹果
同步关系2:有橘子
同步关系3:有空位
int plate;;
semaphore sp; // 盘子里可以有几个水果
semaphore sg1; // 盘子里有橘子
semaphore sg2; // 盘子里有苹果
sp = 1; // 盘子里只能放一个水果
sg1 = 0; // 盘子里没有橘子
sg2 = 0; // 盘子里没有苹果
/* --------------------------------------------- */
process A {
L1: 削一个苹果;
P(sp);
把苹果放入plate;
v(sg2);
goto L1;
}
process B {
L2: 剥一个橘子;
P(sp);
把橘子放入plate;
v(sg1);
goto L2;
}
process C {
L3: P(sg2);
从盘子中取苹果;
V(sp);
吃苹果;
goto L3;
}
process D {
L4: P(sg1);
从盘子中取橘子;
V(sp);
吃橘子;
goto L4;
}
管程
管程概念的提出
- 管程试图抽象相关并发进程对共享变量的访问,以提供一个友善的并发程序设计开发环境(信号量机制的不足:程序编写困难、易出错)
- 管程是由若干公共变量及其说明和所有访问这些变量的过程所组成
- 管程把分散在各个进程中互斥地访问公共变量的那些临界区集中起来管理,管程的局部变量只能由该管程的过程存取
- 进程只能互持地调用管程中的过程
管程的定义
管程是由局部于自己的若干公共变量及其说明和所有访问这些公共变量的过程所组成的软件模块
管程的属性
管程的形式
type 管程名=monitor{
局部遍历说明;
条件变量说明;
初始化语句;
define 管程内定义的,管程外可调用的过程或函数名列表;
use 管城外定义的,管程内将调用的过程或函数名列表;
过程名/函数名(形式参数列表) {
<过程/函数体>;
}
···
过程名/函数名(形式参数列表) {
<过程/函数体>;
}
}
管程的条件变量
- 条件变量(condition variables):当调用管程过程的进程无法运行时,用于阻塞进程的信号量
- 同步原语wait:当一个管程过程发现无法继续时(如发现没有可用资源时),它再某些条件变量上执行wait,这个动作引起调用进程阻塞
- 同步原语signal:用于释放在条件变量上阻塞的进程
管程的执行模型
管程过程执行中signal的处理问题
- 当使用signal释放一个等待进程时,可能出现两个进程同时停留在管程内。解决办法:
- 执行signal的进程等待,直到被释放进程退出管程或等待另一个条件
- 被释放进程等待,直到执行signal的进程退出管程或等待另一个条件
- 霍尔采用了第一种办法
- 汉森选择了两者的这种,规定管程过程所执行的signal操作是过程体的最后一个操作
霍尔管程
实现方法
- 使用signal释放一个等待进程时,霍尔管程让执行signal的进程等待,直到被释放进程退出管程或等待另一个条件
- 霍尔管程基于PV操作原语实现
- Wait和signal可以是程序过程
- 可以用语言机制实现霍尔管程
互斥调用霍尔管程的信号量
TYPE interf = RECORD
mutex: semaphore; // 调用管程前使用的互持信号量
next: semaphore; // 发出signal的进程挂起自己的信号量
next_count: integer;// 在next上等待的进程数
END;
mutex = 1; next = 0; next_count = 0; // 初始化语句
互持调用霍尔管程的框架
P(IM.mutex);
<过程体>;
if IM.next_count > 0 then
V(IM.next);
else
V(IM.mutex);
霍尔管程的条件变量
x_sem: semaphore; // 与资源相关的信号量
x_count: integer; // 在x_sem上等待的进程数
霍尔管程的wait过程
procedure wait(var x_sem: semaphore, var x_count: integer, var IM: interf);
begin
x_count := x_count + 1;
if IM.next_count > 0 then V(IM.next);
else V(IM.mutex);
P(x_sem);
x_count := x_count - 1;
end;
霍尔管程的signal过程
procedure signal(var x_sem: semaphore, var x_count: integer, var IM: interf);
begin
if x_count > 0 then begin
IM.next_count := IM.next_count + 1;
V(x_sem);
P(IM.next); // 进入等待调用管程的队列
IM.next_count := IM.next_count - 1;
end;
end;
例
读者写者问题
有两组并发进程:读者与写者,共享一个文件,要求:
- 允许多个读者同时执行读操作
- 任一写者出现后,新的读者与写者均被拒绝
- 写者在完成写操作之前不允许其他读者和写者工作
- 写者欲工作,要等待已存在的读者完成读操作
关键:
- 等待读的进程与计数;等待写的进程与计数
- 正在读的进程技术;正在等待写的进程计数
代码:
进程通信(消息传递)
- 当进程互相交互时,必须满足两个基本要求:同步和通信
- 为实施互斥,进程间需要同步
- 为了协作,进程间需要交换信息
- 消息传递提供了这些功能,最典型的消息传递原语
- send发送消息的原语
- receive接受消息的原语
直接通信
- 对称直接寻址,发送进程和接受进程必须命名对方以便通信,原语 send() 和 receive() 定义如下:
- send(P, message) 发送消息到进程P
- receive(Q, message) 接受来自进程Q的消息
- 非对称直接寻址,只要发送者命名接收者,而接收者不需要命名发送者,send() 和 receive() 定义如下:
- send(P, message) 发送消息到进程P
- receive(id, message) 接受来自任何进程的消息,变量id置成与其通信的进程名称
(虽然是直接通信,但也需要通过内核传递而不是P、Q点对点)
消息格式
间接通信
- 消息不是直接从发送者发送到接收者,而是发送到由临时保存这些消息的队列组成的一个共享数据结构,这些队列通常称为信箱(mailbox)
- 一个进程给合适的信箱发送消息,另一进程从信箱中获得消息
- 间接通信的send() 和 receive() 定义如下:
- send(A, message):把一封信件(消息)传送到信箱A
- send(B, message):从信箱A接收一封信件(消息)
信箱
- 信箱可以分成信箱头和信箱体两部分:
- 信箱头指出信箱容量、信件格式、存放信件位置的指针等
- 信箱体用来存放信件,信箱体分成若干个区,每个区可容纳一封信
- “发送”和“接受”两条原语的功能为
- 发送信件:如果指定的信箱未满,则将信件送入信箱中由指针所示的位置,并释放等待该信箱中信件的等待着;否则,发送信件者被置成等待信箱状态
- 接受信件:如果指定信箱中有信,则取出一封信件,并释放等待信箱的等待者;否则,接受信件者被置成等待信箱中信件的状态
send/receive原语的算法描述
消息传递求解生产者消费者问题
有关消息传递的若干问题
- 关于信箱容量问题
- 关于多进程与信箱相连的信件接受问题
- 关于信箱所有权问题
- 信箱为操作系统所有是指由操作系统统一设置信箱,归系统所有,供相互通信的进程共享,消息缓冲机制就是一个著名的例子
- 关于信件的格式问题和其他有关问题
- 关于通信进程的同步问题
死锁
死锁的定义
- 一组进程处于死锁状态是指:每一个进程都在等待被另一个进程所占有的、不能抢占的资源
死锁的产生
- 原因:允许多个进程并发执行共享系统资源时,系统必须提供同步机制和进程通信机制。然而,对这种机制使用不当的话,可能会出现进程永远被阻塞的现象。
- 两个进程分别等待对方占有的一个资源,于是两者都不能执行而处于永远等待,这种现象称为“死锁”。例如:
- 打印机与读卡机
- PV操作使用不当产生死锁。
- 同类资源分配不当引起死锁。若系统中有m个资源被n个进程共享,当每个进程都要求K个资源,而$m
- 对临时性资源使用不加限制引起死锁。在进程通信时使用的信件可以看作是一种临时性资源,如果对信件的发送和接受不加限制的话,可能会引起死锁。(循环等待信件到来才能发送信件)
死锁的解决
产生死锁的因素不仅与系统拥有的资源数量有关,而且与资源分配策略,进程对资源的使用要求以及并发进程的推进顺序有关。可以从三个方面解决死锁问题:
死锁的防止
死锁产生的四个必要条件
- 互斥条件:进程应互斥使用资源,任一时刻一个资源仅为一个进程独占
- 占有和等待条件:一个进程请求资源得不到满足而等待时,不释放已占有的资源
- 不剥夺条件:任一进程不能从另一进程那里抢夺资源
- 循环等待条件:存在一个循环等待链,每一个进程分别等待它前一个进程所持有的资源
死锁的防止
破坏四个必要条件之一,死锁就可防止。
- 破坏第一个条件,把独占性资源改造成共享性资源,使资源可同时访问而不是互斥使用。(简单但对许多资源往往不能做到)
- 采用剥夺式调度方法可以破坏第三个条件,但剥夺式调度方法目前只适用于对主存资源和处理器资源的分配,而不适用于所有资源
- 采用**静态分配(预分配)**后,进程在执行中不再申请资源,因而不会出现占有了某些资源再等待另一些资源的情况,即破坏了第二个条件
- 所谓静态分配是指一个进程必须在执行前就申请它所要的全部资源,并且直到它所要的资源都得到满足之后才开始执行。所有并发执行的进程要求的资源总和不超过系统拥有的资源数
- 采用层次分配策略将阻止第四个条件的出现。在层次分配策略下,资源被分成多个层次。
- 一个进程得到某一层的一个资源后,它只能再申请在较高层的资源
- 当一个进程要释放某层的一个资源时,必须先释放所占用的较高层的资源
- 当一个进程获得了某一层的一个资源后,它想再申请该层中的另一个资源,那么必须先释放该层中的已占资源
死锁的避免
- 当不能防止死锁的产生时,如果能掌握并发进程中与每个进程有关的资源申请情况,仍然可以避免死锁的发生
- 只需在为申请者分配资源前先测试系统状态,若把资源分配给申请者会产生死锁的话,则拒绝;否则接受申请,为它分配资源
银行家算法
银行家算法:借钱给有偿还能力的客户
- 系统首先检查申请者对资源的最大需求量,如果现存的资源可以满足它的最大需求量时,就满足当前的申请
- 换言之,仅仅在申请者可能无条件地归还它所申请的全部资源时,才分配资源给它
例
假设系统有三个进程P、Q、R,系统只有一类资源共10个,目前分配情况如下:
进程 |
已占资源 |
还需要申请数 |
P |
4 |
4 |
Q |
2 |
2 |
Q |
2 |
2 |
P或者R再申请资源时,不能分配,因为现在只剩下两个资源,不能满足它们的最大需求
安全状态
安全状态是指系统能按照某种进程顺序,来为每一个进程分配其所需要的资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利地完成。
若系统无法找到这样一个安全序列,则称系统处于不安全状态
死锁的检测
解决死锁问题的另一条途径时死锁检测方法。这种方法对资源的分配不加限制,但系统定时运行一个“死锁检测”程序,判断系统内是否已出现死锁,若检测到死锁则设法加以解除。
检测的一种方法
死锁检测的数据结构
把两张表格中记录的进程使用和等待资源的情况用一个矩阵A来表示
死锁检测的算法
死锁检测程序可用Warshall的传递闭包算法检测是否有死锁发生,对矩阵A构造传递闭包$A^*[b_]$
$A^*[b_]$中的每个$b_$是对$A[b_]$执行如下算法
for k:=1 to n do
for i:=1 to n do
for j:=1 to do
bij:= bij | (bik & bkj)
死锁检测后的解决办法
- 可以采用重新启动进程执行的办法,恢复工作应包含重启动一个或全部进程,以及从哪一个点开始重新启动
- 全部卷入死锁从头开始启动,但这样的代价是相当大的
- 在进程执行过程中定时设置校验点,从校验点开始重新执行
- 中止一个卷入死锁的进程,以后重执行