【操作系统】进程与线程2

资料总结过程中发现有大佬整理过的文章

进程同步

同步,可以称为直接制约关系,指为完成某种任务而建立多个进程,这些进程因为需要协调工作次序而产生的制约关系。最常见直接制约关系为合作。

进程互斥

互斥,可称间接制约关系,当一个进程访问某临界资源时,另一个想要访问该资源的进程必须等待。制约关系是竞争。
对临界资源的互斥访问,在逻辑上分为如下部分

do{
	entry section;    //进入区:检查是否可进入临界区,若可,则对其上锁
	critical section; //临界区:访问资源的代码
	exit section;     //退出区:解锁
	remainder section;//剩余区:其他处理
}while(true)

为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循的原则:

  1. 空闲让进:临界区空闲时,允许有请求的进程立即进入临界区
  2. 忙则等待:临界区被上锁后,其他进程需要进入临界区的进程必须等待
  3. 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区
  4. 让权等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待

互斥软件实现方法

单标志法

每个进程进入临界区的权限只能被另一个进程赋予。设置turn变量。

int turn = 0;//表示当前允许进入临界区的进程号是0`
//P0
while(turn != 0);
critical section;
turn = 1;//愿意让P1访问
remainder section;
//P1
while(turn != 1);
critical section;
turn = 0;
remainder section;

turn初始值为0,刚开始只允许P0进程进入临界区。若P1先上处理机运行,则一直执行while循环判断,直到P1时间片用完,发生调度,切换P0上处理机运行。P0访问临界资源,只有当P0将turn的值修改为1后,P1上处理机运行才能访问临界资源。
主要违反空闲让进原则。如果P0访问完后将权限交给P1,但P1并没有访问临界区的需求,虽然此时临界区空闲,但P0并不被允许访问。

双标志法

设置布尔数组flag[],标记各进程想进入临界区的意愿。每个进程i在进入临界区前先检查有无别的进程想进入临界区,如果没有则flag[i]=true,访问临界区。

bool flag[2];
flag[0] = false;
flag[1] = false;
//P0
while(flag[1]);//检查是否P1有意愿,如果P1有意愿P0可以等
flag[0] = true;//上锁
critical section;
flag[0] = false;//解锁
remainder section;
//P1
while(flag[0]);
flag[1] = true;
critical section;
flag[1] = false;
remainder section;

主要违反忙则等待原则。如果P0发现P1没有意愿准备上锁时,时间片用完。P1发现P0也没有意愿准备上锁时,时间片用完。然后P0上锁,P1也上锁,两个进程则会同时访问临界区。
原因在于:检查与上锁两个处理并不是一气呵成的。

双标志后检查法
//P0
flag[0] = true;//先表达P0有意愿
while(flag[1]);//检查是否P1有意愿,如果有P0可以等
critical section;
flag[0] = false;//解锁
remainder section;
//P1
flag[1] = true;
while(flag[0]);
critical section;
flag[1] = false;
remainder section;

主要违反空闲让进和有限等待原则,会因各进程都长期无法访问临界资源产生饥饿现象。如果P0先上锁,然后P1也上锁。那么P0会一直while,P1也会一直while。

Peterson算法
bool flag[2];
int turn = 0;
//P0
flag[0] = true;//P0想访问
turn = 1;//愿意让P1先访问
while(flag[1] && turn == 1);//如果P1有意愿且P1不想让P0访问,P0等待
critical section;
flag[0] = false;//解锁
remainder section;
//P1
flag[1] = true;
turn = 0;
while(flag[0] && turn == 0);
critical section;
flag[1] = false;
remainder section;

主要违反了让权等待原则。如果进程一直得不到访问权,则在自己的时间片中一直处于while。
但以上所有解决方案都无法实现让权等待。

互斥硬件实现方法

中断屏蔽
关中断;//不允许当前进程被中断
临界区;//
开中断;//

优:简单高效
缺:只适用于操作系统内核进程,不适用于用户进程

TestAndSet指令

TSL指令是用硬件实现,执行过程不允许被中断,具有原子性。

//lock表示当前临界区是否被加锁,
//该方法内的代码不不会被打断,具有一气呵成
bool TestAndSet(bool *lock){ 
	bool old=*lock; //old用来存放lock原来的值 
	*lock=true; //有进程需要使用临界区,将lock设为true 
	return old; //返回lock原来的值,也就是确定有没有人正在用此临界区。返回false,表示没有人。 
}
				  
while(TestAndSet(&lock)); //上锁并检查 
critical section;
lock=false; //解锁 
remainder section; 

不支持让权等待。暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致忙等。

Swap指令

也叫做Exchange指令。在逻辑上与TSL指令一样,硬件实现可能不同。

