【操作系统】进程与线程2
资料总结过程中发现有大佬整理过的文章。
进程同步
同步,可以称为直接制约关系,指为完成某种任务而建立多个进程,这些进程因为需要协调工作次序而产生的制约关系。最常见直接制约关系为合作。
进程互斥
互斥,可称间接制约关系,当一个进程访问某临界资源时,另一个想要访问该资源的进程必须等待。制约关系是竞争。
对临界资源的互斥访问,在逻辑上分为如下部分
do{
entry section; //进入区:检查是否可进入临界区,若可,则对其上锁
critical section; //临界区:访问资源的代码
exit section; //退出区:解锁
remainder section;//剩余区:其他处理
}while(true)
为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循的原则:
- 空闲让进:临界区空闲时,允许有请求的进程立即进入临界区
- 忙则等待:临界区被上锁后,其他进程需要进入临界区的进程必须等待
- 有限等待:对请求访问的进程,应保证能在有限时间内进入临界区
- 让权等待:当进程不能进入临界区时,应立即释放处理机,防止进程忙等待
互斥软件实现方法
单标志法
每个进程进入临界区的权限只能被另一个进程赋予。设置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原语唤醒,将其从阻塞态变为就绪态
}
实现同步互斥访问
互斥:
- 划定临界区
- 设置互斥信号量mutex,初值为1
- 进入区
P(mutex)
——申请资源,临界区之前 - 退出区
V(mutex)
——释放资源,临界区之后
不同临界资源设置不同互斥信号量,PV操作必须成对出现。
PV操作在一个进程中
同步: - 分析什么地方需要实现先后执行的关系。
- 设置同步信号量S,初值为0
- 先释放资源才能申请资源成功
只有当前操作执行完后释放资源,这样后操作执行时才可以申请到资源继续执行。否则后操作一直处于阻塞状态。也就是说,在前操作后执行V(S)
,在后操作前执行P(S)
。
PV操作并不在一个进程中。
生产者-消费者问题
问题描述:系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区取出一个产品并使用。生产者、消费者共享一个初始为空、大小为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]);
思考
}
如何防止死锁产生?以下任意一条规则均可:
- 最多允许4个人同时吃饭
- 在同时拿起两只筷子外加一个互斥锁
- 奇数先拿右筷子,偶数先拿左筷子。、
管程
一种高级同步机制,为了不关注复杂的PV操作。是一种特殊的软件模块,包括这些部分:(类似于Java里的类)
- 局部于管程的共享数据结构说明
- 对该数据结构进行操作的一组过程
- 对局部于管程的共享数据设置初始值的语句
- 管程有一个名字
管程的基本特征: - 局部于管程的数据只能被局部于管程的过程所访问。
- 一个进程只有通过调用管程内的过程才能进入管程访问共享数据。
- 每次仅允许一个进程在管程内执行某个内部过程
用管程解决生产者消费者问题:[[进程与线程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:方案一,得不到就放手。方案二,让OS协助剥夺。缺点:实现复杂,反复申请和释放导致系统开销大,可能导致饥饿
- 破坏条件3:运行前分配好所需资源,并一直保持拥有。缺:资源利用率低,可导致饥饿
- 破坏条件4:给资源编号,必须按编号从小到大的顺序申请资源。缺:不方便增加新设备,用户编程麻烦
- 避免死锁——银行家算法
安全序列:如果系统按照这种序列分配资源,则每个进程都能顺利完成,只要能找出一个安全序列,系统就是安全状态。如果系统处于安全状态,一定不会发生死锁,进入不安全状态,未必发生死锁,发送死锁,一定不安全。
经过计算可发现,(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️⃣:用安全性算法检查此次分配是否会导致系统进入不安全状态 - 处理死锁
资源分配图:
进程结点:圆圈表示
资源结点:对应一类资源
进程结点->资源结点:表示进程想申请几个资源,每条边代表一个
资源结点->进程结点:表示已经为进程分配了几个资源,每条边代表一个
如图,P1请求一个R2,R2分配出去了一个,所以P1可以被满足。然后P1归还资源,P2申请一个R1,此时因为P1已经归还资源,所以P2也能被满足。
检测死锁的算法:在资源分配图中,找到既不阻塞又不是孤点的进程Pi,即该点可以被满足。则消去它所有的请求边和分配边,使其变成孤点。当资源分配图能够消去图中所有的边,则称该图可完全简化,必不可能存在死锁。简化完图后,还连着边的进程就是死锁进程
解除死锁的方法:
- 资源剥夺法。挂起某些死锁进程,并抢占其资源。长时间被挂起的进程会饥饿。
- 撤销进程法。强制撤销部分、甚至全部死锁进程,并剥夺进程资源。代价很大。
- 进程回退法。让一个或多个死锁进程回退到可避免死锁的地步。需要系统记录进程的历史信息,设置还原点。
选择对哪个死锁进程操作的依据: - 进程优先级
- 已执行多长时间
- 还要多久完成
- 已使用多少资源
- 进程类型