引言
我们知道操作系统中的并发进程存在两种关系:独立或交互。交互的进程间可能会产生很多问题。比如买票这个事情,票是两者都在处理的对象,如果有很多人在同时买票,如果询问某一刻剩余多少票,往往会是一个不确定的结果。再比如内存管理中,两个进程访问同一资源,一者使用,另一者可能会陷入永远等待资源的情况。
当然了,无关的并发进程不会产生此类错误,判断并发的进程是无关的可以使用Bernstein条件。在此设R(pi)={a1,a2,…an}为程序pi在执行期间引用的变量集,W(pi)={b1,b2,…bm}为程序pi在执行期间改变的变量集,如果若两个程序的变量集交集之和为空集: R(p1)∩W(p2)∪R(p2)∩W(p1)∪W(p1)∩W(p2)={ },此时并发进程的执行与时间无关。
事实上,很多并发进程之间是交互的。进程间的交互分为竞争、协作和互斥,竞争为多个进程使用同一资源,协作则是同一件事需要多个进程分工完成,互斥是一种特殊的同步,逐次使用互斥资源。
在此基础上,我们引入一种机制来解决并发进程共享变量引起的与时间相关的错误,将共享变量代表的资源叫做临界资源,而涉及的程序段叫做临界区。临界区的原则是一个进程仅能让一个进程进入临界区,由此具体的实施方案如下:
在此,使用锁机制来实现互斥机制,进程在进入临界区前应该上锁,离开临界区时需要开锁,最重要的是进程见到锁必须等待直到锁被打开。
由此出现了Peterson算法:
int turn = 1 or 2; /* 令牌归谁所有,即目前占领临界区的进程对象*/
BOOL inside1 = false; /* P1不想进其临界区 */
BOOL inside2 = false; /* P2不想进其临界区 */
Process P1
{
inside1 = true; //1想进入临界区
turn = 2; //先礼貌的临界区让给p2
while (inside2 && //当2不想进入临界区时
turn == 2);
//临界区 //1使用临界区
inside1 = false; //使用完就不再想进临界区了
}
Process P2 //此处操作 同Process P1
{
inside2 = true;
turn = 1;
while (inside1 &&
turn == 1);
//临界区
inside2 = false;
}
我们又发现,操作系统在执行内核操作时不应被中断的,则应有一个最小粒度操作,我们将其定义为原子操作,也称为原语,是操作系统内核中执行时不可中断的过程。
PV操作和信号量
说了这么多,该介绍信号量了。信号量是一种广义上的锁,并有两个操作原语:P():如果信号量的值为负数则等待,否则将信号量值减1,V():将信号量的值加1,如果有进程在等待则唤醒它。通过使用可以发现,信号量用于制约控制并发进程的执行速度,是一种卓有成效的进程同步(互斥)机制!按照用途划分,信号量可以分为公共信号量和私有信号量。按照信号量的取值可以分为一般信号量和二元信号量。
PV操作解决进程间互斥问题是一般模式为:
semaphore mutex = 1;
Process Pi {
P(mutex);
临界区的操作
V(mutex);
}
信号量使用规则如下:
- 必须置一次且只能置一次初值
- 初值不能为负数
- 只能用于执行P、V操作
- 若s > 0,则s代表还可以使用的资源数,若s < 0,则s表示等待队列里的进程数
- P操作代表请求一个资源,V操作代表释放一个资源
PV操作必须成对出现,有一个P操作就一定有一个V操作。 当为互斥操作时,它们同处于同一进程。当为同步操作时,则不在同一进程中出现 。
如果P(full)或P(empty)和P(mutex)两个操作在一起,那么P操作的顺序至关重要,一个同步P操作与一个互斥P操作在一起时同步P操作在互斥P操作前,而两个V操作无关紧要。
同步问题的分析过程
- 有几个并发进程,公用缓冲区的个数
- 分析每个进程的执行流程,必要时画出它的流程图,找出在何时何处需要等待!
- 设置几个信号量?初始状态是什么?信号量初值设成多少?需要其它变量辅助吗?
- 公用缓冲区是否需要用互斥信号量保护?
实例
一、哲学家吃通心面问题
问题描述:5个哲学家围在桌旁,每人面前有一盘子,每两人之间放一把叉子。每个哲学家思考、饥饿,然后想吃通心面。只有拿到两把叉子的人才可以吃面,并且每人只能直接拿自己左手或右手边的叉子。
semaphore fork[5];
fork[i] = 1;
process Pi { //i=0,1,2,3,4
思考;
P(fork[i]);
P(fork[(i+1)mod 5]);
吃通心面;
V(fork[i]);
V(fork[(i+1)mod 5]);
}
解析:根据问题我们可以知道,因为必须两手都有叉子才可以吃面,这样必然不可能出现五个人同时吃面的情况,即我们假设每个人都开始拿左手边的叉子,大家都在等待右手边有叉子,那就会出现了永远等待的情况。解决的方法有很多,如:
- 至多允许四个哲学家同时吃;
- 奇数号先取左手边的叉子,偶数号先取右手边的叉子;
- 每个哲学家取到手边的两把叉子才吃,否则一把叉子也不取。
二、公交车上司机和售票员的合作
问题描述:
公共汽车上司机和售票员的活动分别如下:
司机:启动车辆;正常行车;到站停车
售票员:关车门;售票;开车门
同步要点:司机要等门关闭才能开车,售票员要等停车才能开门
思考和解决:
因为这是一个同步问题,司机和售货员要合作把公交车的活动进行下去,则使用私有信号量,为了让进程等待,私有信号量的初值一般为0。在此设置两个私有信号量分别指示司机和售票员的行动:semaphore s1=0,s2=0
则司机进程: 售票员进程:
P司机 { P售票员 {
P(s1); V(s1);
行车; 售票;
V(s2); P(s2);
} }
即售票员进程执行时先对s1执行V操作,可以理解为关车门操作。s1为1后,司机就剋启动车辆了,而这是又会对s1进行减1操作,售票员售票完毕后进行等待司机停车,再开车门。司机行车完毕后,司机就会对s2执行V操作,开车门,这时售票员终于可以进行开车门操作,对s2进行P操作。
司机此时进入了等待,当进入下一段行程,即售票员关了车门,司机进程则开始进行。周而复始的操作下去。
三、生产者-消费者问题
问题描述:
生产者(P)与消费者(C)共用一个缓冲区,P进程不能往“满”的缓冲区中放产品,C进程不能从“空”的缓冲区中取产品。 因为共享了缓冲区,故并发的P、C进程有可能产生与时间相关的错误,因而要采取措施使两进程执行同步。 分别为P和C进程设置两个信号量(私有还有公有?初值是多少?),用于指示进程是否可以执行。假设缓冲区只能存放一件产品,初始状态为空。
思考和解决:
同司机和售票员合作的问题,这也是一个经典的同步问题。我们也设置两个信号量,只要生产者生产完毕,则将标志生产者未生产的标志进行p操作,接着送产品到缓冲区供消费者使用,并对缓冲区有无食物进行V操作。消费者看到缓冲区有食物的信号后,才会开始进程,对缓冲区有食物的信号量进行P操作,并从缓冲区取产品,之后设置空标志。此时生产者看到缓冲区为空后,继续生产,再消除缓冲区为空的信号,这样周而复始下去。
Semaphore empty = 1; //生产者的信号量
Semaphore full = 0; //消费者的信号量
P {
while (true)
{
生产一个产品; P(empty);
送产品到缓冲区;
V(full);
}
}
C {
while (true)
{
P(full);
从缓冲区取产品;
V(empty);
消费产品;
}
}
四、苹果橘子问题
问题描述:
桌上有一只盘子,每次只能放入一只水果。爸爸专向盘子中放苹果(apple),妈妈专向盘子中放桔于(orange)。一个儿子专等吃盘子中的桔子,一个女儿专等吃盘子里的苹果。
问题分析:
先不说孝不孝顺的问题了。将盘子看作缓冲区,爸妈为生产者,儿女为消费者。这是多个生产/消费者,一个缓冲区,多种产品的问题。此时爸爸和女儿同步关系,妈妈和儿子同步关系,苹果和橘子为竞争关系。
设置三个信号量,sp用于指示盘子能放几个水果,初值=1,sg1,sg2分别用于指示桔子和苹果的个数,初值=0.
semaphore sp = 1; /* 盘子里允许放一个水果*/
semaphore sg1 = 0; /* 盘子里没有桔子 */
semaphore sg2 = 0; /* 盘子里没有苹果*/
Process father {
削一个苹果;
P(sp);
把苹果放入plate;
V(sg2);
}
Process daughter {
P(sg2);
从plate中取苹果;
V(sp);
吃苹果;
}
Process mother {
剥一个桔子;
P(sp);
把桔子放入plate;
V(sg1);
}
Process son {
P(sg1);
从plate中取桔子;
V(sp);
吃桔子;
}
五、读者写者问题
问题描述:
有两组并发进程:读者和写者,共享一个文件,要求:
允许多个读者同时执行读操作 ;
任一写者在完成写操作之前不允许其它读者或写者工作 ;
写者执行写操作前,应让已有的写者和读者全部退出 。
简单一句话:某一时刻,允许存在多个读者,但仅能有一个写者。
思考和解决:
一个写者到达时,如果发现有读者正在读,则等待;否则开始写。一个读者到达时,如果有写者正在写,则等待;否则开始读。
读者和写者会发生竞争问题,读者和读者可以共存。所以先设置一个信号量W解决读者和写者之间的竞争。
如何判断是否有读者以及读者离开时是最后一个?设置一个int变量rc记录当前正在读文件的读者数量。
semaphore W = 1; int rc = 0。
semaphore W = 1;
int rc = 0;
semaphore Mutex = 1;
process Writer {
P(W);
写文件;
V(W);
}
process Reader {
P(Mutex);
rc = rc + 1;
if rc == 1 then P(W);
V(Mutex);
读文件;
P(Mutex);
rc = rc - 1;
if rc == 0 then V(W);
V(Mutex);
}
六、理发师问题
问题描述:
理发店理有一位理发师、一把理发椅和n把供等候理发的顾客坐的椅子。如果没有顾客,理发师便在理发椅上睡觉,一个顾客到来时,它必须叫醒理发师,如果理发师正在理发时又有顾客来到,则如果有空椅子可坐,就坐下来等待,否则就离开。
思考和解决:
我服睡觉。
int waiting = 0; /*等候理发的顾客数*/
int CHAIRS = n; /*为顾客准备的椅子数*/
semaphore customers,barbers,mutex;
customers = 0; barbers = 1; mutex = 1;
barber() {
while(TRUE){
P(customers);
P(mutex);
waiting = waiting – 1;
V(mutex);
cut_hairs();
V(barbers);
}
}
customer() {
P(mutex);
if waiting < CHAIRS{
waiting = waiting + 1;
V(mutex);
P(barbers);
V(customers);
get_haircut();
}else{
V(mutex);
}
leave();
}
七、学生和监考老师
问题描述:
把学生和监考老师都看作进程,学生有N人,教师1人。考场门口每次只能进出一个人,进考场原则是先来先进。当N个学生都进入考场后,教师才能发卷子,学生交卷后可以离开考场,教师要等收上来全部卷子并封装卷子后才能离开考场。全部流程如下图所示,使用给定的信号量与变量,试用P、V操作解决上述问题中的同步和互斥关系。
思考和解决:
由题可知到,首先要设置学生人数信号量,达到N即学生满信号量进行了V操作后,教师进行发卷操作。老师发卷信号量V操作后,学生则开始答题。交卷离场后,学生为空V操作后,教师开始收卷对信号量进行P操作。
Semaphore entrymutex = 1 ; //入场/互斥
Semaphore waitforstudent = 0 ; //等待学生全部入场
Semaphore d_paper = 0 ; //发卷信号量
Semaphore g_paper = 0 ; //收卷信号量
int studentCount = 0
Process teacher {
P(entrymutex);
进考场
V(entrymutex);
P(waitforstudent);
for (i=1;i<=N;i++) //发卷
V(d_paper);
监考
P(g_paper); //等待学生交齐试卷
收/封试卷
P(entrymutex);
离场
V(entrymutex);
}
Process student_i {
//进考场
P(entrymutex);
studentCount++;
if (studentCount == N) then
V(waitforstudent);
V(entrymutex);
P(d_paper); //等待发卷
考试
//交卷离场
P(entrymutex);
studentCount--;
if (studentCount == 0) then
V(g_paper);
V(entrymutex);
}
通过几道例题的学习我们可以体会到,找到多方共同处理的信号量,厘清同步和竞争关系,然后合理安排PV操作即可。