Swap(bool *a,bool *b){
    bool tmp;
    tmp=*a;
    *a=*b;
    *b=tmp;
}

//lock表示当前临界区是否被加锁
bool old=true;
while(old==true)
    Swap(&lock,&old);
critical section;
lock=false;
remainder section; 
互斥锁mutex lock

一个进程在进入临界区时应获得锁acquire(),在退出临界区时释放锁release()。每个互斥锁有一个布尔变量avaiable,表示锁是否可用。
无法解决让权等待原则。需要连续循环忙等的互斥锁,都可称为自旋锁,比如TSL、Swap、单标志法
等待期不用切换进程上下文。常用于多处理器系统,一个核忙等,其他核照常工作。

也就是说以上所有关于互斥的实现方法都无法解决让权等待。

信号量机制实现进程同步互斥

用户进程可通过使用OS提供的一对原语对信号量操作。原语其实是由关开中断指令实现的一个函数。信号量其实是一个变量
一对原语:wait(S)和signal(S)。这两个常简称为P、V操作。

整形信号量

用整型变量表示系统中某种资源的数量。没有解决让权等待。

int S=1;//某种资源的数量,比如打印机
void wait(int S){
    while(S<=0);
    S=S-1;
}
void signal(int S){
    S=S+1;
}
//进程P0
wait(S);
critical section;
signal(S);
remainder section; 
    
//进程P1
...
wait(S);
critical section;
signal(S);
remainder section; 
记录型信号量

用记录型数据结构表示的信号量。进程能主动进行自我阻塞,因此符合让权等待。

typedef struct{
	int value;//剩余资源数,如果是负数,则表示现在有多少进程在等待
	struct process *L;//等待队列
}semaphore;
void wait(semaphore S){
	S.value--;
	if(S.value<0)
		block(S.L);//剩余资源不够,使用block原语阻塞运行的进程,并将其放入S的等待队列中,将其从运行态变为阻塞态
}
void signal(semaphore S){
	s.value++;
	if(s.value<=0)
		wakeup(S.L);//释放资源后,若有进程在等待该资源,则使用wakeup原语唤醒,将其从阻塞态变为就绪态
}
实现同步互斥访问

互斥:

  1. 划定临界区
  2. 设置互斥信号量mutex,初值为1
  3. 进入区P(mutex)——申请资源,临界区之前
  4. 退出区V(mutex)——释放资源,临界区之后
    不同临界资源设置不同互斥信号量,PV操作必须成对出现。
    PV操作在一个进程中
    同步:
  5. 分析什么地方需要实现先后执行的关系。
  6. 设置同步信号量S,初值为0
  7. 先释放资源才能申请资源成功
    只有当前操作执行完后释放资源,这样后操作执行时才可以申请到资源继续执行。否则后操作一直处于阻塞状态。也就是说,在前操作后执行V(S),在后操作前执行P(S)
    PV操作并不在一个进程中。
    pic

生产者-消费者问题

问题描述:系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区取出一个产品并使用。生产者、消费者共享一个初始为空、大小为n个缓冲区。
只有缓冲区不满,生产者才能放产品。只有缓冲区不空,消费者才能取产品,这里是同步关系。缓冲区是临界资源,各进程必须互斥访问。

semaphore full = 0;//同步信号量
semaphore empty = 1;//同步信号量
semaphore mutex = 1;//互斥信号量
producer(){
	while(1){
		生产
		P(empty);//消耗一个空闲缓冲区
		P(mutex);//对缓冲区上锁,注意两个信号量的顺序
		产品放入缓冲区
		V(mutex);//解锁
		V(full);//增加一个产品
	}
}
consumer(){
	while(1){
		P(full);
		P(mutex);
		缓冲区取产品
		V(mutex);
		V(empty);
		使用产品
	}
}

多生产者-多消费者问题

问题描述:生产者1生产产品1,消费者1消费产品1。生产者2生产产品2,消费者2消费产品2。但却只有一个缓冲区的问题。
同步关系:
生产者1生产后,消费者1消费。s1=0
生产者2生产后,消费者2消费。s2=0
缓冲区为空时,生产者才能放入产品。empty=1
互斥关系:
对缓冲区的访问要互斥进行。mutex=1

semaphore s1 = 0;
semaphore s2 = 0;
semaphore empty = 1;
semaphore mutex = 1;
producer1(){
	生产产品1
	P(empty);
	P(mutex);
	放入产品1
	V(mutex);
	V(s1);
}
consumer1(){
	P(s1);
	P(mutex);
	取出产品1
	V(mutex);
	V(empty);
	使用产品1
}
//-------------------
producer2(){
	生产产品2
	P(empty);
	P(mutex);
	放入产品2
	V(mutex);
	V(s2);
}
consumer2(){
	P(s2);
	P(mutex);
	取出产品2
	V(mutex);
	V(empty);
	使用产品2
}

