文章目录
- 互斥锁
- 信号量
- 整形信号量
- 记录形信号量
- 利用信号量实现进程互斥
- 利用信号量实现同步
- 利用信号量实现前驱关系
互斥锁
现实中的锁有两种状态,打开和关闭,分别对应这资源可以被使用,和不可以被使用,我们可以通过使用钥匙对锁的状态进行改变
那么解决临界区的最简单的工具其实就是锁(mutex lock),我们称之为互斥锁
当一个进程进入临界区的时候,调用acquire函数,进行上锁,相当于不让别人再访问资源
退出临界区的时候调用release函数,来打开锁,可以让别人访问资源
一个锁只能上锁一次,因此在互斥锁中也有一个bool变量available,表示锁是否可以被上锁,可以被上锁时acquire函数就可以调用成功
这两个函数的执行必须是原子的,不允许被中断,因此通常都是硬件实现的
这种互斥锁也称之为自旋锁,主要问题是忙等待,也就是必须循环检查available是否可用
这种互斥锁可以用来解决同步问题
信号量
我们之前简单了解过信号量,本质上其实就是描述资源的数量
可以用来解决互斥和同步的问题,他只能被wait和signal原语访问,也称为PV操作,分别对应着申请和释放资源两个操作
整形信号量
这种就是最基础的用法,描述资源的数目
wait操作和P操作是申请操作,原理可以这样表示
void wait(int& S)
{while(S<=0); // 当资源不足时循环等待S--; // 足够了则申请
}
signal操作和S操作是释放操作
void signal(int& S)
{S++;
}
这种实现方法不遵循让权等待,会一直占用CPU
记录形信号量
这种信号量终于满足了不让忙等的机制,但是代价就是数据结构变得复杂
用一个结构体来描述,其中一个value表示资源数量,一个链表链接等待该资源的进程
struct semaphore // 信号量
{int value; // 资源数量list<PCB> L; // 等待PCB进程链表
};
然后用wait操作或者P操作进行资源申请
void wait(semaphore& S, PCB& new_L)
{S.value--;if(S.value < 0){S.L.push_back(new_L);block(new_L)}
}
这个操作的逻辑就是先将资源数量-1,如果此时资源数量小于0,则加入到等待队列中
这实际上是一种预定操作,虽然现在没有资源,但是我预定好了,并且一直处于等待队列中吗,并且手动block阻塞,不会让其占用CPU,等待释放资源,轮到我了,我再进行唤醒,我自然就可以使用了
void signal(semaphore& S) {S.value++;if (S.value <= 0) {PCB ret = S.L.front();S.L.pop_front();wakeup(ret);}
}
注意是小于等于的时候都要进行唤醒,因为是先进行的+1操作,说明在还没有进行唤醒之前,是小于0的,说明链表不为空
需要说明的是,这里仅是使用C++的list数据结构进行表示,操作系统中并非是用C++代码进行实现的,在Linux中,所有的操作都是使用C语言实现的
利用信号量实现进程互斥
当可用的资源只有一个的时候,实际上信号量就变成了锁
因此我们就可以直接用PV操作来表示申请资源和释放资源
// P1进程
P(S); // 进入区
// 临界区
V(S); // 退出区
// P2进程
P(S); // 进入区
// 临界区
V(S); // 退出区
P操作进去之前一定是需要上锁的
需要注意的是,对于不同的资源需要设置不同的信号量,PV操作也必须成对出现,不然一定会出问题
利用信号量实现同步
同步问题是我们需要不同进程之间的操作执行操作是按照一定顺序的
我们想要解决同步问题,就可以利用PV操作自带的锁,手动阻塞来实现
也就是先运行的进程,进行V操作释放操作,后运行的进程进行P操作申请操作
各自只执行一半
例如func1函数需要先执行,func2函数需要后执行,并且这两个函数分别属于P1和P2两个进程
// P1
func1();
V(S);
// P2
P(S);
func2();
因为两个进程是异步的,我们无法确定哪个进程先执行
但是可以确定的是,两个进程要么P1先执行,P2后执行,要么P2先执行,P1后执行
当P1先执行时,符合我们的要求,func1先执行,V操作释放资源,func2后执行,P操作申请资源,两个进程都相安无事
当P2操作先执行时,进行P操作申请资源,此时P1进程没有释放资源,所以P2进程被阻塞,真正原因其实是因为func1没有被执行,因此所谓的“资源”没有被释放,P2阻塞,于是P1进程运行,只有当P1进程释放资源之后,P2进程才能继续运行func2
利用信号量实现前驱关系
同步是两个进程之间的关系,而当多个进程都有前后制约关系时,我们称之为前驱关系
需要注意的时,进程之前可能没有先后关系,只是其中的某个函数或者语句有先后关系,这里只是为了方便描述
例如有下面的关系
我们可以对每一个箭头都设置一个信号量,或者对每一个节点设置一个信号量
这两种个思路其实都可以用来限制先后顺序,问题的难点在于如何分析题目画出这样的前驱图
例如下面是根据节点设置信号量
semaphore a1=a2=a3=a4=a5=a6=0;// P1
// ..
V(a1); // 表示1号操作完成// P2
P(a1); // 查看1号进程是否完成,未完成自动阻塞
// ..
V(a2); // 表示2号进程完成// P3
P(a1); // 查看1号进程是否完成,未完成自动阻塞
// ..
V(a3); // 表示3号进程完成// P4
P(a2); // 查看2号进程是否完成,未完成自动阻塞
// ..
V(a4); // 表示4号进程完成// P5
P(a2); // 查看2号进程是否完成,未完成自动阻塞
// ..
V(a5); // 表示5号进程完成// P6
P(a4),P(a5),P(a6); // 查看4,5,6号进程是否完成,未完成自动阻塞
// ..
另一种方法就是用来表示线,思路是一样的