class 1.导论
电脑系统可以被分为四个部分:硬件,操作系统,应用程序,用户
什么是操作系统?
两种观点:
对非计算机专业人员
操作系统是购买操作系统时商家提供的所有程序
是安装操作系统是安装到裸机上的所有程序
从系统层面上讲:
操作系统是管理所有的资源,在有效和公平地使用资源的相互冲突的请求之间做出决定。
总之,操作系统是:1.应用程序的基础2.管理电脑硬件3. 充当计算机用户和计算机硬件之间的中介
电脑系统组织
电脑启动(startup)一般分为四个阶段:
- 加电后执行BIOS中的固化程序
- BIOS装入并执行主硬盘的主引导记录(MBR)
- MBR查找装入活动(引导)分区的引导扇区
- 活动分区的引导扇区加载OS内核
中断(interrupt):
因为在学习操作系统之前已经学过机组,所以先从机组的角度解释一下中断。
I/O交换数据的方式大体有三种:程序查询方式,程序中断方式,DMA方式
程序查询方式:是CPU和I/O设备串行执行,CPU每时每刻都在循环查询I/O设备是否准备就绪,当外设未准备好时,CPU就只能循环等待,不能执行其它程序,这种方式中,由于CPU和I/O设备的速率存在很大的差距,所以CPU会浪费大量时间处于空转等待,严重浪费了CPU资源。
程序中断方式:为了解决查询方式的不足,我们提出了中断传送方式。当CPU进行主程序操作时,I/O数据已存入输入端口的数据寄存器;或端口的数据输出寄存器已空,由外设通过接口电路向CPU发出中断请求信号,CPU在满足一定的条件下,暂停执行当前正在执行的主程序,转入执行相应能够进行输入/输出操作的子程序,待输入/输出操作执行完毕之后CPU即返回继续执行原来被中断的主程序。CPU和设备在此情况下并行执行。
DMA方式:外设可通过DMA控制器向CPU发出DMA请求,CPU响应DMA请求,系统转变为DMA工作方式,并把总线控制权交给DMA控制器,由DMA控制器发送存储器地址,执行DMA传送,DMA操作结束,并把总线控制权交还CPU。
设备控制器通过引发中断来通知 CPU 它已完成操作。
分为三步:
- 设备控制器中的中断模块提出中断请求
- CPU响应并处理I/O的中断请求
- 当中断完成后,设备控制器中的中断模块通知CPU,中断已处理完成
硬件中断:
CPU响应中断后,执行OS提供的中断处理程序
软件中断:
分为system call(系统调用)和exception(Unix)
系统调用指软件通过执行一个特殊的操作叫系统调用来触发一个中断。
Trap(陷入):
是由error(错误)或user request(用户请求)引起的软件生成的中断
中断处理:
操作系统通过存储寄存器和程序计数器来保存CPU的状态。
机组中中断处理过程:请求中断→响应中断→关闭中断→保留断点→中断源识别→保护现场→中断服务子程序→恢复现场→中断返回。
操作系统是中断驱动的。
如果没有程序执行,没有I/O设备服务,也没有用户响应,那么一个OS将等待一些事件发生。
存储结构:
主存(main memory)、二级存储(secondary storage)、磁盘
对比存储方式时,从speed,cost,volatility三个角度分析。
多处理器系统:
多个CPU共享内存等系统资源(总线,时钟信号,内存,外围设备)。
三个优点:
- 增加吞吐量(更少时间完成更多工作)
- 节约规模(成本低于同等的多个单处理器系统)
- 增加可靠性(一个处理器的失败不会终止系统)
操作系统结构:
多道程序系统:(为了效率)
单个用户无法让 CPU 和 I/O 设备始终处于繁忙状态
多道程序设计可以组织作业(jobs)(代码和数据),因此 CPU 始终有一个作业可以执行
系统中总作业的子集保存在内存中
通过作业调度选择并运行一项作业
分时系统:利用多道程序与多任务处理使多个用户可以同时使用一台计算机。
正确答案:
解析:由于CPU 始终有一个作业可以执行,所以CPU利用率和系统吞吐量大,I/O设备利用率高,但是系统要付出额外的开销来组织作业和切换作业,所以系统开销大。
计算机中的有些操作,可能影响到操作系统与应用程序的正常执行:
I/O操作
修改系统有关的特殊寄存器
内存相关
其他(访问权限等)
安全保护:
操作系统和用户共享计算机系统的硬件和软件资源。
操作系统必须确保错误(或恶意)的用户程序不会导致操作系统和其他程序错误执行。
包括以下三点:
1.无限循环/进程占用资源(CPU保护)
2.进程相互修改或操作系统(内存保护)
3.用户直接发出I/O指令或执行非法I/O(I/O保护)
如何保证?
privileged instructions(特权指令)
- 将CPU指令系统中可能对操作系统或者其他程序引起的损害指令定义为特权指令
- 不允许在用户程序直接执行这些特权指令
- 只有OS才可以执行这些特权指令
特权指令举例:I/0指令Timer management、Interrupt management、Memory management
关于特权指令的问题:
如何保证特权指令不能再用户程序中执行?
——OS无法实现,应该由CPU完成
CPU提供了两种运行模式:用户模式(user mode)和内核(kernel mode)(核心)模式(用户态和核心态)
在用户模式下,不能执行特权指令;在核心模式下,可以执行所有指令
OS借助这两种运行模式,是用户程序在用户模式中执行,系统代码在核心模式下执行。
如果用户态中执行了特权指令,硬件负责检测,然后陷入到OS内核中进行错误处理
CPU如何区分目前执行的是用户程序还是OS代码?
——在设计CPU的时候,应该限定特权指令只能在一定条件下执行
——OS可以执行所有指令,用户程序不能执行特权指令
如何让用户程序完成某些特权指令所完成的操作?
——用户委托OS完成(system call系统调用)
(系统调用只能在核心态运行)
双模式操作:
硬件提供的模式位
PSW:内核(0)或用户(1)
必须确保用户程序永远无法在监视模式下获得对计算机的控制
class 2: 操作系统服务
OS为用户提供的服务:
程序执行(load run end)
I/O操作
文件系统操作:
读写文件和目录
创建和删除文件和目录
查找文件和目录
列出文件信息
权限管理
交流
错误检测
OS为自身提供的服务:
资源分配
会计(accounting):追踪哪些用户使用了计算机资源和使用了多少
保护和安全
用户通过OS提供的接口使用这些服务:
Command interface:(命令接口)
- CLI(command-line)
- GUI(Graphic user interface)
- Batch
Programming interface:(程序接口)
System calls
API
CLI:(在Unix中被称为shell)——允许直接输入命令
GUI:用户友好性的图形页面,许多系统现在都是CLI和GUI结合使用
Batch:批处理文件是一种简单的程序,可以通过条件语句(if)和流程控制语句(goto)来控制命令运行的流程,在批处理中也可以使用循环语句(for)来循环执行一系列命令。
System calls:
用户程序和操作系统核心之间的接口(class1中讲到,用户想要使用特权指令,需要通过系统调用陷入核心态)
好处:可以保护对关键共享硬件的访问免受错误用户的影响。
API:(Application Program Interface)
程序员可以使用的一组函数
程序大多通过高级 API 访问,而不是直接使用系统调用
API vs. System Calls
Why use APIs rather than system calls?
因为系统调用写起来比较难,(但是在一些场景更有效)
API是通过调用相应的System call来实现的
e.g. C函数printf()的实现过程?
C程序先调用printf()库函数,在库函数中有一条write(),这条这令需要陷入核心态(因为涉及到磁盘的读写),CPU将执行模式从用户态转为核心态,通过系统调用执行完毕后再返回到printf()函数中,返回用户态。
系统调用的执行:
通常,每个系统调用有一个关联的号码
系统调用接口维护一个根据这些数字索引的表(类似于中断向量)
系统调用接口调用操作系统内核中的预期系统调用并返回系统调用的状态和任何返回值
调用者不需要知道系统调用是如何实现的
根据系统调用号,查找系统调用向量表,转到对应的系统调用处理程序
系统调用参数传递:
用于向OS传递参数的三种通用方法:
1、最简单:在寄存器中传递参数
在某些情况下,参数可能多于寄存器
2、由程序将参数放入或压入堆栈并由操作系统从堆栈中弹出
3、参数存储在内存中的块或表中,块的地址作为参数传递到寄存器中(Linux 和 Solaris 采用了这种方法)
块和堆栈方法不限制传递参数的数量或长度
系统结构:
简单结构:
MS-DOS System Structure——旨在以最小的空间提供最多的功能
Layered Approach(分层方法):
操作系统分为多个层(级别),每个层都构建在较低层之上。 最底层(第0层),是硬件; 最高层(N层)是用户界面。
通过模块化,层的选择使得每个层仅使用较低层的功能(操作)和服务
微内核系统结构:
好处:(extend、port、reliable、secure)巧记:【pres】
- 易于拓展微内核
- 易于将OS移植到新架构上
- 更可靠(在核心态中运行的代码更少)
- 更安全
不好:
1. 用户空间到内核空间通信的性能开销
2. 设计困难:操作系统设计者必须决定操作系统的哪些部分需要完整的硬件权限,哪些部分不需要
Modules:
和层次结构类似,但是更灵活一点
Virtual Machines:
虚拟机(Virtual Machine)是指可以像真实机器一样运行程序的计算机的软件实现,是通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。
提供一个虚拟层,将一套真实的硬件系统虚拟成多套硬件系统(相当于提供了多套硬件系统)
在每套虚拟硬件上可以安装不同的操作系统
虚拟机采用分层方法。
它将硬件和操作系统内核视为所有硬件
虚拟机提供与底层裸硬件相同的接口
操作系统创建了多个进程的假象,每个进程都在自己的处理器上执行,并拥有自己的(虚拟)内存
共享物理计算机的资源来创建虚拟机
CPU调度可以创建用户拥有自己的处理器的外观
假脱机和文件系统可以提供虚拟读卡器和虚拟行式打印机
普通用户分时终端作为虚拟机操作员控制台
Class 3:Processes(进程)
进程的概念:
从用户角度,OS的任务之一是运行用户程序。
OS要做到以下三点:
- 多路复用
——多任务,资源共享
——多个程序可并发执行,多处理器环境下多个程序可并行执行,共享CPU、内存等资源
p.s.关于并发和并行的概念:
并发是指多个任务交替使用CPU,每个时刻只有一个任务在占据CPU
并行是指同一时刻多个任务在执行
看到知乎上有一个评论:
你一边吃饭,来了个电话,你吃完以后才接,这是既不支持并发也不支持并行
你一边吃饭,来了个电话,你先接电话再吃饭,支持并发
你一边吃饭,来了个电话,你一边接电话一边吃饭,支持并行
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
- 隔离
——目的:一个程序的运行不能影响OS,以及其他运行的程序
——实施:让运行的程序感觉整个系统只有自己在运行,自己拥有一台私有的计算机系统
——每个程序要有自己独立的运行空间,其他程序不能访问(write or read)
——每个程序要有独立的CPU,不能互相干扰
- 交互
e.g.一个程序在系统中的运行过程
要执行一个程序,需要涉及到众多的问题
需要申请,使用系统资源
如果程序运用时所需的资源目前不可用,程序就无法继续执行,执行过程需要暂停,等资源可用后继续恢复执行
CPU调度
结束后回收已分配的资源
进程之间的交互通信
同一个程序的多次运行
所以,需要有一个实体来描述一个运行中的程序
进程:
进程的本质是一个运行中的程序(顾名思义),是一个程序执行的实例
当一个程序被OS装入到内存中运行时,就成为一个进程
系统运行程序的时候,为其所分配的内存一般包括如下几个部分:
代码区、常量区、全局(静态)变量存储区、堆、栈、命令行参数区
#其中全局(静态)存储区再被分为:
已初始化的数据区
未初始化的数据区(BSS)
#X86中的堆栈:
堆:向上生长(低地址->高地址)
低地址 |
高地址 |
栈:向下生长(高地址->低地址)
#堆栈溢出:DevCpp默认堆栈大小为2MB
再来看看进程的逻辑空间与运行时布局:
在本学期操作系统实验中涉及到了pmap指令:
我们来从pmap指令分析一下进程的逻辑空间的布局:
我自己实现了一个pmap,大概是简单调用了一下/proc/进程号/smaps文件的内容
想了解pmap的同学可以参考CSDN上这篇文章:
https://blog.csdn.net/lijzheng/article/details/23618365
分析截图:
可以看到第一行是属性名称(不管它)前三行是pm(我的测试进程名,其进程号为3366)的映射,第四行是堆,然后四行是libc-2.27.so的库映射,后面三行是ld-2.27.so的映射(先不管空行),后面是栈。先看pm这三行,第一行权限为r-xp,是只读可执行段,推测应该是一个代码段,第二行是只读段(不可执行),应该是rodata区(存放常量和全局变量const等),第三行读写允许段,推测应是rwdata,存放程序运行时可以修改的全局变量。大概这样分析吧……(#^#再多也不会了)
Ps.关于可执行程序:
(这个老师最后一节课随堂测试了,鄙人并不会…)
对于以下程序:
include <stdio.h>
float f;
int main()
{
int x=10;
printf("Hello world\n");
return 0;
}
进程的组成(重点):
*包含程序代码的文本部分
*包含全局变量和静态变量的数据部分
*包含运行时动态分配的内存的堆
*包含临时数据的栈(局部变量、函数参数、函数返回地址)
*当前活动——包括程序指针(PC)、堆栈指针(SP)、处理器寄存器等。
*PCB(process control block,相当于进程的档案,OS->PCB->进程的任何信息)
OS如何感知一个进程的存在?(通过PCB)
一个程序是否能够多次属于同一个进程?
一个程序不能多次次属于同一个进程(如同人不能两次踏进同一条河流一样)
理解:1.一个程序可被多次执行;
2. 每次运行都是一个可执行程序不同的运行活动—OS为其创建的进程也不会相同;
3. 即使每次运行的程序(代码段)相同,但处理的数据(数据段)可能不同;
4. 即使每次运行的程序(代码段)相同,处理的数据(数据段)也相同,但OS为其分配PCB、进程号、堆、栈等系统分配资源肯定不同;
进程的特征(重点):
进程具有五个特征,而程序则不具备
- 动态性
- 独立性
- 并发性
- 异步性
- 结构特征
-
动态性(active):
动态性是进程最基本的特征,表现在进程由创建而产生,由调度而执行,因得不到资源而暂停执行,由撤销而消亡。
进程具有一定的生命周期。(这个生命周期后面会进一步理解)
2.独立性
进程实体是一个能独立运作的基本单位,同时也是系统中独立获得资源和独立调度的基本单位。
在不支持多线程的系统中,进程是CPU独立调度的基本单位。
进程需要一定的资源,已完成其所负担的任务
如CPU time,memory,files,I/O devices
进程拥有资源,因此在上下文切换时,耗时比较大,导致运行效率不高。
如果将进程能够独立获得资源和独立调度这两个属性分开
支持线程的系统中,进程是独立获得资源的基本单位,线程是独立调度的进本单位。
3.并发性(concurrency)
静态的程序没有并发的概念。
运行中的程序,即进程,才可以在系统内并发执行
多个进程能在一段时间内同时(并发)执行
4.异步性
指进程按各自独立的、不可预知的速度向前推进
OS中必须采取措施保证个程序之间协调运行
5.结构特征
从结构上看,进程实体由程序段、数据段、堆、栈、PCB等元素构成。
有的资料将程序段、数据段、PCB成为进程影响
进程状态:
New:进程正在被创建
Ready:进程正在等待被分配给处理器
Running:命令正在被执行
Waiting:进程正在等待一些事件发生
Terminated:进程已经完成执行(仅保留进程的进程表项和pid,其他资源被操作系统收回,不参与进程调度)
经典的进程状态转化图:
UNIX:zombie,dead 仍然存在,等待父进程或系统将其收回,其后才真正消亡。 running->ready可以有以下三种方式: |
|
关于new:
这个进程处于new状态说明它还没准备好运行,因为:
栈还未被分配
控制块还没被创建
PC还没有初值
总而言之就是程序所运行的信息还不完整,尚无法执行。
在Nachos中,当调用fork()后,进程状态便有just created变成ready
前文中提到了操作系统如何感知一个进程?
通过PCB(说句题外话:进程队列,实际上是进程所对应的PCB队列)
那么如何找到PCB呢?类比文件目录表。
PCB的信息组织:
Process state(程序状态):如new,ready这些
Program counter(程序计数器):下一次指令执行的位置
CPU registers(CPU寄存器):所有以进程为中心的寄存器的内容,当中断发生时必须保存该状态信息,以允许进程随后正确地继续。
CPU scheduling information:优先级,调度队列指针
Memory-management information
Accounting information:使用的 CPU、自启动以来经过的时钟时间、时间限制
I/O status information:分配给进程的 I/O 设备、打开文件列表
进程调度:
作业队列——系统中所有进程的集合
当进程进入系统时,它们被放入作业队列中,通常还没有在主内存中
就绪队列——驻留在主内存中的所有进程的集合,准备好并等待执行(等待CPU)
设备队列——等待 I/O 设备的进程集
进程在各个队列之间迁移
调度:
短期调度(CPU调度)
长期调度(作业调度)
#中期调度(交换)
短期调度(CPU调度):
选择那些进程应该被下一个执行和分配CPU
短期调度被频繁调用(执行速度很快)
控制CPU的使用和系统吞吐量
长期调度(作业调度):
通常被用在批处理系统
选择哪个进程应该被加载到内存并被送入就绪队列
长期调度被不频繁使用(执行速度慢)
长期调度控制多道程序的程度(内存中程序的数量)
短期调度和长期调度类比包子铺:
假设有一家包子铺十分火爆,里边现包现卖,铺面里边有一个窗口,十个人先把票递进去,给要的少的或者已经熟了的馅的顾客先给包子,由于人口众多,店铺外也有人在排队。我们将这个铺面看作是内存,进入到里边需要长期调度(将顾客送到里边),进入后给包子是CPU调度,给要的少的或者已经熟了的馅的顾客先给包子可以看做是调度算法,包子可以看做是CPU使用权。
Context switch(上下文切换):
上下文究竟是什么?可以理解为是一个进程运行时的环境。包括每个进程执行过的、执行时的以及待执行的指令和数据;在指令寄存器、堆栈、状态字寄存器等中的内容。此外, 还包括进程打开的文件描述符等.
将CPU切换到另一个进程需要执行当前进程的状态保存和不同进程的状态恢复。
当发生上下文切换时,内核将旧进程的状态保存到其PCB中,然后从新进程的PCB中恢复先前保存的上下文。(PCB中储存了进程的上下文)
进程创建:
0号进程 :
系统引导时创建0号进程(系统中唯一一个不需要通过fork()创建的进程)
然后0号进程利用系统调用fork()进程创建1号进程
0进程就变成对换进程(swaper);
1号进程:
1号进程是系统中除0号进程以外其它进程的祖先进程(init)
用户登录系统后,该进程为用户创建相应的用户进程
该用户进程依次创建该用户的其它进程
一个进程可以创建众多的子进程
父进程何如协调和子进程之间的资源共享、地址空间、执行过程之类的问题?
Resource sharing:
子进程继承父进程在子进程创建之前父进程所拥有的所有资源
子进程创建之后,父子进程开始资源分离(先继承后分离)
Address space:
内核为子进程做一个父进程的上下文的拷贝;
PCB,代码,数据,栈,PCB中的信息:打开的文件,当前工作目录, PC,SP,…..
父进程和子进程在不同的地址空间上运行;
子进程可以装入并执行新的程序(可以通过exec()系统调用实现。)
Execution:
父子进程开始并发执行
通常父进程需要等待其子进程结束
Fork():
内核为系统调用fork()完成的操作:(capp-context,address space,pid,pcb)
- 为子进程在进程表中分配一个空项(PCB)
- 为子进程赋予一个唯一的进程标志号(PID)
- 为子进程分配独立的地址空间
——将父进程的PCB、数据、栈、PC等内容到子进程的相应地址空间中
——对于代码,子进程可能调用exec()装入新的程序而覆盖父进程的代码,因此有的系统不真正将父进程的代码复制到一个新的内存物理区,只是增加该区的引用数即可。
- 复制父进程的上下文到子进程的地址空间
对父进程返回子进程的进程号,对子进程返回零。
子进程继承了父进程的PC、SP,尽管父子进程PC、SP的值相同(相对地址),父子进程访问的是不同地址空间中的区域,因此,父子进程在各自的地址空间中,均从fork()之后的语句开始执行。
Fork()的几个要点:
父子进程具有独立的地址空间。
父子进程资源的共享和分离
父进程中在fork之前创建的变量——继承
PCB:除进程号以外的所有内容
变量:父进程所拥有的全局、局部变量(说明栈也被继承了)等
文件描述符:父进程打开的文件、设备描述符
缓冲区:如c中printf()语句所使用的输出缓存
Fork之后各自创建的分量——分离
PCB、栈、堆:各自新增或修改的内容,独立
变量:各自新定义或修改,独立
文件描述符:存储文件描述符的变量各自独立,对文件的操作也完全独立
缓冲区:如输出缓存
系统调用Wait()
一个进程执行结束后,会进入“terminated”状态,并未被真正销毁
(前面提到过,此时只剩下进程表项,含有进程的退出码,即pid等资源
Unix中,将terminated状态成为zombie状态
当子进程先于父进程执结束后进入僵死状态,此时的子进程被称为zombie)
父进程负责回收僵死子进程的进程信息和资源
父进程通过系统调用wait或waitpid回收其僵死子进程
现在的Unix版本,如果子进程进入僵死状态,但父进程未回收,那么当父进程执行结束退出后,系统会负责回收
孤儿进程:
如果父进程先于子进程执行结束,没有等待子进程结束而先行退出,子进程将成为孤儿进程。(这种状态的原因是父进程没有执行wait等待子进程或父进程执行错误被强行退出)
那么怎么处理这些孤儿进程?
将孤儿进程归属到1号进程(init)
现在的操作系统是将归属到登录的用户进程
Fork()继承I/O(重点)
fork()系统调中,内核为子进程做一个父进程上下文的拷贝
其中包括PCB,复制父进程的PCB作为子进程的PCB
PCB中包含进程打开的文件
重点还是先继承,后分离。
e.g. fd1=open(“/etc/passwd”,O_RDONLY);
fd1为文件描述符(Unix),在windows中被称作文件句柄(handler)
内核为打开的文件维护的3个数据结构
- 进程级(进程私有的)的文件描述符表
前三个是系统预留
0:标准输入
1:标准输出
2:错误输出
- 系统级(全局共享的)的(打开)文件表
内核对所有打开的文件维护一个系统级的表格
一个打开文件句柄中给出了一个打开文件相关的全部信息,主要包括:
1. 当前文件读写偏移量(调用read()和write()时更新,或使用lseek()直接修改)
2. 打开文件时所使用的状态标识(即open()的flags参数)
3. 文件访问模式(如调用open()时所设置的只读模式、只写模式或读写模式等)
4. 与信号驱动相关的设置
5. 对该文件i-node对象的引用
6. 文件类型(例如:常规文件、套接字或FIFO)和访问权限
7. 一个指针,指向该文件所持有的锁列表
8. 文件的各种属性,包括文件大小以及与不同类型操作相关的时间戳
3. 系统级(全局共享的)的(文件系统)i-node表(FCB,UNIX中称为i-node)
系统级的打开文件索引节点表,又称为活动的索引节点表(活动表);
打开一个文件时,将在磁盘上的文件的索引节点装入到内核维护的索引节点表中;
同时设置上一些访问文件时的一些控制信息,如引用数,表示一个文件目前被打开的次数;
进程间通信:
共享内存;
消息队列
信号量;
共享内存:
利用系统调用shmget()创建一个共享存储区
共享内存区域:
应该同时属于要共享该存储区的所有进程的地址空间,这些进程才能访问,才能共享。否则,会导致地址越界。(共享区本来不属于任何一个进程)
具体实现:
利用系统调用shmat()将共享区附接到进程A的地址空间上,成为进程A的地址空间的一部分; (shmat意为shmattach)
进程A即可访问(读写)该共享存储区
进程B不能访问该共享存储区
附接之后,每个进程可得到共享存储区的地址,可以视为C语言中的指针类变量的存储区域
共享完毕后,需要删除该共享存储区
删除之前,需要将共享存储区从进程的地址空间中剥离,才能删除
系统调用shmdt()将共享存储区从进程的地址空间中剥离;(dt意为detach)
利用系统调用shmctl()删除共享存储区
总结:先创建后附接,先剥离再删除
Class 4:threads
我们之前提过,进程是独立申请资源的单位,线程是资源调度的基本单位。
从图中可得到:
隶属关系
线程是进程的运行实体,一个进程至少需要一个线程,可以拥有多个线程,这些线程运行在所属进程的上下文中。
线程隶属于进程,线程不能脱离进程而独立存在。
一个线程能且只能隶属于一个进程。
拥有资源
进程是拥有资源的基本单位,隶属于同一个进程的多个线程共享该进程的代码,数据,堆,打开的文件(包括标准设备)等I/O资源
这些线程不能共享进程(主线程)的栈(存放的局部函数、函数参数、函数返回地址等)、寄存器(包括中断信息等)等资源
因此线程只拥有其运行的所必需的的资源,如寄存器、栈等。
Why?
栈和寄存器组是定义程序执行的动态上下文的组件。
栈随着函数的调用和返回而增长和收缩,并且寄存器的内容在每条指令执行后都会发生变化。
CPU调度与分派的基本单位
线程是进程的实体,线程运行在其所属进程的上下文中。
线程是CPU调度与分派的进本单位(CPU调度)
同一进程中的多个线程可以并发执行。
为什么引入线程的概念?
OS中引入进程是为了多个程序并发执行,提高资源利用率。提高系统的吞吐量。
OS中引入线程是:
隶属于同一个进程的多个线程可以分担进程的任务,并可发执行,缩短了进程任务的执行时间
减少进程并发执行时所付出的时空开销(如上下文切换),使OS具有更好的并发性
进一步提高了资源的利用率
为使进程能并发执行,系统还必须提供一些相应的操作:
创建进程、撤销进程、进程切换
而进程是一个资源的拥有者,创建进程、撤销进程、上下文切换都要消耗很大的时空开销,但如果将进程作为独立申请资源基本单位与CPU调度和分派的基本单位这两个基本属性分开,即1.拥有资源的不频繁调度2.频繁调度的基本不拥有资源,这样就产生了线程,让进程拥有资源,线程参与调度。
线程实体:
程序、数据、TCB(类比PCB,threads control blocks)
Thread vs. Process
调度:
同一进程中,线程的切换不会引起进程切换;
不同进程中的线程之间的切换要引起进程的切换;
并发性:
引入线程的系统中,同一进程中的多个线之间可并发执行,使系统具有更好的并发性,进一步提高了资源的利用率及系统的吞吐量;
拥有资源:
进程是拥有资源的独立单位
线程仅拥有比不可少的资源,可以访问其隶属进程的资源;
系统开销:
系统创建及撤销进程时的开销远远大于创建及撤销线程时的开销;
进程切换时的开销也远远大于线程切换时的开销;
线程优势:(RREU)
Responsiveness(响应性):
——即使程序的某些部分被阻止或正在执行冗长的操作,也允许程序继续运行
Resource Sharing(共享资源):
——线程共享其所属进程的内存和资源
Economy(经济):
——为进程创建分配内存和资源是恒定的,上下文切换是相同的
Utilization of MP Architectures(充分利用多处理器):
——单线程进程只能运行在一个CPU上,而属于一个进程的线程可以运行在不同的CPU上。
多线程模型:
用户级线程和内核级线程的映射
有四种模式:多对一,一对一,多对多,two level
用户级线程的运行需要映射到相应的核心线程才能完成。
用户线程:
用户线程仅存在于用户空间中
用户线程的创建、撤销、线程之间的同步与通信等功能,都无需利用系统调用来实现,而是通过用户级线程来实现。
对于用户级线程的切换,通常是发生在一个应用进程的诸多线程之间,无需内核的支持,也是通过用户级线程库来管理的。
由于切换的规则远比进程调度和切换的规则简单,因而使线程的切换速度特别快。
总结:用户线程的管理与内核无关(包括创建撤销同步通信切换等),不需要通过系统调用来完成。
用户级线程是OS内核不可感知的,需要将用户线程映射到核心线程,由核心线程控制用户线程的进行(一个用户线程必须要映射到一个核心线程,才能在CPU上执行)。
核心线程:
如果系统支持线程,核心线程的管理由内核完成。
线程的创建撤销和切换,都是依靠内核来实现
需要直接或间接通过系统调用来完成
类比PCB,在内核空间中也有一个TCB,内核是根据该控制块感知某线程的存在的。
内核支持线程是OS内核可感知的,用户级线程内核是不可感知的。
OS只为核心线程分配CPU等资源。
四种模式:
- 多对一(many to one)
一次只有一个线程可以访问内核
多个线程无法在多处理器上并行运行
如果线程进行阻塞系统调用,整个进程将阻塞
- 一对一(one to one)
提供比多对一模型更多的并发性
当线程进行阻塞系统调用时允许另一个线程运行
允许多个线程在多处理器上并行运行
创建用户线程需要创建对应的内核线程
创建内核线程的开销可能会降低应用程序的性能
- 多对多(many to many)
将许多用户级线程多路复用到更少或相同数量的内核线程
开发人员可以根据需要创建任意数量的用户线程
允许多个线程在多处理器上并行运行
- two-level model
相当于. In a bank,many custmers, many staffs,and a lot of staffs for VIPs.
几种映射模型的比较:
Class 5:CPU scheduling
基本概念:
调度是操作系统的一项基本功能
通过在进程之间切换 CPU,操作系统可以提高计算机的工作效率。
CPU 调度是操作系统设计的核心。
CPU调度也可以被叫做(进程调度,线程调度)
CPU-I/O burst cycle:
进程的行为和属性:大部分进程,其CPU执行区间与I/O执行区间是交替进行的。
CPU执行区间与I/O执行区间有时成为CPU执行期与I/O执行期。
进程执行以 CPU 周期开始,然后是 I/O 周期,……最后的 CPU 周期以终止执行的系统请求结束。每当一个进程必须等待时(I/O周期,CPU 变得空闲),另一个进程就可以接管 CPU 的使用。
还有一点需要指出:根据大量的统计结果表明,short CPU burst 的数量很大,long CPU burst的数量很少,那么将如何利用OS设计合适的进程调度算法?
CPU调度:
当CPU空闲时,操作系统必须从就绪队列中选择一个进程执行。
选择过程由短期调度程序(CPU调度)执行。
就绪队列可以有:
FIFO队列(先进先出)
优先级队列
树
或者只是一个无序列表
就绪队列中的记录通常是进程的进程控制块(PCB)。
抢先、非抢先式调度(preemptive & nonpreemptive):
非抢先式调度:
分派程序一旦把处理机分配给某进程后便让它一直运行下去,直到进程执行结束后(或出错),或者进程等待某事件而被阻塞时,才能把处理机分配给另一个进程。
抢先式调度:
当进程或线程正在处理机上运行时,系统可根据所规定的原则剥夺分配给此进程的处理器的执行权,将其移入就绪队列中,选择调度其他进程运行。
一个进程的CPU burst被分割成多个执行段
根据CPU执行期,理解抢先 vs. 非抢先式调度
非抢先式:如果一种CPU调度方式尽在一个进程执行完其一个CPU执行期时才引起进程调度,则这种调度方式属于非抢先式调度
抢先式:如果一种CPU调度方式将一个CPU执行期分割成多个CPU执行期,那么这种调度方式就属于抢先式调度。
Dispatcher(调度程序):
调度程序模块将CPU的控制权交给短期调度程序(CPU调度程序)选择的进程
该功能涉及以下内容:
切换上下文(在内核模式下)
切换到用户模式
跳转到用户程序中的正确位置以重新启动该程序
调度延迟 – 调度程序停止一个进程并启动另一进程运行所需的时间
调度标准:
- CPU利用率:保持CPU尽可能繁忙
- 吞吐量:每个单位时间程序要完成的数量
- 周转时间:流程提交到完成的时间间隔(等待进入内存、在就绪队列中等待、在 CPU 上执行以及执行 I/O 所花费的时间总和)
周转时间=进程创建时间-进程结束时间
- 等待时间:进程在就绪队列中等待的时间
CPU 调度算法不影响进程执行或进行 I/O 的时间量;
它仅影响进程在就绪队列中等待的时间量
进程的结束时间-到达时间-执行时间-I/O时间
- 响应时间:从提交请求到生成第一个响应所花费的时间,而不是输出响应所花费的时间
调度算法:
FCFS(先来先服务)
SJF(最短作业优先)
优先级
RR(轮转法)
多级队列
多级响应队列
FCFS:
属于非抢先式调度算法
有利于长作业,对短作业不利,系统吞吐量小。
该算法通常作为其它调度算法的基本(基础)算法。
SJF:
每个进程与其下一个 CPU burst的长度相关联。
使用这些长度以最短的时间安排流程。
有两种方案:
非抢占式(SJF):
系统吞吐量大,平均周转时间短
有利于短作业,不利于长作业
如果采用抢先式调度,下一个CPU周期的长度无法精确获得,只能采用预测的方法。
会导致starvation(饥饿)的发生——长CPU burst进程可能永远不会被执行。
抢占式(SRTF)
——如果新进程到达时 CPU burst长度小于当前执行进程的剩余时间,则抢占。 该方案称为最短剩余时间优先 (SRTF)
SRTF调度算法优先调度就绪队列中(剩余)执行时间最短的进程,每个进程的等待时间最短,平均等待时间也就最短。
优先级调度:
每个进程都有一个优先级编号(整型)
CPU被分配给具有最高优先级的进程(最小整数最高优先级(这个可以规定))
优先级可以定义为
内部
使用一些可测量的数量来计算优先级:时间限制、内存要求、打开文件的数量以及平均 I/O 突发与平均 CPU 突发的比率
外部
由操作系统之外的标准设定,例如流程的重要性、为计算机使用支付的资金类型和金额、赞助工作的部门以及其他通常是政治因素。
优先级调度可以是
抢占式
非抢占式:
当一个新的进程进入就绪队列,将正在运行的进程的优先级与该新进入就绪队列进程的优先级进行比较,以确定是否能够抢先(和SRTF一样,如果新的进程比当前执行的优先级更大,可以抢占,先执行新的)
Problem:
饥饿——低优先级进程可能永远不会执行
解决方案: 老化 – 随着时间的推移,流程的优先级会增加
高响应度比调度算法:
定义:优先级(响应比)=(等待时间+要求服务时间)/要求服务时间=(等待世家/要求服务时间)+1
理解:等待时间越长,防止饥饿,优先级应该增加,所以放在分子上;要求服务时间越大,为了照顾短作业,优先级应该越小,所以应该放在分母
每次调度的时候,系统重新计算各作业或进程的响应比,选择响应比高的进程优先执行。
对于响应比相同的进程,按照FCFS调度
可以采用抢先/非抢先调度
该调度算法防止SJF中长进程出现饥饿现象
该算法即照顾了短作业,又考虑了作业到达的先后次序,也不会使长作业长期得不到服务。
RR(轮转):
对于分时系统
就绪队列使用FCFS
每个进程获得一个小的CPU时间单位(时间量子),通常是10-100毫秒。
经过该时间后,该进程将被抢占并添加到就绪队列的末尾。
如果就绪队列中有 n 个进程,并且时间量为 q,则每个进程一次最多获得 q 个时间单位的 1/n 的 CPU 时间。 没有进程等待超过 (n-1)q 时间单位。
Performance :
q 大 ——FIFO
q 小 —— q 相对于上下文切换必须大,否则开销太高
简单来说,就是每个进程一轮只能执行q个时间单位,剩下没执行完的部分下一轮继续执行。
e.g.:
Time Quantum = 4,都在0时刻到达(ready queue按照FCFS)
观察得出,当一个进程在小于时隙的时间内完成了,不用等到该时隙结束,直接可以下一个时隙。
- 时间片越长,平均周转时间或等待时间越短?
- 时间片越长,上下文切换开销越小
- 根据统计结果,大部分的进程的cpu执行期都比较短
多级队列:
将就绪队列依照进程的类型将其分成多个队列,每个队列设置相应的优先级。
就绪队列被划分为单独的队列:
foreground (interactive)
background (batch)
每个队列都有自己的调度算法:
foreground – RR
background – FCFS
调度必须在队列之间进行
固定优先级调度; (即,从前台服务所有内容,然后从后台服务)。—— 有饥饿的可能性。
时间片 – 每个队列获得一定量的 CPU 时间,可以在其进程之间进行调度
80% to foreground in RR
20% to background in FCFS
简单来说:
将进程按类型分成不同的队列
每个队列有不同的优先级
每个队列有不同的调度算法
当高优先级队列为空,才调度低优先级队列中的进程
多级响应队列:
一个进程可以在各个队列之间移动;(老化可以通过这种方式实现)
多级反馈队列调度程序由以下参数定义:
队列数
每个队列的调度算法
用于确定何时升级进程的方法
用于确定何时降级进程的方法
用于确定当进程需要服务时该进程将进入哪个队列的方法
当一个进程执行完一个时间片,但尚未结束,则降级进入低级队列
新创建进程进入最高优先级队列
可以防止低优先级队列中的进程出现饥饿(starvation)现象—aging
Three queues:
Q0 – RR with time quantum 8 milliseconds
Q1 – RR time quantum 16 milliseconds
Q2 – FCFS
多级反馈队列三要素:多级队列、不同的调度算法、反馈
线程调度:
本地调度 – 线程库如何决定将哪个线程放入可用的 LWP
用户级线程最终必须映射到关联的内核级线程。
实质上是选择用户线程与核心线程的映射顺序
全局调度——内核如何决定接下来运行哪个内核线程
调度核心线程获得CPU的执行权
Class6: Process Synchronization(进程同步)
背景:
互斥:
并发进程需要共享一些资源,这些资源不能同时访问。
同步:
线程并发执行中有时需要互相协调,互相协作,这个时候就需要同步。(相对于异步)
临界区,临界资源:
1.临界资源是指某一段时间只能提供给一个进程使用的资源,如设备,内存,表格等。 2.临界区指的是进程使用临界资源的那段代码。
3.临界区和临界资源是相对的。具体到进程,临界资源可以理解为临界区的概念。也就是说我们对进程考虑的时候,只需要考虑临界区即可。
4. 临界资源要求互斥地共享,或互斥地访问
5. 即要保证访问临界资源的原子性
互斥:
当多个进程或线程并发访问共享数据的时候,可能导致数据的不一致性。
多发生于多个进程几乎同时访问临界资源的时候。
临界区问题的解决方案:
1. 互斥--忙则等待,保证临界区互斥访问)
2. 前进--有空让进,当无进程在临界区执行时,若有进程进入应允许;否则可能会出现“饥饿”现象
3. 有限等待--当一个进程申请进入临界区,应限制其它进程进入临界区的次数,以便申请的进程有机会进入临界区)(otherwise starvation maybe occurs)
互斥的实现方法:
-
软件方法(Peterson’s sulution)
进程可以共享一些公共变量来同步它们的操作。
Peterson’s Solution Algorithm 1:
变量turn相当于一个门票或token,由两进程轮流使用;
获得令牌,进入;没有获得令牌,等待;
退出时,移交令牌;
满足互斥和有限等待,但不满足前进
两进程轮流地访问临界资源
Peterson’s Solution Algorithm 2:
flag相当于门口的一个登记簿,当进程进门时登记,然后查看其它进程是否已经登记,如果其它进程已经登记,则等待;
退出时清除登记信息。
满足互斥,但不满足前进。(当两进程几乎同时到达,Pi与Pi可能会在各自的while循环中无穷循环、无限等待;(互相谦让))
Peterson’s Solution Algorithm 3:
登记+令牌
在门口登记(欲访问资源),并把令牌移交给另一个进程
退出时消除登记信息;
如果双方都已经登记,但拥有令牌的进程会进入;(前进)
如果对方尚未登记,但拥有令牌,自己会进入;(前进)
如果对方已经登记,且拥有令牌,对方进入,自己等待;(互斥)
进入时令该令牌转交(有限等待)
满足三个条件
-
硬件方法
所有解决方案都基于lock的思想,通过锁保护关键区域
Lock机制:
acquire lock:
需要测试lock的当前状态 (test)
如果lock当前处于开锁状态,则关锁(set) ,然后访问临界区;
否则等待,直至锁被打开,然后关锁并继续执行;
release lock:开锁;
单处理机,关中断。(中断屏蔽)
TestAndSet Instruction
Swap Instruction
-
信号量
信号量s是整型数,只能执行初始化和PV操作。
p操作:
即wait()操作;
wait(){
while(s<=0){
busy wait;//忙等
}s--;
}
v操作:
即signal操作;
signal(){
s++;
}
可以这样理解,s代表当前设备数量,wait操作执行的是等待设备数大于0时,将s--,此事s--相当于给设备上锁。signal操作执行的是当设备使用完毕,开锁的一个状态,s++相当于开锁。
但是这种busy wait的形式很消耗cpu资源,而且适用于多核CPU,不适用于单核。为了改进,我们有了新的方法:
创建一个这样的semaphore结构体:有两个部分,一个是value,另一个是一个PCB队列。
实现的过程大概是:
当有一个新的进程要使用临界资源,它先执行wait(),先s--,如果设备数小于,那么说明此时没有空闲设备,那么区分于PV操作,它不会进行忙等,而是选择将此进程加入PCB队列,然后自己挂起(blocked)。
当一个进程执行完毕,将执行signal操作,先释放资源s++,如果s小于等于0,说明此时PCB队列里还有进程。那么此时他将唤醒一个进程。
当理解了这个过程,会意识到这个wait和signal是多么精准的描述。
信号量及两个操作的物理含义:
S.value的初值代表系统中某类资源的数目,因而信号量又被称为资源信号量。
当S.value<0,其绝对值代表在信号量链表队列S.L的长度,即在该信号量下等待的进程数;
每次wait操作,意味着进程申请一个单位的资源,表示为S.value:=S.value-1;
每次signal操作,意味着进程释放一个单位的资源,表示为S.value:=S.value+1;
e.g. 假设现在只有一台打印机,三个进程都想使用。Value初值为1;第一个进程P1先来,执行wait操作,value--=0,进程不用阻塞,占据打印机正常执行。这时,P2来了,执行wait操作,value--=-1,小于零说明此时没有空闲设备,且绝对值表示等待队列应该有一个进程 就是P2。P2加入pcb队列,且主动阻塞。这时第一个进程P1执行完毕,执行signal释放资源,value++=0,唤醒pcb队列中的P2,然后P2占据打印机执行,执行完毕后释放资源,signal操作,value++=1;此时P3来了,执行wait操作,value--=0,可以执行,占据打印机资源执行。
信号量的应用-前趋关系
前趋图:
节点是进程所对应的需要协调的操作(语句)
边是进程之间的依赖关系
对应每条边设置一个信号量
算法描述:
有几条边就建立几个信号量,对每条边都这样分析
对于某一结点,
如果有入边,需要在协调的操作前边加上一个wait()操作,其中的信号量与其前驱中signal()使用的相同;
wait()操作的个数与其入边的数目相同;
如果有出边,需要在协调的操作后面加上一个signal()操作,其中的信号量与其后继中wait()使用的相同;
signal()操作的个数与其出边的数目相同;
生产者-消费者问题(P-C):
相当于缓存区的拿放问题。
先抽象这个基本问题:
对于p(生产者),在往缓存区放东西的时候,有两种可能:
1.满buffer,等待(c取走后唤醒)
2.空buffer,放(唤醒c)
对于c(消费者),在从缓存区取东西的时候,有两种可能:
1.满buffer,取(唤醒p)
2.空buffer,等待(p放后唤醒)
首先确定p,c的操作:
p:
while(1){
放东西;
}
c:
while(1){
取东西;
}
然后在受到限制的语句前放置p语句:(也能理解,要先wait一下)
在受限制后的语句放v语句。
p:
while(1){
p();
放东西;
v();
}
c:
while(1){
p();
取东西;
v();
}
然后确定信号量。
p:
while(1){
p(empty);
放东西;
v(full);
}
c:
while(1){
p(full);
取东西;
v(empty);
}
确定信号量的大小时,有这样的方法:
将p或c孤立出来,然后看p操作能执行几次,这个例子中p操作能执行buffer大小b那么大,设buffer大小为n,那么empty为n,full为0(最开始为0);
以后对于解决生产着消费者之类的问题,可以采取这样的流程,确定代码。
一个生产者进程,一个消费者进程,共享N个缓冲区:
Shared data:
Int in,out;
Semaphore:
Empty=N,full=0;
In=0,out=0;
多个生产者多个消费者共享N个缓冲区
Semaphore mutex1=1, mutex2=1, empty=N ,full=0;
int in=0, out=0;
多了两个信号量,mutex1和mutex2这两个互斥信号量,为了保证多个进程互斥访问临界资源in和out;
为简化P-C模型的实现,将缓冲池视为一个临界资源;
生产者与消费者均互斥访问缓冲池;
生产者之间、消费者之间也互斥访问缓冲池;
发现减少了一个信号量,mutex1和mutex2合并成一个信号量mutex,也就是说现在是生产者与消费者之间互斥访问,生产者之间和消费者之间也互相访问。
这里需要注意到一个很重要的问题:一定要同步在前,互斥在后。否则有死锁的可能。
(假设互斥在前,同步在后。消费者先占据buffer,发现buffer是满的,这时他得等待消费者取走,但是此时消费者由于互斥没办法访问buffer,所以造成了死锁。(hold and wait))
e.g.
中间进门后的V操作和出门前的V操作能不能去掉?(看似是冗余)
不可以。这时一个并发粒度的问题。如果去掉以后,相当于包场了,即这个人进去以后别人不能进去,等到他出来才能进去。我们要保证并发粒度最大化。
读者写者问题:
问题描述:
前提:
reader只能read,不能写。
writer既能read也能write;
我们需要保证:
多读者可以同时read,但必须与写者互斥;但某一时间只能允许一个写者读或写。
抽象为以下几点:
1.有一个共享对象,允许多个reader同时访问,但必须与writer互斥;writer之间也必须互斥;
reader之间不需互斥
reader与writer之间互斥
writer之间互斥
分析:
对于写者,只要互斥即可。
对于读者,其中两个读者比较重要,第一个进入的读者与最后一个离开的读者。
第一个进入的读者,应该拒绝写者,但不能拒绝其他读者。
最后一个离开的读者,应该释放对象,如果需要的话,还要唤醒等待的写者。
Shared data:
Readcount=0;
Semaphore:
Mutex =1;
Wrt=1;
但该实现存在问题:
Readcount不互斥。
- readcount的计数不正确
- 若目前有写者正在访问,第一个到来的读者会等待,但后续的读者仍然可以访问该对象。
改进:
对readcount加一个互斥信号量:
对第一个问题的解决,很显然。
对第二个问题的解决:如果写者正在写,那么第一个读者到wait(wrt)的时候被阻塞了,当时他占据了mutex资源,那么mutex资源得不到释放,所以后面的第二个第三个读者都进不来。
完美解决!
- 读者等待队列
- 第一个读者,在信号量wrt的队列中等待
- 第二个,以及后续的读者,在信号量mutex的等待队列
该方案存在什么问题?
Problem: 该方案读者优先,可能导致写者出现“饥饿”现象;
Solution:当有写者到来时,该写者阻止后续的读者访问该对象;(写者优先,后面课件中,添加一个信号量就能实现)
写者优先的reader算法:
主要和读者优先的reader算法不同的是,要设置一个信号量,当有写者到来时,封锁后续的读者。
Shared data:
Int readcount=0;
Semorphore:
wrt=1:写者之间互斥,写者与读者之间互斥
rmutex=1:读者之间对计数器readcount的互斥
w=1:实现写者优先
哲学家问题:
Shared data:
Bowl of rice (data set)
Semaphore chopstick [5] initialized to 1
The structure of Philosopher i:
但是会造成死锁;
如何解决?
- 允许最多4个哲学家同时吃饭,第5个等待
Semaphore seat=4,chopstick[i]=1 (i=0..4);
- 只有当一个哲学家同时拿起两只筷子才能吃饭;即只有两只筷子同时空闲,才能吃饭
(之后的破环死锁条件的之hold and wait中的wait不hold)
管程实现(monitor)之后详细介绍(参考实验中的代码)(重点)
- 奇数哲学家:先左后右;偶数哲学家:先右后左
Semaphore chopstick[i]=1 (i=0..4);
- 1~4号哲学家先左后右,最后一个哲学家先右后左
Semaphore chopstick[i]=1 (i=0..4);
e.g.:
Solution:
但其实不必太关注行为,比如叫号和为顾客服务可以看做连起来的行为:
理发师问题:(重点)
简单问题:
某理发店有一个接待室和一个理发室组成。理发室中有一把理发椅,接待室中有N把椅子。
若没有顾客等待理发,则理发师睡眠等待。
当一个顾客到达理发店后,若发现座位已满,则顾客等待;若发现理发师忙而接待室中有空座位,顾客则坐在椅子上等待;若发现理发师正在睡眠,则将理发师唤醒。
试写一个程序协调理发师和顾客之间的活动。
Semaphore:
chairs=N;
customers=0;
barberReady=0;
原问题:
某理发店有一个接待室和一个理发室组成。理发室中有一把理发椅,接待室中有N把椅子。若没有顾客等待理发,则理发师睡眠等待。当一个顾客到达理发店后,若发现座位已满,则选择离开;若发现理发师忙而接待室中有空座位,顾客则坐在椅子上等待;若发现理发师正在睡眠,则将理发师唤醒。试写一个程序协调理发师和顾客之间的活动。
对于“选择离开”,需要设置一个共享变量,用来记录当前接待室里有多少个人;
Shared data:
Chairs;//椅子的个数
Waiting=0;//记录等待服务的顾客数
Semaphore:
Customers=0;
BarberReady=0;
WaitingMutex=1;//实现waiting 的互斥访问
吸烟者问题:
简单吸烟者问题:
有三个抽烟者坐在桌子边,每个抽烟者不断地卷烟并抽烟。
抽烟者卷起并抽掉一只烟需要有三种材料:烟草(T)、纸(P)和火柴(M)。
三个吸烟者中,每个吸烟者有两种材料,各自缺少烟草、纸和火柴。
假设吸烟者A缺烟草(T),吸烟者B缺纸(P),吸烟者C缺火柴(M)
有一个供应者,无限地供应所有三种材料,但每次仅提供三种材料中的一种。
供应者将提供的一种材料放到桌子上
得到缺失一种材料的抽烟者在卷起并抽掉一颗烟后会发信号通知供应者,让它继续提供一种材料。
在材料被相应的吸烟者取走之前,不允许供应者供应新的材料。
这一过程重复进行。
Semaphore:
Table=1;
T=0,P=0,M=0;
doneSmoking=0;
(先同步再互斥)
二级吸烟者:
有三个抽烟者坐在桌子边,每个抽烟者不断地卷烟并抽烟。
抽烟者卷起并抽掉一只烟需要有三种材料:烟草、纸和火柴。
三个吸烟者中,每个吸烟者有两种材料,各自缺少烟草、纸和火柴。
有两个供应者,无限地供应所有三种材料中的一种,但每个供应者每次仅提供三种材料中的一种。
每个供应者将提供的一种材料放到桌子上
得到缺失一种材料的抽烟者在卷起并抽掉一颗烟后会发信号通知供应者,让它继续提供一种材料。
在材料被相应的吸烟者取走之前,不允许供应者供应新的材料。
这一过程重复进行。
Semaphore:
Table=1;
Agent=1;
T=0,P=0,M=0;
DoneSmoking=0;
(两个供应者之间要互斥)
原始问题:
有三个抽烟者,每个抽烟者不断地卷烟并抽烟。
抽烟者卷起并抽掉一只烟需要有三种材料:烟草、纸和火柴。
三个吸烟者中,每个吸烟者只有一种材料,即一个抽烟者有烟草,一个有纸,另一个有火柴。
有一个供应者,无限地供应三种材料,但每次仅提供三种材料中的两种。
得到缺失的两种材料的抽烟者在卷起并抽掉一颗烟后会发信号通知供应者,让它继续提供三种材料中的两种材料。
在两种材料被相应的吸烟者取走之前,不允许供应者供应新的材料。
这一过程重复进行。
Semaphore:
Table=1;
doneSmoking=0;
match=0;
paper=0;
tobacco=0;
但这种方案会导致死锁;
改进:
Semaphore:
Table=1;
Tobacco_paper=0;
tobacco_matches=0;
paper_matches=0;
doneSmoking=0;
Class 7:Deadlocks
死锁的定义:
死锁是指一组处于等待(阻塞)状态的进程,每一个进程持有其它进程所需要的资源,而又等待使用其它进程所拥有的资源,致使这组进程互相等待,均无法向前推进。
三个点:一组进程(至少三个),处于等待(阻塞)状态,互相等待
一个典型的例子:
P0和P1同时执行,P0申请A资源,P1申请B资源,然后他们P0此时申请B,P1申请A,达成了互相等待的状态,此时就是死锁。
若无外界干预,死锁的进程将无法被唤醒。
死锁的原因:
竞争资源:
对一些独占性资源,如打印机,表格等,数目不满足进程的需求时,进城之间需要竞争使用这些资源,可能会产生死锁。
进程间的推进顺序不当:
进程在运行过程中,需要多种(个)资源,而请求和释放资源的顺序不当,可能会导致进程之间互相等待对方所占用的资源,从而产生死锁。
竞争资源 与 推进顺序不当:
死锁的四个特征:
Mutual exclusion(竞争)
Hold and wait:持有至少一个资源的进程正在等待获取其他进程持有的额外资源。
No preemption(不抢占):资源只能由持有该资源的进程在完成其任务后自愿释放。
Circular wait(循环等待)
只要破坏其中一个条件死锁即可被破坏。
Resource Allocation Graph(RAG):
Pi->Rj:表示进程Pi申请资源Rj的一个实例
Rj->Pi: 资源Rj的一个实例分配给进程Pi
表示进程
:表示4个实例的资源
:表示Pi申请资源Rj的一个实例
p.s.根据设备独立性,申请该类资源的任何一个实例均可,不能申请到具体的某个实例
:资源Rj的一个实例分配给进程Pi
p.s. 因为已经实际分配,要具体到hold哪个实例
如果图中无环能说明没有死锁发生。
如果图中有环,分两种情况:
- 若每类资源均是单实例,那么死锁
- 若每类资源多实例,那么可能死锁也可能不死锁。
e.g.
解决思路:
画出RAG,给出一个死锁点
重点强调:死锁的进程一定是处于阻塞状态。
画图时,一般情况下,每个进程的操作应预留一个申请操作,以便当以后执行该申请操作时使进程进入等待状态。
Methods for Handling Deadlocks(死锁处理):
操作系统对死锁所采取的措施(三种):
Deadlock prevention:
预防。对进程对资源的使用加上诸多限制条件,以防出现死锁现象。
Deadlock avoidance:
避免。基于进程及系统的一些先验知识,当进程申请资源时,若发现满足该资源的请求可能导致死锁发生,则拒绝该申请。(先评测,再决定是否通过申请)
Deadlock detection and recovery:
先污染后治理。放任污染,当无法继续进行下去时,重开。
Deadlock prevention:
破坏四个条件之一即可。
Mutual exclusion:
必须适用于不可共享的资源。
对于打印机、互斥锁这样的独占性资源,不仅不能破坏它们的互斥特性,而且还应加以保证。
Hold and wait:
资源静态分配策略,占有不等待:
要求进程在开始执行之前请求并分配其所有资源
为进程请求资源的系统调用先于所有其他系统调用。
等待不占有:
当进程不拥有资源时才可申请资源,或进程使用完并释放一种资源后,才可以申请另一种资源。
优点:简单,易于实现且很安全
缺点:低资源利用率、饥饿、根据现在的编译器,进程直到运行时才知道他需要哪些资源。(但批处理系统可以采用,因为在用户提交的作业中列出了他们所需要的资源)
No preemption:
系统剥夺某些阻塞进程的资源,将其分配给其它等待该资源的进程
如果申请资源而未满足,则释放自己已经获得的资源。(哲学家就餐问题中,一个哲学家申请不到另一支筷子,就放下已经拿起的筷子。)
问题:当进程唤醒调度执行时,需要重新获取被剥夺的资源。适用于状态可保存及恢复的资源,如CPU寄存器,Memory etc;一般不适用于像互斥锁、信号量以及打印机这类需要互斥非共享使用的资源(独占性资源)。
Circular Wait:
可以将系统中所有的资源类型进行线性排队,并统一编号。
进程可以在任何时候提出资源请求
但是所有资源请求必须按照资源编号递增的顺序提出,不允许进程请求比当前所占有设备编号低的资源。
如果进程需要同一资源的多个实例时,需要一起申请它们。
或者,规定当进程申请某种资源类型时,如果其所占有的资源中有编号大于所请求资源的编号,应先予以释放。
如:
如果线程T1获取锁的顺序是lock1lock2。
而另一个线程T2获取锁的顺序是lock2lock1
软件witness将给出警告。
Deadlock avoidance:
通过摒弃循环等待条件来避免死锁的发生。
Resource-allocation state:
被定义为
系统可用资源数量
进程的分配资源
流程的最大要求
要求系统有一些额外的可用先验信息。
死锁避免算法动态检查资源分配状态,以确保永远不会出现循环等待情况。
安全序列:
此时的安全序列是()。
(有些题中是allocation,need,available)
选D。
因为先看可用资源,[021]只有P1的尚需资源[001]都小于[021],所以先执行P1,执行后释放已分配资源[200],则此时的可用资源变成[221],然后可以执行P4,执行P4后变成[222],再不发执行下去了。所以安全序列不存在。
银行家算法:
多个实例。
每个过程都必须事先声明最大用途。
当进程请求资源时,它可能必须等待。
当一个进程获得所有资源时,它必须在有限的时间内归还它们。
N:进程数量 m:资源类型数量
Available:可用数量。如果 available [j] = k,则有 k 个资源类型 Rj 的实例可用。
Max:如果 Max [i,j] = k,则进程 Pi 最多可以请求资源类型 Rj 的 k 个实例。
Allocation: 如果 Allocation[i,j] = k,则 Pi 当前已经分配 Rj 的 k 个实例。
Need:如果 Need[i,j] = k,则 Pi 可能还需要 k 个 Rj 实例才能完成其任务。
Need [i,j] = Max[i,j] – Allocation [i,j].
其实就是拿当前的available去匹配一个need小于当前available的进程(按照顺序),然后释放他占用的进程,更新available,再不断匹配的过程。
进程 Pi 的资源请求算法:
- 合法性检查
- 资源可用性检查
- 假分配,试探性分配
- 安全性检查
Deadlock Detection
基于RAG的死锁检验:
死锁定理:看RAG是不是可完全简化的。
对于单实例的资源图:如果存在环,那么说明一定存在死锁。
对于多实例的资源图:
Detection Algorithm
Available
Allocation
Request: 表示每个进程当前的请求
e.g. 问该时刻是否存在死锁进程?
检测方法:如果没有特别指定采用哪种方法,可以采用如下三种方法:
1、对于资源为单个实例的情况,检测是否存在死锁进程,可以画出进程等待图,然后检测是否存在圈。
2、采用死锁定理
3、采用死锁检测算法