吸烟者问题

问题描述:假设系统中三个抽烟者,一个供应者。抽烟者需要三种材料:烟草、纸和胶水。抽烟者A拥有烟草,抽烟者B拥有纸,抽烟者C拥有胶水。供应者无限提供三种材料,每次将两种材料放桌子上,某位抽烟者就会抽一根烟,并告诉供应者抽完了,供应者则会继续放材料,让三个抽烟者轮流抽烟。
临界区:桌子
同步:
桌子材料满足抽烟者A,A抽烟。off1=0
桌子材料满足抽烟者B,B抽烟。off2=0
桌子材料满足抽烟者C,C抽烟。off3=0
发出完成信号,供应者放东西。finish=1
三个抽烟者轮流抽烟。int i = 0

semaphore off1=0,off2=0,off3=0,finish=1;
int i=0;
producer(){
	while{
		P(finish);
		if(turn==0){
	        在桌子上放烟草和纸
	        V(off1);
	    }else if(i==1){
	        在桌子上放纸和胶水
	        V(off2);
	    }else{
	        在桌子上放胶水和烟草
	        V(off3);
	    }
	    i=(i+1)%3;
	}
}
consumer1(){
	while(1){
		P(off1);
		抽烟
		V(finish);
	}
}
...

当缓冲区为1时,实际运行过程中同一时刻只可能有一个进程访问缓冲区,因此无需额外设置mutex对缓冲区加锁。但如果有两个及以上桌子时,则必须使用mutex。

读-写问题

问题描述:系统中有读写两组并发进程,共享一个文件,可同时访问,但不能同时写或写读。
临界区:一个文件
互斥关系:写进程-读进程,写进程-写进程

semaphore rw = 1;
semaphore mutex = 1;//对count变量的互斥操作
int count = 0;//当前有几个读进程在访问文件
semaphore w = 1;//没有此信号量,读进程是优先的。写进程会一直阻塞等待,会饿死。因此它用于实现写优先。
write(){
	while(1){
		P(w)
		P(rw);
		写文件
		V(rw);
		V(w);
	}
}
//第一个读进程对文件上锁,最后一个读进程对文件解锁。count的值也需要读进程间互斥访问。
reader(){
	while(1){
		P(w);
		P(mutex);
		if(count==0)
			P(rw);
		count++;
		V(mutex);
		V(w);
		读文件
		P(mutex);
		count--;
		if(count==0)
			V(rw);
		V(mutex);
	}
}

读1->写1->读2:写1会被阻塞在P(rw),而读2会被阻塞在P(w),此时只有读1能够顺利运行,运行完后发现count=0,则释放rw,此时写1则可以运行。

哲学家问题

问题描述:一个圆桌5个哲学家,每两个人中间摆了一根筷子。哲学家思考和进餐。只有饥饿时会试图拿起左右两根筷子,如果筷子在别人手中,则需要等待。进餐完毕后,放下筷子继续思考。
筷子的访问是互斥的。
思路:当每个哲学家需要同时持有两个临界资源才能开始进餐。如何避免临界资源分配不当造成的死锁现象。

semaphore chopstick[5]={1,1,1,1,1};
semaphore mutex=1;
pi(){//i号哲学家
	P(mutex);
	P(chopstick[i]);//拿左筷子
	P(chopstick[(i+1)%5]);//拿右筷子
	V(mutex);
	吃饭
	V(chopstick[i]);
	V(chopstick[(i+1)%5]);
	思考	
}

如何防止死锁产生?以下任意一条规则均可:

  1. 最多允许4个人同时吃饭
  2. 在同时拿起两只筷子外加一个互斥锁
  3. 奇数先拿右筷子,偶数先拿左筷子。、

管程

