进程的类型
按照使用资源的权限:系统进程、用户进程
按照对CPU的依赖性:偏CPU进程、偏I/O进程
为了能更好地描述程序的顺序和并发执行情况,引入用于描述程序执行先后顺序的前趋图。
前趋图是指一个有向无循环图,可记为DAG,它用于描述进程之间执行的先后顺序。
图中的每个结点可用来表示一个进程或程序段,乃至一条语句,结点间的有向边则表示两个结点之间存在的偏序(有向顺序)或前趋关系。
注意前趋图中是不允许有循环的,否则必然会产生不可能实现的前趋关系。
按照某种先后次序顺序执行,仅当前一进程段执行完后,才运行后一程序段。
顺序执行的程序举例:
程序I: 输入程序:用于输入用户的程序和数据
程序C:计算程序:对所输入的而数据进行计算
程序P:打印程序:打印计算结果。
顺序执行流程图
顺序执行特征
- 顺序性 :处理机严格地按照程序所规定的顺序执行
- 封闭性 :程序运行时独占全机资源,资源的状态(除初始状态外)只有本程序才能改变它,程序一旦开始执行,其结果不受外界影响。
- 可再现性 :只要程序执行时的环境和初始状态相同,当程序重复执行时,不论它时从头到尾不停顿地执行、还是“停停走走”地执行,都能获得相同的结果。
为什么要并发执行
程序顺序执行虽然带来方便,但系统资源的利用率却很低。为此,应该让程序或程序段能够并发执行。
程序并发执行的必要条件:不存在前趋关系的程序
对于具有下述四条语句的程序段:
S1:a=x+2
s2:b=y+4
s3:c=a+b
s4=d-c+b
可以看出:S;必须在a和lb被赋值后方能执行;S必须在S,之后执行;但S和 S则可以并发执行,因为它们彼此互不依赖。
并发执行特征
- 间断性
- 失去封闭性
- 不可再现性
例如:有两个程序 A 和 B并发执行,它们共享一个变量N
程序 A:N=N+1; 程序 B:print(N); N=0;
程序 A 和 B以不同的速度运行(失去了封装性,导致不可再现性)
----->此题产生了三种情况:A比B快、B比A快、A介于B和A之间。
(假设某时刻变量N的值为n)
A比B快:n+1, n+1, 0
B比A快:n, 0,1
A介于B和A之间:n, n+1,0
在多道程序环境下,程序的执行属于并发执行,决定了通常的程序是不能参与并发执行的,否则程序的运行也就失去了意义。
为了能使程序并发执行,并且可以对并发执行的程序加以描述和控制,引入了“进程”的概念。
由程序段、相关的数据段和PCB(进程控制块)三部分便构成了进程实体(即进程映像)。通常情况下,把进程实体就简称为进程。
进程是程序的依次执行
进程是一个程序及其数据在处理机上顺序执行时发生的活动。
进程时具有独立功能的程序在一个数据集合上运行的过程。它是系统进行资源分配和调度的一个独立单位。
进程是进程实体的运行过程,是系统进行资源分配和调度的一个独立单位。
进程的特征
进程和程序是两个截然不同的概念,除了进程具有程序所没有的PCB结构外,还具有下面一些特征:
每个进程实体中包含了程序段和数据段这两个部分,因此说进程与程序是紧密相关的。
但是从结构上看,进程实体中除了程序段和数据段外,还必须包含一个数据结构,即进程控制块PCB。
进程是程序的一次执行过程,因此是动态的;动态性还表现在进程由创建而产生、由调度而执行、由撤消而消亡,即它具有一定的生命周期。
而程序则只是一组指令的有序集合,并可永久地存放在某种介质上,其本身不具有运动的含义,因此是静态的。
多个进程实体可同时存放在内存中并发地执行,其实这正是引入进程的目的。
而程序(在没有为它创建进程时)的并发执行具有不可再现性,因此程序不能正确地并发执行。
进程是一个能够独立运行、独立分配资源和独立接受调度的基本单位。
程序(在没有为它创建进程时)不具有PCB,所以它不可能在多道程序环境下独立运行。
进程与程序不一一对应
进程的三种基本状态
一般而言,每一个进程至少应处于以下三种基本状态之一:
(1)就绪(Ready)状态。(等待CPU)
(2)执行(Running)状态。(执行进程)
(3)阻塞(Block)状态。(除了CPU还缺少一些东西
一般是I\0
)
三种基本状态的转换
创建和终止状态
(1)创建状态
首先由进程申请一个空白PCB,并向PCB中填写用于控制和管理进程的信息;
为该进程分配运行时所必须的资源;
把该进程转入就绪状态并插入就绪队列之中。
注:如果进程所需的资源尚不能得到满足,比如系统尚无足够的内存使进程无法装入其中,此时创建工作尚未完成,进程不能被调度运行,于是把此时进程所处的状态称为创建状态。
引入创建状态是为了保证进程的调度必须在创建工作完成后进行,以确保对进程控制块操作的完整性。同时创建状态的引入也增加了管理的灵活性,OS可以根据系统性能或主存容量的限制,推迟新进程的提交。
(2)终止状态
当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,它将进入终止状态。
进入终止态的进程以后不能再执行,但在操作系统中依然保留一个记录,其中保存状态码和一些计时统计数据,供其他进程收集。
一旦其他进程完成了对其信息的提取之后,操作系统将删除进程,即将其PCB清零,并将该空白PCB返还系统。
挂起操作的引入
将主存的一部分移到外存中。
引入挂起操作的原因,是基于系统和用户的如下需要:
引入挂起原语操作后三个进程状态的转换
在引入挂起原语 suspend 和 激活原语active,在它们作用下,进程将可能发生一下几种状态
原语就是一段代码,执行后就不能中断
引入挂起操作后五个进程状态的转换
引进创建和终止状态后,在进程状态转化时,要增加考虑下面几种情况:
1.操作系统中用于管理控制的数据结构
在计算机系统中,对于每个资源和每个进程都设置了一个数据结构,用于表征其实体,我们称之
为资源信息表或进程信息表(由PCB构成),包含
了资源或进程的标识、描述、状态等信息以及一批指针 。
通过这些指针,可以将同类资源或进程的信息表,或者同一进程所占用的资源信息表分类链接成不同的队列,便于操作系统进行查找。
OS管理的这些数据结构一般分为以下四类:
进程控制块,是操作系统核心中一种数据结构,主要表示进程状态。
作用是使一个在多道程序环境下不能独立运行的程序(含数据),成为一个能独立运行的基本单位或与其它进程并发执行的进程。
或者说,OS是根据PCB来对并发执行的进程进行控制和管理的。 PCB通常是系统内存占用区中的一个连续存区。
2.进程控制块PCB的作用
作为独立运行基本单位的标志
能实现间断性运行方式
提供进程管理所需要的信息
提供进程调度所需要的信息
实现与其他进程的同步与通信
3.进程控制块中的信息
外部标识符 | 内部标识符 |
---|---|
方便用户(进程)访问 | 方便系统对进程的使用 |
由字母、数字组成 | 唯一的数字标识符,其编号(数字)可以重复使用 |
创建者提供 | 操作系统设置 |
寄存器类型 | 作用 |
---|---|
通用寄存器 | 用户程序可访问,用于暂存信息 |
指令计数器 | 存放要访问的下一条指令的地址 |
程序状态字PSW | 含有状态信息,如条件码、执行方式、中断屏蔽标志等 |
用户栈指针 | 存放过程和系统调用参数及调用地址。栈指针指向该栈的栈顶 |
4. 进程控制块的组织方式
进程控制是进程管理中最基本的功能,
主要包括创建新进程、终止已完成的进程、将因发生异常情况而无法继续运行的进程置于阻塞状态、负责进程运行中的状态转换等功能。
1.处理机的执行状态
为了保护系统程序
系统态(管态、内核态):系统管理程序执行时的状态。具有较高的特权,能执行一切指令,访问所有寄存器和存储器
用户态(目态):以后程序执行时的状态。具有较低的特权,只能执行规定指令,访问指定的寄存器和存储器
2.操作系统内核的功能
(1)中断处理
(2)时钟管理
(3)原语操作
中断(外部中断) :是指处理机对系统中或系统外发生的异步事件的响应。异步事件是指无一定时序关系的随机发生的事件,如外部设备完成了数据传输任务,某一实时控制设备出现情况等。
原语 :是由若干条指令组成的,用于完成一定功能的一个过程。
与一般过程的区别在于:它们是"原子操作"。所谓原子操作,是指一个操作中的所有动作要么全做,要么全不做。换言之,它是一个不可分割的基本单位,因此,在执行过程中不允许被中断。原子操作在管态(内核态)下执行,常驻内存。
(1)进程管理
(2)存储器管理
(3)设备管理
1.进程的层次结构
在OS中,允许一个进程创建另一个进程,通常把创建进程的进程称为父进程,而把被创建的进程称为子进程。子进程可继续创建更多的孙进程,由此便形成了一个进程的层次结构。
在UNIX中,进程与其子孙进程共同组成一个进程家族(组)。
(▲)引起创建进程的事件
为使程序之间能并发运行,应先为它们分别创建进程
(▲)进程的创建
在系统中每当出现了创建新进程的请求后,OS便调用进程创建原语 Creat 按下述步骤创建一个新进程:
(1)申请空白 PCB,为新进程申请获得唯一的数字标识符(内部标识符),并从PCB集合中索取一个空白PCB。
(2)为新进程分配其运行所需的资源,包括各种物理和逻辑资源,如内存、文件、I/O设备和 CPU 时间等。
(3)初始化进程控制块(PCB)。
(4)如果进程就绪队列能够接纳新进程,便将新进程插入就绪队列。
创建进程伪代码:
Create(S¡, M¡, P¡){//CPU的状态,内存,优先级
p=Get_New_PCB();//分配新的PCB
pid=Get_New_PID();//分配进程的PID
p->ID=pid;I//设置进程的PID
p->CPU_State=S¡;//CPU的状态
p->Memory=M;//内存
p->Priority=Pi;//优先级
p->Status.Type=“Ready”;//进程状态
p->Status.List=RL; //进程队列RL:Ready List
Insert(RL, p);//将进程p插入就绪队列
Scheduler( );//调度程序
Windows下创建进程: CreateProcess ()
BOOL CreateProcess(
LPCTSTR lpApplicationName, // 应用程序名称
LPTSTR lpCommandLine, // 命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes, // 进程的安全属性
LPSECURITY_ATTRIBUTES lpThreadAttributes, // 线程的安全属性
BOOL bInheritHandles, // 是否继承父进程的属性
DWORD dwCreationFlags, // 创建标志
LPVOID lpEnvironment, // 指向新的环境块的指针
LPCTSTR lpCurrentDirectory, // 指向当前目录名的指针
LPSTARTUPINFO lpStartupInfo, // 传递给新进程的信息
LPPROCESS_INFORMATION lpProcessInformation // 新进程返回的信息
);
/**
第 1 个参数 lpApplicationName 是输入参数,指向启动进程的 exe 文件。
第 2 个参数 lpCommandLine 是输入参数,是启动进程的命令行中的参数。
当这两个参数都不为 NULL 时,第 1 个参数指定要启动的进程 exe 文件(不带参数),第 2 个参数指定启动进程所需参数。第 1 个参数也可以为 NULL,此时第 2 个参数就不能为 NULL,在 lpCommandLine 需要指定出要启动的程序名以及所接参数,彼此间以空格隔开,其中第 1 个参数即是程序名。
第 3 个参数 lpProcessAttributes 是输入参数,指向 SECURITY_ATTRIBUTES 结构变量,是进程的安全属性,可以为 NULL 则使用默认的安全属性。
第 4 个参数 lpThreadAttributes 是输入参数,同第 3 个参数一样,指向 SECURITY_ATTRIBUTES 结构变量。
第 5个参数 bInheritHandles 是输入参数,表示新进程是否从调用进程处继承了句柄。
如果参数的值为 TRUE,调用进程中的每一个可继承的打开句柄都将被子进程继承。被继承的句柄与原进程拥有完全相同的值和访问权限;如果设为 FALSE,那么不继承。
第 6 个参数 dwCreationFlags 是输入参数,表示进程的创建标志以及优先级控制。
如 : CREATE_NEW_CONSOLE 会使新建的控制台程序拥有一个新的控制台; DEBUG_PROCESS 调用进程将被当作一个调试程序,并且新进程会被当作被调试的进程。系统把被调试程序发生的所有调试事件通知给调试器。
第 7 个参数 lpEnvironment 是输入参数,指向新进程的环境变量块,如果设置为 NULL,那么使用父进程的环境变量。
第 8 个参数 lpCurrentDirectory 是输入参数,指定创建后新进程的当前目录,如果设置为 NULL,那么就在父进程所在的当前目录。
第 9 个参数 lpStartupInfo是输出参数,指向一个用于决定新进程的主窗体如何显示的 STARTUPINFO 结构体,该结构里可以设定启动信息,可以设置为 NULL 。
第 10 个参数 lpProcessInformation 是输出参数,指向一个用来接收新进程的识别信息PROCESS_INFORMATION结构体。
*/
简单调用示例
#include
int main(){
STARTUPINFO si=(sizeof(si)};
PROCESS_INFORMATION pi;
char *ZW ="C:\\Windows\\system32\\notepad.exe";
char *szCommandLine ="C:\\my.txt";
CreateProcess(ZW,
szCommandLine,
NULL,NULL, FALSE,NULL,NULL,NULL,
&si,
&pi);
return 0;
}
创建新进程
创建进程内核对象,创建虚拟地址空间
装载EXE和/或DLL的代码和数据到地址空间中
创建主线程和线程内核对象
启动主线程,进入主函数(main)
1. 引起进程终止的事件
2.进程的终止过程
若系统中发生了要求终止进程的某事件,OS便调用进程终止原语,按下述过程去终止指定的进程:
(1)根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程的状态;
(2)若被终止进程正处于执行状态,应立即终止该进程的执行,并置调度标志为真,用于指示该进程被终止后应重新进行调度;
(3)若该进程还有子孙进程,还应将其所有子孙进程也都予以终止,以防它们成为不可控的进程;
(4)将被终止进程所拥有的全部资源或者归还给其父进程,或者归还给系统;
(5)将被终止进程(PCB)从所在队列(或链表)中移出,等待其它程序来搜集信息。
1. 引起进程阻塞和唤醒的事件
2. 进程阻塞过程
正在执行的进程,如果发生了上述某事件,进程便通过调用阻塞原语block将自己阻塞。可见, 阻塞是进程自身的一种主动行为。
进入block过程后,由于该进程还处于执行状态,所以应先立即停止执行,把进程控制块中的现行状态由"执行"改为阻塞,并将PCB插入阻塞队列。
如果系统中设置了因不同事件而阻塞的多个阻塞队列,则应将本进程插入到具有相同事件的阻塞队列。最后,转调度程序进行重新调度,将处理机分配给另一就绪进程,并进行切换,亦即,保留被阻塞进程的处理机状态按新进程的PCB中的处理机状态设置 CPU 的环境。
3. 进程唤醒过程
当被阻塞进程所期待的事件发生时,比如它所启动的I/O操作已完成,或其所期待的数据已经到达,则由有关进程(比如提供数据的进程)调用唤醒原语wakeup,将等待该事件的进程唤醒。
wakeup执行的过程是:首先把被阻塞的进程从等待该事件的阻塞队列中移出,将其PCB中的现行状态由阻塞改为就绪,然后再将该PCB插入到就绪队列中。
1. 进程的挂起
当出现引起进程挂起的事件时,系统将利用挂起原语suspend( )将指定进程或处于阻塞状态的进程挂起
suspend()原语的执行过程:首先检查被挂起进程的状态,若处于活动就绪状态,便将其改为静止就绪;对于活动阻塞状态的进程,则将之改为静止阻塞。(内存到外存)
2. 进程的激活过程
当发生激活进程的事件时,系统将利用激活原语active( )将指定进程激活
active()原语执行过程激活原语先将进程从外存调入内存,检查该进程的现行状态,若是静止就绪,便将之改为活动就绪;若为静止阻塞便将之改为活动阻塞
如果不能采取有效的措施,对多个进程的运行进行妥善的管理,必然会因为这些进程对系统资源的无序争夺给系统造成混乱。致使每次处理的结果存在着不确定性,即显现出其不可再现性。
引入:进程同步是一个OS级别的概念,是在多道程序的环境下,进程间存在着不同的制约关系。
为了协调这种互相制约的关系,实现资源共享和进程协作,从而避免进程之间的冲突,引入了进程同步。
1. 两种形式的制约关系
(1)间接相互制约关系:互斥问题(共享系统资源,如I\0设备,CPU)—是同步的特例
(2)直接相互制约关系:同步问题
某些应用程序,为了完成某任务而建立了两个或多个进程。这些进程将为完成同一项任务而相互合作。
2.临界资源
一段时间内只允许一个进程访问的资源
许多硬件资源如打印机、磁带机等,都属于临界资源,诸进程间应采取互斥方式,实现对这种资源的共享。
3.经典例题 生产者-消费者问题
有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有n个缓冲区的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中;消费者进程可从一个缓冲区中取走产品去消费。尽管所有的生产者进程和消费者进程都是以异步方式运行的,但它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已装满产品且尚未被取走的缓冲区中投放产品
怎么实现
说明:投入一个产品时,buffer中暂存产品的数组指针in加1。
由于缓冲池是被组织成循环缓冲的,故in=(in+1)%n表示+操作。
输出指针out,out=(out+1)%n,当(in+1)%n == out时,表示缓冲池已满;当in==out时,表示缓冲池为空。
数据结构
int in=0,out=0//定义指针
int counter = 0;//定义计数器
item buffer[n];//分配缓冲区
局部变量:nextp//存放刚生产出来的产品
,nextc //存放每次要消费的产品
/*--------------------------------------------------------*/
void producer(void ){//生产者进程
while(1){
produce an item in nextp;//生产一个产品放在nextp
while (counter==n);//判断缓冲区是否满了
buffer[in] =nextp; //将产品存入缓冲区
in=(in+1)%n;//修改缓冲区指针,指向下一个空位置
counter++}//修改计数器
}
void consumer(void){ //消费者进程
while(1){
while (counter==0);//判断缓冲区是否为空
nextc=buffer[out];
out= (out+1)%n;
counter--;
consume the item in nextc;
}
}
并发执行有不可再现性
为了预防产生这种错误,解决此问题的关健是应把变量counter作为临界资源处理,即令生产者进程和消费者进程互斥地访问变量counter。
4.临界区
由前所述可知,不论是硬件临界资源还是软件临界资源,多个进程必须互斥地对它进行访问。
把在每个进程中访问临界资源的那段代码称为临界区(critical section)。
显然,若能保证诸进程互斥地进入自己的临界区,便可实现诸进程对临界资源的互斥访问。
为此,每个进程在进入临界区之前,应该先对欲访问的临界资源进行检查,看它是否正在被访问。
在临界区前面增加一段用于进行上述检查的代码,把这段代码称为进入区(entry section)。
在临界区后面也要加上一段称为退出区(exit section)的代码,用于将临界区正被访问的标志恢复为未被访问的标志。
进程中除了上述进入区、临界区及退出区之外的其它部分的代码,都称为剩余区(remainder section)。
可把一个访问临界资源的循环进程描述如下:
while(TRUE){
进入区
临界区
退出区
剩余区
}
5.(▲)同步机制应遵循的规则
注:有限等待不能死等;让权(让的是CPU)不能忙等,自己进入阻塞状态(block)中。
(1)死等状态:进程在有限时间内根本不能进入临界区,而一直在尝试进入,陷入一种无结果的等待状态。
(没有进入临界区的正在等待的某进程根本无法获得临界资源而进入进程,这种等待是无结果的,是死等状态~)
这个时候应该放弃这个无结果的事情,保证自己等待的时间是有限的
(2)忙等状态:当一个进程正处在某临界区内,任何试图进入其临界区的进程都必须进入代码连续循环,陷入忙等状态。连续测试一个变量直到某个值出现为止,称为忙等。
(没有进入临界区的正在等待的某进程不断的在测试循环代码段中的变量的值,占着处理机而不释放,这是一种忙等状态~)-> 这个时候应该释放处理机让给其他进程
(3)有限等待:对要求访问临界资源的进程,应保证有限时间内能进入自己的临界区,以免陷入“死等”状态。
(受惠的是进程自己)
(4)让权等待:当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态。
(受惠的是其他进程)
虽然可以利用软件方法解决诸进程互斥进入临界区的问题,但有一定难度,并且存在很大的局限性,因而现在已很少采用。
相应地,目前许多计算机已提供了一些特殊的硬件指令,允许对一个字中的内容进行检测和修正,或者是对两个字的内容进行交换等。可利用这些特殊的指令来解决临界区问题。
- 在对临界区进行管理时,可以将标志看成一把锁
- 锁开进入,锁关等待
- 每个要进入临界区的进程必须先对锁进行测试
1.关中断
在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断。这样,进程在临界区执行期间,计算机系统不响应中断,从而不会引发调度,也就不会发生进程或线程切换。
由此,保证了对锁的测试和关锁操作的连续性和完整性,有效地保证了互斥。
关中断方法的缺点:
2.利用 Test - and -Set 指令实现互斥
boolean TS(Boolean *lock){
boolean old;
old=*lock;
*lock=TRUE;
return old;
}
//-----------------------------------
do{
...
while TS(&lock);
critical section;//临界区
lock= FALSE;
remainder section;
}
while(TRUE);
3.利用Swap指令实现进程互斥
该指令称为对换指令,在Intel 80x86中又称为XCHG指令,用于交换两个字的内容。
void swap(boolean *a, boolean *b)
boolean temp;
temp =*a;
*a= *b;
*b = temp ;
}
//----------------------------------------------
do{
key = TRUE;
do{
swap(&lock,&key);
}while(key!=FALSE);
critical section
lock = FALSE;
remainder section;
}while (TRUE)
硬件同步特点:
可实现进程互斥访问
当临界资源忙时存在“忙等”状态,不符合“让权等待”原则
很难解决复杂同步问题
整型信号量和记录型信号量的进程互斥问题针对是多个并发进程仅共享一个临界资源的情况。
AND型信号量是一个进程往往需要获得两个或更多的共享资源后方能执行其任务。
1.整型信号量
整型信号量定义为表示资源数目的整型量
除初始化外,仅能通过原子操作wait(S)和signal(S)访问
wait(S)也称P操作、signal(S)也称V操作
wait(S)
{
while (S<=0);/* do no-op */
S=S--;
}
signal(S)
{
S=S++;
}
wait(S) 和 signal(S) 是原子操作,执行时是不可中断的。另外,在wait操作中,对S的测试和做S=S–操作时都不可中断,信号量只能通过原语操作来访问,不能被进程调度所打断
2.记录型信号量
记录型信号量机制是一种不存在“忙等”现象的进程同步机制。
但在采取了“让权等待”的策略后,又会出现多个进程等待访问同一临界资源的情况。
为此在信号量机制中,除了需要一个用于代表资源数目的整型变量value外,还应增加一个进程链表指针list,用于链接上述的所有等待进程。
typedef struct {
int value;
struct process_control_block *list;//每一个块里面放的都是PCB,且进程堵塞原因一致
}semaphore;
wait(semaphore *S)
{
S->value--; //请求一个该类资源
if (S->value<0)
block(S->list);
//该类资源已分配完毕,调用block原语,进行自我阻塞并放弃处理机、插入到信号量链表S.L中
}
signal(semaphore *S)
{
S->value ++; // 已经释放进程
if(S.value<=0)
wakeup(S->list); // 相应的可以唤醒一个进程
}
注:
(1)S->value 的值为负数的绝对值为阻塞进程的个数,正数为进程个数(该类资源数)。
(2)S->value 的初值表示系统中某类资源的数目,又称资源信号量。
(3)若 S->value 的初值为1,表示只允许一个进程访问临界资源,此时信号量转化为互斥信号量。
3.AND信号量
假定现有两个进程A和B,它们都要求访问共亨数据D和E,当然,共享数据都应作为临界资源。
为此,可为这两个数据分别设置互斥的信号量Dmutex和Emutexo
process A: process B:
wait(Dmutex); wait(Emutex);
wait(Emutex); wait(Dmutex);
若进程A和B按下述次序交替执行wait操作:
process A: P(Dmutex);于是Dmutex=0
process B:P(Emutex);于是Emutex=0
process A:P(Emutex);于是Emutex=-1 A阻塞
process B:P(Dmutex);于是Dmutex=-1B阻塞
最后,进程A和B将处于僵持状态。
AND同步机制的基本思想
将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。
为此,在wait 操作中增加了一个AND条件,或称为同时wait操作,即 Swait 操作定义如下:
Swait (S1,S2,...,Sn)
{
while(TRUE)
{
if(Si >= 1 && ...&& Sn>=1) //每个资源都可用
{
for(i=1;i<n;i++)
Si -- ; //分配所有资源
break;
}
else
{
//将进程放入与Si < 1的第一个Si相关联的等待队列中,并将该进程的程序计数设置为交换操作的开始
place the process in the waiting queue associates with the first Si found with Si < 1,and set the program count of this process to the beginning of Swait operation}
}
}
Ssignal(S1,S2,...,Sn)
{
while(TRUE)
{
for (i=1;i<n;i++)
{
Si ++; //释放所有资源
//将与Si关联的队列中等待的所有进程移至就绪队列中。
Remove all the process waiting in the queue associsted with Si into the ready queue.}
}
}
4.信号量集(了解)
在前面所述的记录型信号量机制中,wait(S)或signal(S)操作仅能对信号量施以加1或减1操作,意味着每次只能对某类临界资源进行一个单位的申请或释放。当一次需要N个单位时,便要进行N次 wait(S)操作,这显然是低效的,甚至会增加死锁的概率。
此外,在有些情况下,为确保系统的安全性,当所申请的资源数量低于某一下限值时,还必须进行管制,不予以分配。
因此,当进程申请某类临界资源时,在每次分配之前,都必须测试资源的数量,判断是否大于可分配的下限值,决定是否予以分配。
对进程所申请的所有资源以及每类资源不同的资源需求量,在一次P、V操作中完成申请或释放。进程对信号量S的测试值不再是1,而是该资源分配的下限值 t,即要求S≥t,否则不予分配。一旦允许分配,进程对该资源的需求值是d,表示资源占有量,进行S,= S;-di。
Swait(S1, t1, d4, …Sn tn dn);
Ssignal(S1, d1,… Sn, dn);
1. 利用信号量实现进程互斥
为使多个进程能互斥的访问某临界资源,只需为该资源设置一个互斥信号量mutex,并设置其初值为 1 ,然后将各进程访问该资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可。
semaphore mutex=1; //只有一个资源
PA()
{
while(1)
{
wait(mutex); //当 mutex < 0 时停止
临界区
signal(mutex);
剩余区
}
}
PB()
{
while(1)
{
wait(mutex);
临界区
signal(mutex);
剩余区
}
}
(1)wait 和 signal必须成对出现
(2)缺少 wait 会导致系统混乱,不能保证对临界区资源的互斥
(3)缺少 signal 将会使临界资源永远不被释放,从而使因等待的资源阻塞进程不被唤醒
2.利用信号量实现前趋关系(同步关系)
为实现先后执行 ——> 这种前趋关系,只需要
使进程P1和P2共享一个公用信号量S,其初值为0,将 signal 操作放在语句S1后面,而在S2语句前面插入 wait 操作。由于S被初始化为0,若P2先执行必定阻塞,只有在进程P1执行完S1、signal(S)后使S++,P2进程方能执行S2(在执行S2前先等待,故初值为0做wait操作)
- 信号量机制存在的问题:编写程序困难、易出错
- 1973年,Brinch Hansen首次在程序设计语言(Pascal)中引入了“管程”成分——一种高级同步机制
1. 管程的定义(进程的秘书)
所谓管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发。
可利用共享数据结构抽象的表示系统中的共享资源,并将对该共享数据结构实施的特定操作定义为一组过程。
进程对共享资源的申请、释放和其他操作都必须通过这组过程。
一个管程定义了一个数据结构和能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。
2.管程四部分
管程的组成
- 一个锁
- 控制管程代码的互斥访问
- 0或者多个条件变量
- 管理共享数据的并发访问
Monitor monitor_name //管程名
{
share variable declarations;//共享条件变量;
cond declarations; //条件变量
public:
void P1(){} //PV操作
void P2(){}
{initialization code;//初始化}
}
3.条件变量
利用管程实现进程同步时,必须设置同步工具,如两个同步操作原语 wait 和 signal。
当某进程通过管程请求获得临界资源而未能满足时,管程便调用 wait 原语使该进程等待,并将其排在等待队列上。仅当另一个进程访问完成并释放该资源后,管程才又调用signal原语,唤醒等待队列中的队首进程。
注意事项:
(1)管程对每个条件变量,都须予以说明,其形式为:condition x,y
(2)该变量应置于wait 和 signal 之前,可表示为X.wait 和 X.signal
例如:由于共享数据被占用而使调用进程等待,该条件变量形式为:condition nonbusy
wait 原语 ————> nonbusy.wait 或 cwait(nonbusy)
signal 原语 ————> nonbusy.signal
s=s+1
操作,总会改变信号量的状态。4.进程和管程的区别
①数据结构的属性不同
②执行的操作不同
③设置的目的不同
④工作方式不同
⑤是否具有并发性
⑥是否具有动态性
5.管程的特征
①模块化
②抽象数据类型
③信息掩蔽
问题描述:
假定在生产者和消费者之间的共用缓冲池中具有 n 个缓冲区,这时可以利用互斥信号量mutex(初始值 1)实现进程对缓冲池的互斥作用;利用信号量 empty(初始值为n)和 full(初始值为0)分别表示缓冲池中空缓冲区和满缓冲区的数量。
又假定这些生产者和消费者相互等效,只要总缓冲区未满,生产者便可将消息送入缓冲池中,只要缓冲池没空,消费者便可从缓冲池中取走一个消息
int in=0, out=0;
// item 为消息的结构体
item buffer[n];
semaphore mutex =1; //缓冲区互斥操作的互斥信号量
semaphore empty = n; //空缓冲区数目,类型为资源信号量
semaphore full = 0; //满缓冲区为数目,类型为资源信号量
void producer(){
do{
// 生产者产生一条消息
producer an item next p;
......
// 判断缓冲池中是否仍有空闲的缓冲区
P(empty);
// 判断是否可以进入临界区(操作缓冲池)
P(mutex);
// 向缓冲池中投放消息
buffer[in] = nextp;
// 移动入队指针
in = (in+1) % n;
//退出临界区,允许别的进程操作缓冲池
V(mutex);
// 缓冲池中数量的增加,可以唤醒等待的消费者进程。
V(full);
}while(true);
}
void consumer(){
do{
//判断缓冲池中是否有非空的缓冲区(消息)
P(full);
// 判断是否可以进入临界区(操作缓冲池)
P(mutex);
nextc = buffer[out];
// 移动出队指针
out = (out+1) % n;
// 退出临界区,允许别的进程操作缓冲池
V(mutex);
// 缓冲池中空缓冲区数量加1, 可以唤醒等待的生产者进程
V(empty);
// 消费消息
consumer the item in next c;
}while(true);
}
int in=0, out=0;
// item 为消息的结构体
item buffer[n];
semaphore mutex =1; //缓冲区互斥操作的互斥信号量
semaphore empty = n; //空缓冲区数目,类型为资源信号量
semaphore full = 0; //满缓冲区为数目,类型为资源信号量
void producer() //生产者进程
{
while(1)
{
produce an item nextp;
Swait(empty,mutex)
buffer[in] = nextp;
in = (in+1)%n; //循环队列
Ssignal(empty,mutex); //解锁
}
}
void consumer() //消费者进程
{
while(1)
{
Swait(full,mutex)
nextc = buffer[out];
out = (out+1)%n; //循环队列
Ssignal(empty,mutex)
consumer the item in nextc;
}
}
建立一个管程,并命名为producerconsumer,或简称PC。其中包括两个过程:
(1)put(x)过程。生产者利用该过程将自己生产的产品放到缓冲池中,并用整型变量count来表示在缓冲池中已有的产品数目,当count >= n 时,表示缓冲池已满,生产者等待。(也就意味需要有2个条件变量)
(2)get(x)过程。消费者利用该过程从缓冲池中取出一个产品,当count <= 0 时,表示缓冲池中已经无可取用的产品,消费者等待。
对于条件变量notfull 和 notempty,分别有两个过程cwait 和 csignal对它们进行操作:
(1)cwait(condition)过程:当管程被一个进程占用时,其他进程调用该过程时阻塞,并挂在条件condition 的队列上
(2)csignal(condition)过程:唤醒在cwait执行后阻塞在条件 condition 队列上的进程,如果这样的进程不止一个,则选择其中一个实施唤醒操作;如果队列为空,则无操作而返回
monitor producer-consumer //类似于一个类
{
int in=0,out=0,count=0;
itm buffer[n];
condition notfull,notempty;
void put(item nextp)
{
if count >= n then
cwait (notfull)//判断是否满了
buffer [in]=nextp;
in= (in+1)%n;
count =count +1;
csignal(notempty);
}
void get(item *x)
{
if count <= 0 then
cwait (notempty)
*x=buffer[out];
out= (out+1)%n;
count =count - 1;
csignal(notfull); //告诉生产者你可以生产了
}
}PC;
//利用管程解决生产者-消费者问题
process producer()
{
item x;
while (TRUE)
{
produce an item in x;
PC.put(item); //相当于调用方法
}
}
process consumer()
{
item x;
while (TRUE)
{
PC.get(item);
consumer an item in x;
}
}
问题描述:
5个哲学家坐在桌子边,桌上有 5 个碗和 5 只筷子,哲学家的生活方式是交替的进行思考和进餐,哲学家饥饿时便拿起两边的筷子进餐,但只有当拿到两只筷子后才能进餐,用餐完毕放下筷子,继续思考。
小提取:
semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex = 1; //设置取筷子的信号量
void philosopher(int i) // 第 i 位哲学家的活动可以描述为
{
while( 1 )
{
P(chopstick[i]); //画个圈你就知道,这是他左手边的筷子
P(chopstick[i+1] % 5
...
//eat
V(chopstick[i+1] % 5); //释放右边筷子
V(chopstick[i]); //释放左边
//think
}
}
上述解法可保证不会有两位相邻的哲学家同时进餐,但却有可能引起死锁。
方法1:互斥吃饭
至多只允许四位哲学家同时去拿左边的筷子,最终能保证至少有一位哲学家能够进餐,并且吃完后,释放两只筷子
semaphore chopstick[5]={1,1,1,1,1};
semaphore eating = 4; //仅允许四个哲学家可以进餐
void philosopher(int i) // 第 i 位哲学家的活动可以描述为
{
while( 1 )
{
P(eating); //请求就餐,若是第五个则先挨饿
P(chopstick[i]); //画个圈你就知道,这是他左手边的筷子
P(chopstick[i+1] % 5);
eating(); //进餐
V(chopstick[i+1] % 5); //释放右边筷子
V(chopstick[i]); //释放左边
V(eating); //释放信号量给其他哲学家
}
}
方法2:互斥拿筷子的动作
仅当哲学家的左、右两只筷子均可用的时候,才可以进餐
semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex = 1; //设置取筷子的信号量
void philosopher(int i) // 第 i 位哲学家的活动可以描述为
{
while( 1 )
{
thinking();
P(mutex); //在取筷子前获得互斥量
P(chopstick[i]); //画个圈你就知道,这是他左手边的筷子
P(chopstick[i+1] % 5);
V(mutex); //释放互斥量
eating(); //进餐
V(chopstick[i+1] % 5); //释放右边筷子
V(chopstick[i]); //释放左边
}
}
要求每个哲学家先获得两个临界资源(筷子)后方能进餐,本质就是 AND 同步问题,故用 AND 信号量机制解决哲学家进餐问题是最简洁的解法
semaphore chopstick[5]={1,1,1,1,1};
process(i)
{
while(1)
{
think;
Swait(chopstick[i+1] % 5,chopstick[i]); //把两个筷子全拿起来
eat;
Ssignal(chopstick[i+1] % 5,chopstick[i]);
}
}
高级进程通信工具有四大类:1.共享存储器 2.管道通信 3.消息传递系统 4.客户-服务器系统
共享存储器
又可以分为下面两种类型:
管道通信系统
管道机制提供三方面协调能力
消息传递系统
分为两类:
客户-服务器系统
分为三类:
套接字(socket)
远程过程调用(RPC)
远程方法调用(RMC)
套接字
- 起源于20世纪70年代UNIX
- 是UNIX操作系统下的网络通信
- 最初被设计在一个主机上面多个应用程序之间的通信
- 套接字是目前最流行的网络通信接口,专门为C/S模型设计
- 套接字是用于通信的数据结构,包含目的地址,端口号,传输协议,进程所在网络地址等
- 多个系统调用:listen,bind,connect,accept,read,write,close
远程过程调用
- 远程过程(函数)调用RPC,是一个通信协议,用于通过网络连接系统
- 该协议允许一台计主机(本地)系统上的进程调用另外一台主机(远程)系统上的进程,和普通过程调用类似
- 本地和主机上都需要运行网络守护进程,负责网络消息传递
直接消息传递系统
直接通信原语分为两类:
对称寻址方式 (1对1)
非对称寻址方式 (1对多)
消息格式:
进程的同步方式 :
通信链路
(1)信箱的结构:信箱定义位一种数据结构
逻辑上分成两部分(这与生活中互发邮件的功能很像)
(2)信箱的原语(系统为邮箱通信提供了若干条原语)
(3)信箱的类型(稍微注意有考点)
根据邮箱创建者不同,分成以下三类
私用邮箱(进程结束邮箱消失)
公用邮箱(OS创建进行指派)
共享邮箱
消息缓冲队列通信机制(广泛用于本地进程):发送进程利用 Send 原语将消息直接发送给接收进程,接收进程则利用 Receive 原语接收消息。
消息缓冲队列通信机制中的数据结构
(1)消息缓冲区
typedef struct message_buffer {
int sender; //发送者进程标识符
int size; //消息长度
char *text; //消息正文
struct message_buffer *next; //指向下一个消息缓冲区的指针
}
(2) PCB 中有关通信的数据项
typedef struct processcontrol_block {
...
struct message_buffer *mq; //消息队列队首指针
semaphore mutex; //消息队列互斥信号量
semaphore sm; //消息队列资源信号量.
...
}PCB
发送原语首先根据发送区a 中所设置的消息长度a.size来申请一缓冲区i ,接着把发送区a中的信息复制到缓冲区i中。为了能将i挂在接收进程的消息队列mq上,应先获得接收进程的内部标识符j,然后将i挂在j.mq上。由于该队列属于临界资源 ,故在执行insert操作的前后都要执行wait和signal操作。
void send(receiver,a){
getbuf(a.size, i);
copy(i.sender,a.sender);
i.size=a.size;
copy(i.text,a.text);
i.next=O;
getid(PCBset,receiver.j);
wait(j.mutex);
insert(&j.mq,i);
signal(j.mutex);
signal(j.sm);
}
接收进程调用接收原语receive(b),从目已的消息缓冲队列mq中摘下第一个消息缓冲区i,并将其中的数据复制到以b为首址的指定消息接收区内。
void receive(b){
j=internal name;
wait(j.sm);
wait(j.mutex);
remove(j.mq,i);
signal(j.mutex);
copy(b.sender,i.sender);
b.size =i.size;
copy(b.text,i.text);
releasebuf(i);
}
在OS中引入进程的目的是为了使多个程序能并发执行,以提高资源利用率和系统吞吐量,那么在操作系统中再 引入线程,则是为了减少程序在并发执行时所付出的时空开销,使OS具有更好的并发性。
将进程的两个基本属性分开:作为调度和分派的基本单位,不同时作为拥有资源的单位,以“轻装上阵”;
特性 | 进程 | 线程 |
---|---|---|
调度的基本单位 | 在传统OS中,进程是作为独立调度和分配的基本单位,因而进程是能独立运行的基本单位。在每次被调度时,都需要进行上下文切换,开销较大。 | 在引入线程的OS中,已把线程作为调度和分配的基本单位,因而线程是能独立运行的基本单位。在同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,必然就会引起进程的切换。 |
并发性 | 进程之间可以并发执行。 | 在引入线程的OS中不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,甚至还允许在一个进程中的所有线程都能并发执行。同样,不同进程中的线程也能并发执行。这使得OS具有更好的并发性,从而能更加有效地提高系统资源的利用率和系统的吞吐量。 |
拥有资源 | 进程可以拥有资源,并作为系统中拥有资源的一个基本单位。 | 线程本身并不拥有系统资源,而是仅有一点必不可少的、能保证独立运行的资源。 线程除了拥有自己的少量资源外,还允许多个线程共享该进程所拥有的资源,这首先表现在:属于同一进程的所有线程都具有相同的地址空间,这意味着,线程可以访问该地址空间中的每一个虚地址;此外,还可以访问进程所拥有的资源。 |
独立性 | 不同进程之间具有独立性。 | 在同一进程中的不同线程之间的独立性要比不同进程之间的独立性低得多。这是因为,为防止进程之间彼此干扰和破坏,每个进程都拥有一个独立的地址空间和其它资源,除了共享全局变量外,不允许其它进程的访问。但是同一进程中的不同线程往往是为了提高并发性以及进行相互之间的合作而创建的,它们共享进程的内存地址空间和资源。 |
系统开销 | 在创建或撤消进程时,系统都要为之分配和回收进程控制块、分配或回收其它资源,如内存空间和I/O设备等。OS为此所付出的开销,明显大于线程创建或撤消时所付出的开销。类似地,在进程切换时,涉及到进程上下文的切换,而线程的切换代价也远低于的。 | 在Solaris2OS中,线程的创建要比进程的创建快30倍,而线程上下文切换要比进程上下文的切换快5倍。 此外,由于一个进程中的多个线程具有相同的地址空间,线程之间的同步和通信也比进程的简单。因此,在一些OS中,线程的切换、同步和通信都无需操作系统内核的干预。 |
支持多处理机系统 | 在多处理机系统中,对于传统的进程,即单线程进程,不管有多少处理机,该进程只能运行在一个处理机上。 | 对于多线程进程,可以将一个进程中的多个线程分配到多个处理机上,使它们并行执行,这无疑将加速进程的完成。 |
如同每个进程有一个进程控制块一样,系统也为每个线程配置了一个线程控制块TCB,将所有用于控制和管理线程的信息记录在线程控制块中。
通常在多线程OS中的进程都包含了多个线程,并为它们提供资源。OS支持在一个进程中的多个线程能并发执行,但此时的进程就不再作为一个执行的实体。多线程OS中的进程有以下属性:
(1)进程是一个可拥有资源的基本单位
(2)多个线程可并发执行
(3)进程已不再是可执行的实体
支持线程KST同样也是在内核的支持下运行的,它们的创建、阻塞、撤消和切换等,也都是在内核空间实现的。
同一进程下的线程切换开销比较大。
用户级线程是在用户空间中实现的。对线程的创建、撤消、同步与通信等功能,都无需内核的支持,即用户级线程是与内核无关的。
有些OS把用户级线程和内核支持线程两种方式进行组合,提供了组合方式ULT/KST线程。
多对一模型 将多个用户线程映射到一个内核控制线程。
优势:更好的并发性,一个线程阻塞,允许调度另一个线程执行;
劣势:开销大;整个系统线程数有限;
一对一模型 为每一个用户线程都设置一个内核控制线程与之连接。
优势:开销小,效率高
劣势:一个线程阻塞,整个进程阻塞;映射到一个内核的多线程不能使用多处理机
多对多模型 将多个用户线程映射到多个内核控制线程。
结合上述两种方式的优势
在仅设置了内核支持线程的os中,一种可能的线程控制方法是,系统在创建一个新进程时,便为它分配一个任务数据区PTDA,其中包括若干个线程控制块TCB空间
内核支持线程的创建、撤消均与进程的相类似。
内核支持线程的调度和切换与进程的调度和切换十分相似,也分抢占式方式和非抢占方式两种。
所谓“运行时系统”,实质上是用于管理和控制线程的函数(过程)的集合,其中包括用于创建和撤消线程的函数、线程同步和通信的函数,以及实现线程调度的函数等。正因为有这些函数,才能使用户级线程与内核无关。运行时系统中的所有函数都驻留在用户空间,并作为用户级线程与内核之间的接口。
这种线程又称为轻型进程LWP。每一个进程都可拥有多个LWP,同用户级线程一样,每个LWP都有自己的数据结构(如TCB),其中包括线程标识符、优先 级、状态,另外还有栈和局部存储区等。LWP也可以共享进程所拥有的资源。LWP可通过系统调用来获得内核提供的服务,这样,当一个用户级线程运行时,只须将它连接到一个LWP上,此时它便具有了内核支持线程的所有属性。这种线程实现方式就是组合方式。
线程的创建
在多线程OS环境下,应用程序在启动时,通常仅有一个线程在执行,该线程被人们称为“初始化线程”。它可根据 需要再去创建若干个线程。在创建新线程时,需要利用一个线程创建函数(或系统调用),并提供相应的参数,如指向线程主程序的入口指针、堆栈的大小,以及用于调度的优先级等。在线程创建函数执行完后,将返回一个线程标识符供以后使用。
线程的终止
当一个线程完成了自己的任务(工作)后,或是线程在运行中出现异常清况而须被强行终止时,由终止线程通过调用相应的函数(或系统调用)对它执行终止操作。但有些线程(主要是系统线程),它们一旦被建立起来之后,便一直运行下 去而不被终止。在大多数的os中,线程被中止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止的线程才与资源分离,此时的资源才能被其它线程利用。
终止线程的方式有两种:一种是在线程完成了自己的工作后自愿退出;另一种是线程在运行中出现错误或由于某种原因而被其它线程强行终止。