1.进程的定义及组成,程序和进程的区别,进程的四大特征
进程的定义
进程是程序的执行过程,是系统进行资源分配和调度的一个独立单位
(程序的执行属于并发执行,但是程序是不能参与并发执行的,进程可以并发执行,并且对并发执行的程序加以描述和控制)
进程的组成
进程由程序段、相关的数据段和PCB组成
PCB,叫做进程控制块,作用:使参与并发的执行的每个程序(含数据)都能独立的运行,系统利用PCB来描述进程的基本情况和活动过程,进而管理和控制进程
程序和进程的区别
本质区别:进程是动态的,而程序则是静态的。程序是指令的有序集合,无执行含义,而进程则强调执行的过程。进程具有并行特征,而程序没有。
区别有:
(1)进程是一个动态的概念,而程序是一个静态的概念,程序是指令的有序集合,无执行含义,进程则强调执行的过程;
(2)进程具有并行特征(独立性,异步性),程序没有;
(3)不同的进程可以包含同一程序,同一程序在执行中也可以产生多个进程。
进程的四大特征
1)动态性
进程的实质是程序的执行过程,动态性就是进程最基本的特征;表现在:进程由创建而产生,由调度而执行,由撤销而消亡,进程有一定的声明周期,而程序则只是一组有序指令的集合,本身不具有活动的含义,是静态的。
2)并发性
是指多个进程共存于内存中,且能在一时间内同时执行,引入进程也正是为了使进程能和其他进程并发执行,并发性使进程的重要特征,同时也成为了OS的重要特征,而程序(为建立PCB)是不能参与并发执行的。
3)独立性
是指进程是一个能够独立运行、独立获得资源、独立接受调度的基本单位、但是为建立PCB的程序都不能作为一个独立的单位参与并发执行。
4)异步性
是指进程是按异步方式运行的,即按各自独立的、不可预知的速度向前推进。
2.进程的状态及状态切换
进程的3种基本状态
1)就绪状态
是指进程已处于准备好执行的状态,即进程已分配到除CPU以外的所有必要资源后,只要再获得CPU,便可立即执行,如果系统中有许多处于就绪状态的进程,则通常会将它们按一定的策略(如优先级策略)排成一个队列,称该队列为就绪队列。
2)执行状态
是指进程获得CPU后其程序“正在执行”这一状态,单处理机系统中,只有一个进程处于执行状态,而在多处理机系统中,则可能会有多个进程处于执行状态。
3)阻塞状态
正在执行的进程由于发生某事件(如I/O请求,申请缓冲区失败等)而暂时无法继续执行,即指进程的执行受到了阻塞,此时发生进程调度,OS会把处理机分通常另一个就绪进程,让受阻进程处于暂停状态,一般将这种状态成为阻塞状态,通常系统会将处于阻塞状态的进程排成一个队列,称该队列为阻塞队列。
进程3种基本状态间的转换
创建状态和终止状态
1)创建状态
具体说明:进程申请一个空白PCB,并向PCB中填写用于控制和管理进程的信息,为该进程分配运行时所必须的资源,将该进程的状态转换为就绪状态并将其插入就绪队列之中,但如果进程所必须的资源尚不能得到满足,此时创建工作尚未完成,进程不能被调度运行,将此时的状态称为创建状态(或新建状态)。
2)终止状态
进程的终止也要通过两个步骤,首先,等待OS进行善后处理;然后,将进程的PCB清零,并将PCB空间返还OS。当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被OS所终止,或是被其他有终止权的进程所终止时,它就会进入终止状态。
进程的5种基本状态及其转换关系
挂起操作
含义:当该操作作用于某个进程时,该进程将被挂起,这意味着此时该进程处于静止状态.
原因:
1)终端用户的需要。终端用户自己的程序在运行期间发现有可疑问题,希望暂停程序执行,以便用户研究其执行情况或对其进行修改。
2)父进程的需要。有时父进程希望挂起自己的某个子进程,以便考查和修改该子进程或者协调各子进程间的活动
3)负荷调节的需要。当实时系统中的工作负荷较重,可能会影响到对实时任务的控制时,系统可把一些不重要的进程挂起,以保证自身能正常运行。
4)OS的需要。OS有时希望挂起某些进程,以便检查在进程运行过程中资源的使用情况或进行记账。所记录的信息包括CPU时间、实际使用时间、作业或进程数量等。
引入挂起操作后进程3个基本状态间的转换
引入挂起原语Suspend和激活原语Active(二者须成对使用)后,进程可能会发生以下几种状态转换。
1)活动就绪--->静止阻塞
进程处于未被挂起的就绪状态,称为活动就绪状态。此时进程接受调度,当被挂起原语Suspend进程挂起后,进程状态转变为静止就绪状态,此时不会被调度执行。
2)活动阻塞--->静止阻塞
静止阻塞在其所期待的事件发生之后,将从静止阻塞状态变为静止就绪状态。
3)静止就绪--->活动就绪
用激活原语Active将其激活,转变为活动就绪状态
4)静止阻塞--->活动阻塞
用激活原语Active将其激活,转变为活动阻塞状态
引入挂起操作之后进程的五个基本状态之间 的转换
3.进程的撤销(进程的终止)子进程撤销后,应将从父进程那获得的资源返回给父进程
进程撤销,也叫终止进程,释放进程占有的资源只是撤销进程过程的一部分。
引起进程终止的事件
1)正常结束,表示进程的任务已经完成,准备退出运行。
2)异常结束,是指进程在运行时,发生了某种异常事件,使程序无法继续运行。
3)外界干预,是指进程应外界请求而终止运行。
(1)操作员或OS干预
(2)父进程请求,当子进程完成父进程所要求的任务时,父进程可以提出请求结束该子进程。
(3)父进程终止,它的所有子孙进程都应当结束。
进程终止过程
1)根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从该进程的PCB中读取该进程 的状态
2)若进程处于执行状态,立即终止该进程的执行,并置调度标志为真,以指示该进程被终止后应重新进程调度
3)若该进程还有子孙进程,则还应终止其子孙进程,以防止其称为不可控的进程
4)将被终止的进程所拥有的全部资源,或归还给其父进程,或归还给系统
5)将被终止的进程的PCB从所在队列(或链表)中移出,等待其他程序来搜集信息。
进程终止产生的不良后果
僵尸进程:
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源。
孤儿进程:
在操作系统领域中,孤儿进程指的是在其父进程执行完成或被终止后仍继续运行的一类进程。这些孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
4.进程同步
原因:引入进程之后,如果不对进程进行妥善管理,则必然会由进程对系统资源的无序争夺,给系统造成混乱。为了保证多个进程能有条不紊地运行,必须引入进程同步机制。
把异步环境下的一组并发进程因直接制约而互相发送消息、互相合作、互相等待,使得各进程按一定的速度执行的过程,称为进程同步。具有同步关系的一组并发进程称为协作进程。
数据的不一致性
共享数据并发/并行访问;数据不一致性
又称不可再现性:同一进程在同一批数据上多次运行的结果不一样
解决方法-同步(互斥)机制
数据不一致性的解决方法(引入原子操作)
原子操作:一个操作在整个执行期间不能中断
引起数据不一致性的原因
是进程间存在竞争条件,即多个进程并发访问同一共享数据,而共享数据的最终结果取决于最后操作的进程
两种形式的制约关系
1)互斥关系
多个程序并发执行时,由于共享系统资源,如CPU、I/O设备等,这些并发操作的程序执行之间会形成相互制约的关系,形成对该类资源共享的互斥关系,使用该资源时先提出申请,不能直接使用。
互斥含义:
进程排他性地运行某段代码,任何时候只有一个进程能够运行
互斥访问独占资源
2)同步关系
某些应用程序为了完成某项任务,会建立两个或多个进程。这些进程会为了完成同一任务而相互合作。称为同步关系。
同步含义:
协调进程的执行次序,使并发进程间能有效地共享资源和相互合作,保证数据的一致性。
临界区
临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用,例如:semaphore。只能被单一线程访问的设备,例如:打印机。
访问临界资源的那一部分代码称为临界区(代码片段),临界区是进程内的代码(涉及临界资源的代码区),每个进程有一个或多个临界区,每个进程进入临界区之前,应先对欲访问的临界资源进行检查,看它是否被访问,如果该临界资源未被访问,进程便可进入临界区对该资源进行访问,因此,在临界区前面增加一段用于进行上述检查的代码,把这段代码称为进入区,在临界区后面也要加入一段代码称为退出区,剩下的称为剩余区
若能保证各个进程互斥进入具有相同临界资源的临界区,可实现对临界资源的互斥访问
while(true) { 进入区 临界区; 退出区 剩余区; }
临界区的使用准则
一、互斥准则
假定进程pi在某个临界区执行,其他进程将被排斥在该临界区外
1)有相同临界资源的临界区都需要互斥
2)无相同临界资源的临界区不需互斥
二、有空让进准则
当临界区内无进程执行时,不能无限期地延长下一个要进临界区进程的等待时间
三、有限等待准则
每个进程进入临界区前的等待时间必须有限,不能无限等待
访问临界区的方法
临界资源
需要采用互斥的资源称为临界资源(一次只允许一个进程使用的资源)
又称互斥资源、独占资源或共享变量。
共享资源
一次允许多个进程使用的资源
同步机制遵循的4条准则
1)空闲让进
当无进程处于临界区时,表明临界资源处于空闲状态,应允许1个请求进入临界区的进程立即进入自己的临界区,以有效地利用临界资源。
2)忙则等待
当已有进程进入临界区时,表明临界资源正在被访问,因而其他试图进入临界区的进程必须等待,以保证对临界资源的互斥访问。
3)有限等待
对于要求访问临界资源的进程,应保证其在有限时间内能进入自己的临界区,以免陷入“死等”状态。
4)让权等待(原则上应遵循,但非必须)
当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入“忙等”状态。
5.信号量机制
信号量定义
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。
1)保证两个或多个代码段不能被并发调用
2)在进入关键代码段前,进程必须获取一个信号量,否则不能运行
3)执行完该关键代码段,必须释放信号量
4)信号量有值,为正说明它空闲,为负说明其忙碌
PV操作
wait 称为P操作
signal称为V操作
PV操作是一种实现进程互斥与同步的有效方法。PV操作与信号量的处理相关,P表示通过的意思,V表示释放的意思。
PV操作是典型的同步机制之一。用一个信号量与一个消息联系起来,当信号量的值为0时,表示期望的消息尚未产生;当信号量的值非0时,表示期望的消息已经存在。用PV操作实现进程同步时,调用P操作测试消息是否到达,调用V操作发送消息。
信号量类型
计数信号量
变化范围:没有限制的整型值
计数信号量=同步信号量
二值信号量
变化范围仅限于0和1的信号量
二值信号量=互斥信号量
信号量机制包括
1)整型信号量
整型信号量是一个整数,如果大于0表示可以获得信号量,小于等于0表示无法获得信号量
进程调用wait(S)表示要获得一个信号量。
如果S大于0:该进程可以获得一个S信号量,继续运行;
如果S小于等于0:无法获得S信号量,则无法运行下去
wait(S): while S<=0 ; S--; Signal(S)://释放获得的信号量 S++;
存在问题忙等
如S<=0:该进程将不断重复执行while语句,浪费CPU资源
2)记录型信号量
忙等的解决方法是引入记录型信号量。记录型信号量增加一个等待队列,当一个进程无法获得一个信号量时,马上释放CPU并把自己转换为等待状态,加入该信号量的等待队列,从而消除忙等。
//记录型信号量定义 typedef struct{ int value; struct process *list; }semaphore Wait(semaphore *S) { S->value--; if(S->value<0) { add this process to list S->list; block(); } } Signal(semaphore *S) { S->value++; if(S->value<=0) { remove a process P from list S->list; wakeup(P); } }
记录型信号量是把先把信号量的值减1后再判断,而整型信号量是先判断再减1
为什么整型信号量是先判断再减1
目的是可以知道由于申请该信号量而阻塞的进程数量
S是一个负数时,|S|表示S的等待队列该信号量的进程数目(跟信号量S的初值没有关系)
记录型信号量的改进在于通过加入了阻塞和唤醒机制,消除了忙等。
信号量S的使用
S必须置一次且只能置一次初值
S初值一般不为负数
除了初始化,只能通过执行P、V操作来访问S
3)AND型信号量·
4)信号量集
互斥信号量使用:
1)Semaphore *S;//初始化为1
2)wait(S);
CriticalSection()//临界区
3)signal(S);
同步信号量使用:
例子:P1和P2需要C1比C2先运行
semaphore S=0 P1: C1; signal(S); P2: wait(S); C2;
6信号量的应用(三类经典同步问题)
1)生产者消费者问题(共享有限缓冲区)
使用同步和互斥机制保证生产者和消费者在并发执行时数据的一致性
大体流程
生产者: { ... 生产一个产品 ... 把产品放入指定缓冲区 }
消费者: { ... 从指定缓冲区取出产品 ... 消费取出的产品 ... }
互斥分析基本方法
1)分析每个进程的代码,查找临界资源。
2)划分临界区;
3)定义互斥信号量并赋初值,一般互斥信号量的初值为1;
4)在临界区前的进入区加wait操作,在临界区后的退出区加signal操作。
同步分析
1)找出需要同步的代码片段(关键代码);
2)分析这些代码片段的执行次序,谁先谁后,有没有一定的次序;
3)根据次序分析,增加同步信号量并赋初始值;
4)在关键代码前后分别加wait和signal操作
同步信号量定义
共享数据
semaphore *full,*empty,*m;//full:满缓冲区的数量 empty:空缓冲区数量 初始化: full->value=0;empty->value=N;m->value=1;
解决方法
生产者: { ... 生产一个产品 ... wait(empty); wait(m); C1:把生产者放入指定缓冲区 signal(m); signal(full); }
1)当empty大于0时,表示有空缓冲区,继续执行;否则,表示无空缓冲区,当前生产者阻塞。
2)把full值加1,如果有消费者等在full的队列上,则唤醒该消费者。
消费者: { ... wait(full); wait(m); C2:从指定缓冲区取出产品 signal(m); signal(empty); ... 消费取出的产品 ... }
1)当full大于0时,表示有满缓冲区,继续执行;否则,表示无满缓冲区,当前消费者阻塞。
2)把empty值加1,如果有生产者等在empty的队列上,则唤醒该生产者
生产者消费者问题深入研究
1.如何分析生产者消费者中的同步和互斥现象?
互斥分析基本方法
1)分析每个进程的代码,查找临界资源。
2)划分临界区;
3)定义互斥信号量并赋初值,一般互斥信号量的初值为1;
4)在临界区前的进入区加wait操作,在临界区后的退出区加signal操作。
例子(单生产者-单消费者)
生产者进程enter() item nextProduced; while(1) { buffer[in]=nextProduced; in = (in+1)%BUFFER_SIZE; counter++; } 消费者进程remove() item nextConsumed; while(1) { NextConsumed=buffer[out]; out = (out+1)%BUFFER_SIZE; counter++; }
寻找共享变量
定义信号量m,初值为1
生产者进程enter() item nextProduced; while(1) { buffer[in]=nextProduced; in = (in+1)%BUFFER_SIZE; wait(m); counter++; signal(m); } 消费者进程remove() item nextConsumed; while(1) { NextConsumed=buffer[out]; out = (out+1)%BUFFER_SIZE; wait(m); counter++; signal(m); }
2.如何实现各类复杂的生产者消费者同步问题?
同步分析
1)找出需要同步的代码片段(关键代码);
2)分析这些代码片段的执行次序,谁先谁后,有没有一定的次序;
3)根据次序分析,增加同步信号量并赋初始值;
4)在关键代码前后分别加wait和signal操作
需要协同的部分
生产者:把产品放入缓冲区(C1)
消费者:从缓冲区取出产品(C2)
两种运行次序(不同条件下不同运行次序)
缓冲区空时:C1->C2
缓冲区满时:C2->C1
多生产者与多消费者
在多个消费者中,多个消费者操作都会导致in的改变,以及使缓冲区改变,如果不加以限制,则会使数据发生不一致性,同理消费者的out也是如此
在这里我们可以将
buffer[in]=nextProduced; in = (in+1)%BUFFER_SIZE; counter++;
一整个设置为临界区,前后PV操作
或者对
counter++;
进行PV操作
但是对
buffer[in]=nextProduced; in = (in+1)%BUFFER_SIZE;
前后设置PV操作,使不同生产者之间发生互斥操作,这边与消费者的临界区无互斥关系
所以一共要设置三个临界区
生产者 生产一个产品 1)判断是否有空缓冲区,如果没有则阻塞 C1:把产品放入指定的缓冲区 2)满缓冲区数量加1,如果有消费者由于等满缓冲区而阻塞,则唤醒一个消费者
消费者 1)判断是否有满缓冲区,如果没有则阻塞 从满缓取出一个产品 2)空缓冲区数量加1,如果有生产者由于等空缓冲区而阻塞,则唤醒一个生产者
多生产者多消费者问题还应该在设置同步信号量的时候,再加上互斥信号量,这样子可以避免其他进程在工作时,进程再次进入临界区,防止出现数据不一致性
例子1 桌子上有一个盘子,每次只能放一个水果。爸爸专门向盘子中放苹果,妈妈专门放桔子,儿子等着吃盘中的桔子,女儿等着吃苹果。用P、V操作实现他们之间的同步。
Semaphore mutex=1,apple=0,orange=0, empty=1;Begin Parbegin Father: begin Repeat 准备苹果 Wait(empty); 在确认盘中是否为空,若empty不为0,则可继续执行下面 Wait(mutex); 在确认其他进程是否在执行,若没有,则减一,开始后续操作 放入苹果; 生产者行为 Signal(mutex); 释放生产者动作,允许其他进程执行 Signal(apple); 生产者行为成功,给apple库存加一,允许后续消费apple进程执行 Until false; end ********************************************************************** Daughter:begin Repeat Wait(apple); 判断apple是否有有库存,若apple为0,则持续等待,否则减一并执行后续功能 Wait(mutex); 判断是否有其他进程正在执行,若有,持续等待 拿走苹果; 消费者行为 Signal(mutex); 释放资源,允许其他进程进入临界区 signal(empty); 释放资源,允许生产者进程进入临界区 Until false; end ********************************************************************** Parbegin Mother: begin Repeat 准备橘子 Wait(empty); 判断盘子是否为空,empty为1则执行下一步 Wait(mutex); 判断当前是否有进程占用临界区 放入橘子; 生产者行为 Signal(mutex); 释放资源,允许其他进程进入临界区 Signal(orange); 释放资源,允许消费者进程进入临界区 Until false; end ********************************************************************** Son:begin Repeat Wait(orange); 判断orange是否有有库存,若orange为0,则持续等待,否则减一并执行后续功能 Wait(mutex); 判断是否有其他进程正在执行,若有,持续等待 拿走橘子; 消费者行为 Signal(mutex); 释放资源,允许其他进程进入临界区 signal(empty); 释放资源,允许生产者进程进入临界区 Until false; end
例子2 桌上有空盘 允许放一个苹果。爸爸可向盘中放一个切位两块的苹果,儿子和女儿各吃其中一个块苹果。规定儿子和女儿不能同时吃掉两块,用PV原语实现爸爸、儿子、女儿3个并发进程的同步。
分析:分解法
合并
semphore empty=1,e1=0,e2=0 father() { p(empty); p(empty); 将苹果切成两块; signal(e1); signal(e2); } son(){ p(e1); 吃一块苹果; signal() } daughter() { p(e2); 吃一块苹果; signal(empty) }
semphore empty=1,e1=0,e2=0,metux=0 father() { p(empty); 将苹果切成两块; v(e1); v(e2); } son(){ p(e1); 吃一块苹果; v(metux) } daughter() { p(e2); p(metux) 吃一块苹果; v(empty) }
3 桌子上有一空盘,允许存放2个不同水果(不允许存放2个相同水果)。爸爸可向盘中放苹果,妈妈可向盘中盘中方橙子。等盘子满后,儿子吃盘中的半个橙子和半个苹果,女儿吃盘子中半个橙子和苹果,不允许儿子或女儿一人吃掉去全部水果,用PV原语实现爸爸、妈妈、儿子、女儿四个人的进程的同步。
mother() { P(e1); p(e1); 将橙子放入盘中; v(f1); v(f2); } father() { p(e2); p(e2); 将苹果放入盘中; v(f3); v(f4); } daughter() { p(f2); p(f3); 吃一半橙子和苹果; v(e1); v(e2); } son() { p(f1); p(f4); 吃一半橙子和苹果; v(e1); v(e2) }
多生产者多消费者问题和单生产者单消费者问题本质上是差不多的。 生产者与消费者公用一个信号量来限制使用临界区资源 不同的消费者与不同的生产者对各自的产品与消费对象都用一个独立的信号量来控制,避免空等和错误使用
2) 读者写者问题(数据读写操作)
读者写者问题描述
读者写者操作要求
1)允许多个读者读同一个数据区
2)不允许读者与写者同时访问数据区
3)不允许多个写者同时访问数据区
读者优先问题
解决方法:让所有的读者和写者进程的读写都互斥。
临界区在读者进程中是读操作,在写者进程中是写操作
Semephore *W;W->value=1; Readers ... P(W) 读 V(w) ... Writers ... P(W) 写 V(W)
这种模式要求读者之间也要互斥,违背了“有写者在等,但有其他读者在读时,则新读者可进入数据区读”这个要求。当一个读者获得信号量W进入数据区读后,后续的读者无法继续进入数据区,不能实现读共享。
实现读者的共享读
解决方法:
读者进入数据区读时,区分第一个读者和其它读者;读者离开数据区时,区分最后离开的读者和其它读者。
增加一个读者计数器rc,设置初始值为0;
Readers ... rc++; if(rc==1)P(W); 读 rc--; if(rc==0)V(W); ... Writers ... P(w) 写 V(w) ...
rc是一个临界资源必须让读者互斥访问
如果不将rc置为临界区,可能会导致多个进程同时操作rc导致rc的数据不一致性,导致混乱
Readers ... P(M); rc++; if(rc==1)P(W); V(M) 读 P(M); rc--; if(rc==0)V(W); V(M) ... Writers ... P(w) 写 V(w) ...
3) 哲学家就餐问题(资源竞争)
问题描述:
5个哲学家
每个哲学家左右各有一根筷子
5根筷子
每个哲学家只有拿起左右两个筷子才能吃饭
对应系统中多个进程竞争共享资源的问题
把5根筷子分别看做5个互斥信号量,任意一个哲学家只有拿起左右两根筷子,也就是获得左右两个信号量后才能吃饭,吃完饭后,该哲学家应该放下拿起的两根筷子,也就是释放左右两个信号量。
Semephore *chopstick[5];//初始值为1 哲学家 i; P(chopstick[i]);拿左边筷子 P(chopstick[(i+1)%5)];拿右边筷子 吃饭 V(chopstick[i]);//放下左边筷子 V(chopstick[(i+1)%5]);//放下右边筷子
对于任意一个哲学家i来说,在吃饭前要通过P操作,也就是wait操作,拿起自己左右的两个筷子。P(chopstick[i])拿左边筷子,P(chopstick[(i+1)%5])拿右边筷子。
V(chopstick[i])放下左边筷子 V(chopstick[i+1]%5)放下右边筷子
存在死锁问题
当每个哲学家都拿起自己左边的筷子,导致没有右边的筷子资源可以使用,这5个哲学家之间对筷子存在循环等待,从而导致他们都无法吃饭,形成死锁。
这种死锁会导致进程无法推进,资源无法使用,是必要解决的。
为防止死锁发生可采取的措施
1)最多允许4个哲学家同时坐在桌子周围
具体的解决方法如下:
1)声明一个同步信号量seat,初始值为4
2)规定每个哲学家必须申请到椅子坐下后才能拿筷子
3)在哲学家吃完饭后,必须从椅子上站起来离开,便于其他哲学家坐下吃饭
semephore *chopstick[5]; //初始值为1 semaphore *seat; //初始值为4 哲学家 i; ... P(seat); //看看4个座位是否有空 P(chopstick[i]); //拿左边筷子 P(chopstick[(i+1)%5]);//拿右边筷子 吃饭 V(chopstick[i]); //放下左边筷子 V(chopstick[(i+1)%5]);//放下右边筷子 V(seat); //释放占据的位置 ...
2)仅当一个哲学家左右两边筷子都可用时,才允许他拿筷子
1)两根筷子都空闲,则该哲学家可以拿起两根筷子吃饭
2)只要有一根筷子在被其它哲学家使用,那么两根筷子都无法拿到
哲学家分为3个状态: int *state={thinking,hungry,eating}; 设置5个信号量,对应5个哲学家 semaphore *ph[5];//初始值为0 semaphore *m;//初始值为1 void test(int i) { if(state[i]==hungry)&& //是否饿了 (state[(i+4)%5]!=eating)&& //左边哲学家是否在吃饭 (state[(i+1)%5]!=eating) //右边哲学家是否在吃饭 { state[i]=eating; //设置哲学家状态为eating V(ph[i]); //ph[i]设置为1 } } 哲学家i:0~4 思考中... 1)state[i]=hungry; P(m);//互斥信号量 2) test(i) V(m) P(ph[i]); 拿起左边筷子 拿起右边筷子 吃饭... 放下左边筷子 放下右边筷子 state[i]=thinking; test((i+4)%5); test((i+1)%5);
3)给所有哲学家编号,奇数号哲学家必须首先拿左边筷子,偶数号哲学家则反之
理解信号量S值的含义:
1)S>0 :有S个资源可用
2)S=0 :无资源可用
3)S<0 :则|S|表示S等待队列中的进程个数
理解wait和signal这两个原子操作
wait(S)或P(S)表示申请一个资源
signal(S)或V(S)表示释放一个资源
注意的是信号量的初值
1)互斥信号量初始值一般为1
2)同步信号量初始值一般为0~N的整数
信号量的使用中注意的问题
P、V操作必须成对出现,有一个P操作就一定有一个V操作和它对应
当为互斥操作时,P、V操作处于同一进程内
当为同步操作时,则在不同进程内
两个在一起的P操作的顺序至关重要,否则会导致死锁
同步与互斥P操作在一起时,同步P操作在互斥P操作前,两个V操作的次序无关紧要
S和Q是两个初值为1的二值信号量 P0 wait(S); wait(Q); ... signal(S) signal(Q) P1 wait(Q); wait(S); ... signal(Q); signal(S);
线程
线程的定义
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
为什么引入线程
引入线程之后,不仅是进程之间可以并发,进程内的各线程之间也可以并发,从而进一步提升了系统的并发度,使得一个进程内也可以并发处理各种任务。并且引入线程之后,进程只作为除CPU之外的系统资源的分配单元(像打印机、内存地址空间等都是分配给进程的)
为什么要使用多线程
引入多线程最主要的原因是为了提高资源的利用率。现在的cpu都是多核心的这意味着多个线程可以同时运行,从而可以减少了线程上下文切换的开销。第二点是可以防止阻塞。如果单核cpu使用单线程,那么只要这个线程被阻塞了,那么整个程序也就被阻塞。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
进程和线程的区别
1)调度的基本单位
线程作为调度和分派的基本单位,因而线程是能独立运行的基本单位。同一进程中,线程的切换不会引起进程的切换,但从一个进程中的线程切换到另一个进程中的线程时,必然会引起进程的切换。
2)并发性
进程可以并发,一个进程中的多个线程也可以并发,一个进程中的所有线程都可以并发,不同进程中的线程也能并发执行,使OS具有了更好的并发性,提高资源利用率和系统的吞吐量。
3)拥有资源
进程可以拥有资源,并可作为系统中拥有资源的一个基本单位,线程可以说是几乎不拥有资源,仅有的资源是为了确保自身能够独立的运行。同时还允许多个线程共享它们共属的进程所拥有的资源。
4)独立性
进程拥有独立的地址空间和其他资源,除了共享全局变量外,不允许自身以外的进程访问自己地址空间中的地址。线程可以共享进程的内存地址空间和资源,每个线程都可以访问它们所属进程地址空间中的所有地址。
5)系统开销
在创建(撤销)进程时,系统要为它分配(向它回收)TCB和其他资源(如内存空间和I/O设备等)。OS为此所付出的开销,明显大于线程创建/撤销时所付出的开销,在进行进程切换时,涉及进程上下文的切换,线程的切换代价则远低于线程的。
6)支持多处理机系统
单线程进程,不管有多少处理机,该进程只能运行在一个处理机上,但是对于多线程进程,其可以将一个进程中的多个线程分配到多个处理机上,并行运行,无疑能加速进程的完成。
管程
信号量机制的优点
程序效率高、编程灵活
信号量机制的问题
需要程序员实现,编程困难
维护困难、容易出错
解决方法
管程
由编程语言解决同步互斥问题
Hansen的管程定义
一个管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据
互斥
管程中的变量只能被管程中的操作访问
任何时候只有一个进程在管程中操作
每一个管程类似临界区
互斥由编译器完成
同步
条件变量
(条件变量组成队列,当执行p操作,队列增加一个变量,执行v操作,对列减少一个变量)
唤醒和阻塞操作
x.wait():进程阻塞直到另一个进程调用x.signal()
x.signal():唤醒另外一个进程
管程内可能存在不止一个进程
如:进程P调signal操作唤醒进程Q后
P、Q的执行顺序的可能
1)P等待直到Q离开管程(Hoare)
进程互斥进入管程
如果有进程在管程内运行,管程外的进程等待
入口队列
等待进入管程的进程队列
管程内进程P唤醒Q后
P等待,Q运行,P加入紧急队列
紧急队列的优先级高于入口队列
执行x.signal操作
x的条件队列空:空操作,执行该操作进程继续运行
x的条件队列非空:唤醒该条件队列的第一个等待进程Q,执行该操作进程进入紧急队列
管程解决哲学家就餐问题
monitor DP { //共享变量 enum {THINKING;HUNGRY,EATING}state[5]; condition self[5]; //--------------------------------------------- //初始化 initialization_code(){ for(int i=0;i<5;i++) state[i]=THINKING; } //------------- //操作 void pickup (int i){ state[i]=HUNGRY; test(i); if(state[i]!=aEATING) self[i].wait(); } void putdown (int i){ state[i]=THINKING; //test left and right neighbors test((i+4)%5); test((i+1)%5); } void test(int i){ if((state[(i+4)%5]!=EATING)&&(state[i]==HUNGRY)&&(state[(i+1)%5]!=EATING)) { state[i]=EATING; self[i].signal(); } } }
每个哲学家按照以下的顺序轮流调用操作pickup()和putdown()
dp.pickup(i) 吃饭 dp.putdown(i)
2)Q等待知道P离开管程(Lampson&Redll,MESA语言)
3)P的signal操作时P在管程内的最后一个语句(Hansen,并行Pascal)
linux同步机制
1)使用禁止中断来实现短的临界区
2)自旋锁(Spinlock)不会引起调用者阻塞
3)互斥锁(Mutex)
4)条件变量(Condition Variable)
5)信号量(Semaphore)
Windows同步机制
1)事件(Event)
2)临界区(Critical Section)
3)互斥锁(Metux)
4)自旋锁(Spinlock)
5)信号量(Semaphore)