一种高级同步机制,为了不关注复杂的PV操作。是一种特殊的软件模块,包括这些部分:(类似于Java里的类)

  1. 局部于管程的共享数据结构说明
  2. 对该数据结构进行操作的一组过程
  3. 对局部于管程的共享数据设置初始值的语句
  4. 管程有一个名字
    管程的基本特征:
  5. 局部于管程的数据只能被局部于管程的过程所访问。
  6. 一个进程只有通过调用管程内的过程才能进入管程访问共享数据。
  7. 每次仅允许一个进程在管程内执行某个内部过程
    用管程解决生产者消费者问题:[[进程与线程2#生产者-消费者问题|相关内容]]
monitor producerConsumer
	condition full,empty;
	void insert(Item item){//放入一个产品
		if(count==N)
			wait(full);
		count++;
		insert_item(item);
		if(count==1)//当count==1时证明可能有消费者在被阻塞,因此需要唤醒被阻塞的消费者
			signal(empty);
	}
	Item remove(){//取出一个产品
		if(count==0)
			wait(empty);
		count--;
		if(count==N-1)//证明缓冲区是满的,现在不满了,可以唤醒生产者进程了
			signal(full)
		return remove_item();//返回的是一个指针
	}
end monitor;
//——————————
producer(){
	while(1){
		item=生产的产品
		ProducerConsumer.insert(item);
	}
}
consumer(){
	while(1){
		item=ProducerConsumer.remove();
		消费产品item
	}
}

Java中如果用关键字synchronized描述一个函数,则该函数在同一时间段内只能被一个线程调用。

死锁

在并发环境下,每个进程都占有一个资源,同时又在等待另一个进程占有的资源,发生死锁。

死锁:各进程互相等待对方手里的资源,导致各进程都阻塞,无法向前推进。进程一定处于阻塞态。
饥饿:由于长期得不到想要的资源,某进程无法向前推进的现象。进程可能处于阻塞态也可能处于就绪态
死循环:某进程执行过程中一直挑不出循环的现象。进程处于运行态。

产生死锁的条件:必须同时满足以下四个条件

  1. 互斥条件:对必须互斥使用的资源的争抢
  2. 不剥夺条件:进程获得的资源在未使用完前,不能由其他进程强行夺走,只能主动释放
  3. 请求和保持条件:进程已有资源,但又提出新的资源请求,而该资源被其他进程占有。此时请求进程被阻塞,但对自己手头的资源不放
  4. 循环等待条件:循环等待链
死锁的处理
  1. 预防死锁:破坏产生死锁的四种条件之一即可
  • 破坏条件1:将临界资源改造为可共享使用的资源。缺:可行性不高
  • 破坏条件2:方案一,得不到就放手。方案二,让OS协助剥夺。缺点:实现复杂,反复申请和释放导致系统开销大,可能导致饥饿
  • 破坏条件3:运行前分配好所需资源,并一直保持拥有。缺:资源利用率低,可导致饥饿
  • 破坏条件4:给资源编号,必须按编号从小到大的顺序申请资源。缺:不方便增加新设备,用户编程麻烦
  1. 避免死锁——银行家算法
    安全序列:如果系统按照这种序列分配资源,则每个进程都能顺利完成,只要能找出一个安全序列,系统就是安全状态。如果系统处于安全状态,一定不会发生死锁,进入不安全状态,未必发生死锁,发送死锁,一定不安全。
    例子
    经过计算可发现,(3,3,2)可满足P1和P3,然后更新剩余可用资源,继续进行计算。
    假设系统中有n个进程,m种资源:
    Max[n][m]:表示各进程对资源的最大需求数
    Allocation[n][m]:表示已经给各进程分配了多少资源
    Max-Allocation=Need:表示各进程还需要多少资源
    Available[m]:表示还有多少可用资源
    Request[m]:表示某进程i此次申请的各种资源数
    步骤:
    1️⃣:检查此次申请是否超过了之前声明的最大需求数Request[j]<=Need[i,j]
    2️⃣:检查此时系统剩余的可用资源是否满足此次要求Request[j]<=Available[j]
    3️⃣:试探分配,更改数据结构Available,Allocation,Need
    4️⃣:用安全性算法检查此次分配是否会导致系统进入不安全状态
  2. 处理死锁
    资源分配图:
    进程结点:圆圈表示
    资源结点:对应一类资源
    进程结点->资源结点:表示进程想申请几个资源,每条边代表一个
    资源结点->进程结点:表示已经为进程分配了几个资源,每条边代表一个
    资源分配图
    如图,P1请求一个R2,R2分配出去了一个,所以P1可以被满足。然后P1归还资源,P2申请一个R1,此时因为P1已经归还资源,所以P2也能被满足。
    检测死锁的算法:在资源分配图中,找到既不阻塞又不是孤点的进程Pi,即该点可以被满足。则消去它所有的请求边和分配边,使其变成孤点。当资源分配图能够消去图中所有的边,则称该图可完全简化,必不可能存在死锁。简化完图后,还连着边的进程就是死锁进程
    解除死锁的方法:
  • 资源剥夺法。挂起某些死锁进程,并抢占其资源。长时间被挂起的进程会饥饿。
  • 撤销进程法。强制撤销部分、甚至全部死锁进程,并剥夺进程资源。代价很大。
  • 进程回退法。让一个或多个死锁进程回退到可避免死锁的地步。需要系统记录进程的历史信息,设置还原点。
    选择对哪个死锁进程操作的依据:
  • 进程优先级
  • 已执行多长时间
  • 还要多久完成
  • 已使用多少资源
  • 进程类型
posted @ 2023-05-23 20:31  梅落南山  阅读(17)  评论(0编辑  收藏  举报