什么是进程同步
答:进程同步指的是,由于进程并发执行具有异步性(即各自以独立地、不可预知的速度向前推进),但是某些情况下又需要进程之间进行配合和协调来完成一项工作(存在执行的顺序),因此操作系统需要提供一种机制来实现这一功能,即进程同步。
例:
读进程和写进程并发地执行,由于并发必然导致异步性,因此“写数据“和”读数据“两个操作执行的先后顺序是不确定的。而实际中,又必须按照”写数据->读数据“的顺序来执行。
同步亦称直接制约关系,它是指为了完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
什么是进程互斥
-
进程同步需要”共享“的支持。各个并发执行的进程不可避免地需要共享一些资源(比如内存、打印机、摄像头)
-
两种资源共享的方式:
-
互斥共享:一段时间内只允许一个进程访问
-
同时共享:允许一段时间内多个进程”同时“方法(为什么同时加引号,因为CPU并发执行进程,没有真正的并行执行)
-
对临界资源的互斥访问,可以在逻辑上分为如下四个部分
- 进入区:负责检查是否可以进入临界区,进入则上锁
- 临界区:进程中访问临界资源的代码
- 退出区:负责解除锁
- 剩余区:做其他处理
-
为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以后原则:
- 空闲让进:临界区空闲,允许一个请求进入临界区的进程立即进入临界区
- 忙则等待:当已有进程进入临界区时,其他试图进入临界区的进程必须等待
- 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)
- 让权等待:当进程不能进入临界区时,应该立即释放处理机,防止进程忙等待。(四种软件实现方法:单标志检查法、双标知)
进程互斥的软件实现方法
单标志法
双标志先检查法
-
算法思想:设置一个布尔型数组flag[],数组中各个元素用来标记各进程想进入临界区的意愿。每个进程在进入临界区之前先检查当前有没有别的进程想要进入临界区,如果没有,则把自身对应的标志设置为true,之后开始访问临界区。
bool flag[2];
flag[0]=false;
flag[1]=false;
while(flag[1]);
flag[0]=true;
critical section;
flag[0]=false;
remainder section;
while(flag[0]);
flag[1]=true;
critical section;
flag[1]=false;
remainder section;
-
双标志先检查法存在的主要问题:违反忙则等待原则。
- 原因在于:进入区的检查和上锁两个处理不是一气呵成的(不是原子性)。如果检查通过后,在访问临界区之前发生进程切换,则会造成多个进程同时访问临界区。
双标志后检查法
-
算法思想:双标志先检查法的改进版。通过先上锁,后检查来避免先检查法的缺陷。
bool flag[2];
flag[0]=false;
flag[1]=false;
flag[0]=true;
while(flag[1]);
critical section;
flag[0]=false;
remainder section;
flag[1]=true;
while(flag[0]);
critical section;
flag[1]=false;
remainder section;
-
存在的主要问题:违背了有限等待和空闲让进原则。会因各进程都无法长期访问临界资源而产生饥饿现象。
Peterson算法
-
算法思想:主动让对方先使用临界区。
bool flag[2];
int turn=0;
flag[0]=false;
flag[1]=false;
flag[0]=true;
turn=1;
while(flag[1] && turn==1);
critical section;
flag[0]=false;
remainder section;
flag[1]=true;
turn=1;
while(flag[0] && turn ==1);
critical section;
flag[1]=false;
remainder section;
-
Peterson算法存在的主要问题:违背了让权等待原则(当进程无法进入临界区时,应该立即释放处理机)。 Peterson算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待三个原则。但是依然不够好。
进程互斥的硬件实现方法
中断屏蔽方法
利用开/关中断指令实现。关中断后,无法发生中断切换进程,则不存在两个进程同时访问某个临界区。
关中断;
临界区;
开中断;
优点:简单、高效
缺点:
- 不适合多处理机(关中断只对某个处理机管用)
- 只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用非常危险)
- 自己想的一条缺点:关中断是无差别攻击,使得别的许多没有与当前进程竞争临界区的进程也无法获得处理机,降低了操作系统的灵活性。
TestAndSet(TS指令/TSL指令)
简称TS指令,也有些地方称为TestAndSetLock指令,或者TSL指令。TSL指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成。以下是用C语言描述的逻辑。
bool TestAndSet(){
bool old;
old=*lock;
*lock=true;
return old;
}
while(TestAndSet(&lock));
critical section;
lock=false;
remainder section;
优点:实现简单,把“上锁”和“检查“通过硬件的方式变成原子操作;适用于多处理机环境。
缺点:仍然不满足让权等待原则(无法进入临界区时释放处理机)。 原因:暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致忙等。
Swap指令(XCHG指令)
Swap指令,又叫XCHG指令,是用硬件实现的,执行的过程中不允许被中断。以下是C语言描述的逻辑
Swap(bool *a,bool *b){
bool tmp;
tmp=*a;
*a=*b;
*b=tmp;
}
bool old=true;
while(old==true)
Swap(&lock,&old);
临界区代码...
lock=false;
剩余区代码...
逻辑上和TSL一样,优缺点和TSL也一样,不满足让权等待。
信号量机制
1965年,荷兰学者Dijkstra提出了一种卓有成效的实现进程互斥、同步的方法–信号量机制。
用户进程可以通过操作系统提供的一对原语(原语:无法中断、连续执行的一组操作)来对信号量进行操作,从而很方便地实现了进程互斥、进程同步。
信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为1的信号量。
一对原语:wait(S)和signal(S),信号量为S。 wait、signal原语简称为P、V操作(来自荷兰语proberen和verhogen)。
整型信号量
用一个整数型的变量作为信号量,用来标识系统中某种资源的数量。
与普通整型变量的区别:对信号量的操作只有三种,初始化、P操作、V操作
e.g. 某计算机系统中有一台打印机
int S=1;
void wait(int S){
while(S<=0);
S=S-1;
}
void signal(int S){
S=S+1;
}
...
wait(S);
使用打印机资源
signal(S);
...
...
wait(S);
使用打印机资源...
signal(S);
检查和上锁一气呵成,避免了并发、异步导致的问题。
存在的问题:不满足“让权等待”,会发生“忙等”
记录型信号量
整形信号量的缺陷是存在“忙等”问题,因此人们又提出了“记录型信号量”,即用记录型数据结构标识的信号量。
typedef struct{
int value;
struct process *L;
}semaphore;
void wait(semaphore S){
S.value--;
if(S.value<0)
block(S.L);
}
void signal(semaphore S){
S.value++;
if(S.value<=0)
wakeup(S.L);
}
wait(S), signal(S)也可以记为P(S)、V(S)
该机制遵循了让权等待原则,不会发生忙等现象。原因在于,当一个进程无法进入临界区时,会自我阻塞!从而释放处理机的占用。
用信号量实现进程的互斥、同步、前驱关系
实现进程互斥
- 分析并发进程的关键活动,划分临界区(如,打印机的访问就应该放在临界区)
- 设置互斥信号量mutex,初值为1
- 在临界区之前执行P(mutex)
- 在临界区之后执行V(mutex)
semaphore mutex=1;
P1(){
...
P(mutex);
临界区代码
V(mutex);
...
}
P2(){
...
P(mutex);
临界区代码
V(mutex);
...
}
P、V操作需要成对出现。缺少P不能保证互斥访问,缺少V则会导致资源永远不被释放,等待进程永远不会被唤醒。
注意,对不同的临界资源要设置不同的互斥信号量
实现进程同步
进程同步:要让并发进程按照要求有序地推进。
用信号量实现进程同步:
- 分析什么地方需要实现“同步关系”,即必须保证一前一后执行的两个操作(或者两句代码 )
- 设置同步信号量S,初值为0
- 在“前操作”之后执行V(S)
- 在“后操作”之前执行P(S)
实现进程的前驱关系
前驱关系问题,本质上就是更复杂的同步问题
生产者消费者问题
问题描述:系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区取出一个产品并使用。生产者、消费者共享一个初始为空、大小为n个缓冲区。
semaphore mutex1=1;
semaphore full=0;
semaphore empty=n;
Producer(){
while(1){
生产产品
P(emoty);
P(mutex1);
将产品放入缓冲区
V(mutex1);
V(full);
}
}
Consumer(){
while(1){
P(full);
P(mutex1);
取出产品
V(mutex1);
V(empty);
}
}
实现互斥的P操作一定要在实现同步的P操作之后,否则会发生死锁;V操作不会导致进程阻塞,因此两个V操作顺序可以交换。
semaphore mutex=1;
semaphore empty=9;
semaphore A3=0;
semaphore A4=0;
PutPaper(){
while(1){
P(empty);
P(mutex);
if(放A3纸)
V(A3);
else(放A4纸)
V(A4);
V(mutex);
}
}
GetPaper(){
while(1){
if(取A3纸){
P(A3);
P(mutex);
取A3纸
V(mutex);
V(empty);
}else{
P(A4);
P(mutex);
取A4纸;
V(mutex);
V(empty);
}
}
}
多生产者-多消费者问题
问题描述:桌子上有一只盘子,每次只能向其中放入一个水果。爸爸转向盘子中放苹果,妈妈专向盘子中放橘子,儿子专门吃橘子,女儿专门吃苹果。只有盘子为空时,爸爸或者妈妈才能向盘子中放入一个水果。仅当盘子中有自己需要的水果时,儿子或女儿才可以从盘子中取出水果。请用PV操作实现上述过程。
semaphore apple=0;
semaphore orange=0;
semaphore empty=1;
Father(){
while(1){
P(empty);
放入苹果;
V(apple);
}
}
Mother(){
while(1){
P(empty);
放入橘子;
V(orange);
}
}
Son(){
while(1){
P(apple);
拿出苹果;
V(empty);
}
}
Daughter(){
while(1){
P(orange);
拿出橘子;
V(empty);
}
}
如果将题目中的缓冲区数量改成2,则需要假如mutex,对盘子的互斥访问。
semaphore mutex=0;
semaphore apple=0;
semaphore orange=0;
semaphore empty=2;
Father(){
while(1){
P(empty);
P(mutex);
放入苹果;
V(mutex);
V(apple);
}
}
Mother(){
while(1){
P(empty);
P(mutex);
放入橘子;
V(mutex);
V(orange);
}
}
Son(){
while(1){
P(apple);
拿出苹果;
V(empty);
}
}
Daughter(){
while(1){
P(orange);
拿出橘子;
V(empty);
}
}
吸烟者问题
问题描述:一个系统有三个抽烟者进程和一个供应者进程。每个抽烟者不停地卷烟并且抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸、第三个拥有胶水。供应者无限地提供三种材料,供应者每次将两种材料放在桌子上,拥有剩下那种材料的抽烟者卷起一根烟并抽掉它,并给供应者进程一个信号告诉他完成了,供应者就会放另外两种材料在桌上,这一过程一直重复(让三个抽烟着轮流地抽烟)。
semaphore m1=0,m2=0,m3=0;
semaphore table=1;
int turn=0;
Producer(){
while(1){
P(table);
if(turn==0){
在桌子上放烟草和纸
V(m1);
}else if(turn==1){
在桌子上放纸和胶水
V(m2);
}else{
在桌子上放胶水和烟草
V(m3);
}
turn=(turn+1)%3;
}
}
Consumer1(){
while(1){
P(m1);
卷烟,抽烟
V(table);
}
}
Consumer2(){
while(1){
P(m2);
卷烟,抽烟
V(table);
}
}
Consumer3(){
while(1){
P(m3);
卷烟,抽烟
V(table);
}
}
缓冲区数量为1,实际运行过程中同一时刻只可能有一个进程访问缓冲区,因此无需额外设置mutex对缓冲区加锁
读者-写者问题
问题描述:一个系统中有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但是若某个写进程和其他进程(读/写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:
- 允许多个读者可以同时对文件执行读操作
- 只允许一个写者前往文件中写信息
- 任一写者在完成操作之前,不允许其他读者或写者工作
- 写者执行写操作前,应让已有的读者和写者全部退出。
semaphore mutex=1;
semaphore lock=1;
int reader=0;
Writer(){
while(1){
P(lock);
写入文件
V(lock);
}
}
Reader(){
while(1){
P(mutex);
if(reader==0){
P(lock);
}
reader++;
V(mutex);
读文件
P(mutex);
reader--;
if(reader==0)
V(lock);
V(mutex);
}
}
写者优先存在的问题:只要有一个读者残留,那么剩余读者就会源源不断地进入,导致写者饥饿(读者会在写者之前插队)
semaphore mutex=1;
semaphore lock=1;
semaphore w=1;
int reader=0;
Writer(){
while(1){
P(w);
P(lock);
写入文件
V(lock);
V(w);
}
}
Reader(){
while(1){
P(w);
P(mutex);
if(reader==0){
P(lock);
}
reader++;
V(mutex);
V(w);
读文件
P(mutex);
reader--;
if(reader==0)
V(lock);
V(mutex);
}
}
哲学家进餐问题
一张圆桌上坐着五名哲学家,每两个哲学家之间的桌上摆了一根筷子,桌子的中间是一碗米饭。哲学家们思考和进餐,只有饥饿时会试图拿起左右两根筷子(一根一根拿起)。如果筷子已经在他人手上,这需要等待。进餐完毕后,放下筷子继续思考。
- 关系分析:筷子的访问是互斥关系
- 整体思路:每个哲学家进程需要同时持有两个临界资源才能开始进餐。如何避免临界资源分配不当造成的死锁现象,是哲学家就餐问题的精髓。
- 信号量设置。定义互斥信号量数组chopstick[5]={1,1,1,1,1}用于实现5根筷子的互斥访问。并对哲学家按0~4进行编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5。‘
如果不施加任何条件,则会发生死锁
semaphore chopstick[5]={1,1,1,1,1};
Pi(){
P(chopstick[i]);
P(chopstick[(i+1)%5]);
就餐
V(chopstick[i]);
V(chopstick[(i+1)%5]);
思考
}
如何防止死锁的发生?可以施加一些条件(下面任一条件均可防止死锁)
- 最多允许4人同时用餐
- 同时拿起两根筷子,如果做不到则等待
- 奇数先拿右手筷子,偶数先拿左手筷子(保证了相邻两个哲学家如果都拿起第一只筷子以后,其中一人可以拿起另一只吃饭,另一个人阻塞)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)