目录
基本概念
主要任务
对多个相关进程在执行次序上进行协调,使并发执行的诸进程之间能按照一定的规则(或时序)共享系统资源,并能很好地相互合作,从而使程序的执行具有可再现性。
两种形式的制约关系
1) 间接相互制约关系(譬如:进程间共享系统的互斥硬件资源)
2) 直接相互制约关系(譬如:合作完成一个任务,共享同一个缓冲区的数据)
临界资源(Critical Resouce)
临界资源是一次仅允许一个进程使用的共享资源。各进程采取互斥的方式,实现共享的资源称作临界资源。属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。
临界区(critical section)
不论是硬件临界资源还是软件临界资源,多个进程必须互斥地对它进行访问。人们把在每个进程中访问临界资源的那段代码称为临界区(critical section)。
一次只允许一个进程进入的该进程的那一段代码!
while(true)
{
进入区(检查欲访问的临界资源标志,置为访问中)
临界区(访问临界资源)
退出区(修改临界资源的访问标志,置为未被访问)
剩余区
}
同步机制应遵循的规则
为实现进程互斥地进入自己的临界区,可用软件方法,更多的是在系统中设置专门的同步机构来协
调各进程间的运行。所有同步机制都应遵循下述四条准则:
(1) 空闲让进。(若干进程要求进入空闲临界区时,若资源空闲, 应尽快使一进程进入临界区)
(2) 忙则等待。
(3) 有限等待。(从进程发出进入请求到允许进入,不能无限等待)
(4) 让权等待。(若不能进入自己的临界区,应立即释放cpu,以免进程陷入“忙等”)
硬件同步机制
关中断
关中断是实现互斥的最简单的方法之一。在进入锁测试之前关闭中断,直到完成锁测试并上锁之后才能打开中断。这样,进程在临界区执行期间,计算机系统不响应中断,从而不会引发调度,也就不会发生进程或线程切换。由此,保证了对锁的测试和关锁操作的连续性和完整性,有效地保证了互斥。
关中断的方法存在许多缺点:
① 滥用关中断权力可能导致严重后果;
② 关中断时间过长,会影响系统效率,限制了处理器交叉执行程序的能力;
③ 关中断方法也不适用于多CPU 系统,因为在一个处理器上关中断并不能防止进程在其它处理器上执行相同的临界段代码。
利用Test-and-Set实现互斥
这是一种借助一条硬件指令—“测试并建立”指令TS(Test-and-Set)以实现互斥的方法。在许多计算机中都提供了这种指令。可以看作是一个函数过程(原子操作)。
boolean TestAndSet(boolean &lock)
{
boolean rv = lock;
lock = true;
return rv;
}
while(TestAndSet(&lock)) ;
临界区
lock = false; //退出区
剩余区
每个临界资源设置一个布尔变量lock,由于变量lock代表了该资源的状态,故可以将它看成一把锁。lock的初值为false,表示临界值资源空闲
利用Swap指令实现互斥
该指令称为对换指令,在Intel 80x86中又称为XCHG指令,用于交换两个字的内容。
void swap(boolean &lock, boolean &key)
{
boolean tmp = lock;
lock = key;
key = tmp;
}
每个临界资源设置一个全局的布尔变量lock,初值为false,在每个进程中再利用一个局部变量key
适用于单处理器或共享主存的多处理器。但当临界资源忙碌时,其他访问进程必须不断的进行测试,处于一种“忙等”状态,不符合“让权等待”原则。难于用于解决复杂的进程同步问题。
解决“忙等”的一个方案:添加 WaitQueue,等待队列。当进入while忙等时,把TCB加入等待队列,让出CPU。当运行中的线程退出后,Release设置value=0,从等待队列获取一个线程,唤醒他。但 这里会发生上下文切换。如果临界区很长,远远大于上下文切换的开销,那么采取
WaitQueue更合理。
信号量机制
整型信号量
最初由Dijkstra把整型信号量定义为一个用于表示资源数目的整型量S,它与一般整型量不同,除初始化外,仅能通过两个标准的原子操作(Atomic Operation) wait(S)和signal(S)来访问。很长时间以来,这两个操作一直被分别称为P、V操作。
wait(S){
while(S <=0);
S--;
}
signal(S){
S++;
}
记录型信号量
在整型信号量机制中的wait操作,只要是信号量S≤0,就会不断地测试。因此,该机制并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。记录型信号量机制则是一种不存在“忙等”现象的进程同步机制。但在采取了“让权等待”的策略后,又会出现多个进程等待访问同一临界资源的情况。为此,在信号量机制中,除了需要一个用于代表资源数目的整型变量value外,还应增加一个进程链表指针list,用于链接上述的所有等待进程。
struct semaphore
{
int value; //记录资源个数
PCB *queue; //记录等待在该信号量上的进程
}
P(semaphore s); //消费资源
V(semaphore s); //产生资源
P(semaphore s)
{
s.value--;
if(s.value < 0) {
sleep(s.queue);
}
}
V(semaphore s)
{
s.value++;
if(s.value <= 0) {
wakeup(s.queue);
}
}
AND型信号量
前面所述的进程互斥问题针对的是多个并发进程仅共享一个临界资源的情况。在有些应用场合,是一个进程往往需要获得两个或更多的共享资源后方能执行其任务。假定现有两个进程A和B,它们都要求访问共享数据D和E,当然,共享数据都应作为临界资源。
AND同步机制的基本思想是:将进程在整个运行过程中需要的所有资源,一次性全部地分配给进程,待进程使用完后再一起释放。只要尚有一个资源未能分配给进程,其他所有可能为之分配的资源也不分配给它。也就是,对若干个临界资源的分配采取原子操作方式:要么把它所请求的资源全部分配到进程,要么一个也不分配。
信号量集
在前面所述的记录型信号量机制中,wait(S)或signal(S)操作仅能对信号量施以加1或减1操作,意味着每次只能对某类临界资源进行一个单位的申请或释放。当一次需要N个单位时,便要进行N次wait(S)操作,这显然是低效的,甚至会增加死锁的概率。此外,在有些情况下,为确保系统的安全性,当所申请的资源数量低于某一下限值时,还必须进行管制,不予以分配。因此,当进程申请某类临界资源时,在每次分配之前,都必须测试资源的数量,判断是否大于可分配的下限值,决定是否予以分配。
信号量的应用
利用信号量实现进程互斥
为使多个进程能互斥地访问某临界资源,只需为该资源设置一互斥信号量mutex,并设其初始值为1,然后将各进程访问该资源的临界区CS置于wait(mutex)和signal(mutex)操作之间即可。
利用信号量实现前趋关系
可利用信号量来描述程序或语句之间的前趋关系。设有两个并发执行的进程P1和P2。P1中有语
句S1;P2中有语句S2。我们希望在S1执行后再执行S2。为实现这种前趋关系,只需使进程P1和P2共享一个公用信号量S,并赋予其初值为0,将signal(S)操作放在语句S1后面,而在S2语句前面插入wait(S)操作,即
在进程P1中,用S1;signal(S);
在进程P2中,用wait(S);S2;
由于S被初始化为0,这样,若P2先执行必定阻塞,只有在进程P1执行完S1; signal(S);操作后使S增为1时,P2进程方能成功执行语句S2。同样,我们可以利用信号量按照语句间的前趋关系,写出一个更为复杂的可并发执行的程序。
经典同步问题
生产者消费者问题
生产者消费者问题即一组生产者向一组消费者提供产品,他们共享同一个缓冲区。其中生产者需要在缓冲区有空闲的情况下传输产品;消费者需要在缓冲区有产品的情况下消费产品,且多个生成者或消费者需要互斥使用缓冲区。
针对上述问题,我们需要设置两种同步信号量:empty 和 full,其中empty表示缓冲区空闲的数量,初始值为1,full表示缓冲区存在产品的数量,初始值为0;设置一个互斥量 mutex,初始值为1:
首先确定临界资源为缓冲区
生产者需要缓冲区有空闲才能将生产的产品传入缓冲区,因此设置一个同步信号量empty = N(N为缓冲区可容纳的产品数量)
消费者需要缓冲区有产品才能进行消费,因此需设置一个同步信号量full = 0(一开始没有产品)
为控制生产者消费者对临界资源互斥访问,需要设置一个互斥信号量 mutex = 1
//生产者消费者问题
semphore full = 0;
semphore empty = 1;
semphore mutex = 1;
producer()
{
while(1)
{
//生产产品
P(empty);//申请空闲区
P(mutex);//申请缓冲区
向缓冲区运送商品
V(mutex);//释放缓冲区
V(full);//增加缓冲区产品数量
}
}
consumer()
{
while(1)
{
P(full);//等待缓冲区的产品
P(mutex);
消费
V(mutex);
V(empty);//消费完,缓冲区的产品减少,增加空闲区
}
}
P(full)/P(empty)和P(mutex)不可颠倒,否则会发生死锁。
读者-写者问题
存在一个共享数据区,有一些只能对数据区进行读取的进程,一些只能对数据区进行写的进程。
要求:
- 任意读者可以同时读取文件
- 一次只能有一个写者对数据区进行写
- 如果写者正在操作,禁止任何进行对数据区进行操作
1、读者优先,即读者进行操作时,写者不能进行写
对于该问题需要设置几个信号量。
首先由于写者要等所有读者操作完后才能操作,所以需要设置一个表示当前正在操作的读者数量的信号量,readcount,初值为0。
由于读者操作时会修改readcount的值,因此需要设置一个互斥信号量rmutex,初值为1。(控制多个读者互斥使用readcount)
设置一个互斥型号量mutex,用于对写者的数据区进行互斥访问。
//读者优先
semphore rmutex = 1;
semphore mutex = 1;
int readcount = 0;
reader()
{
while(1)
{
P(rmutex);//
if(readcount == 0)//如果当前没有读者,则申请读者进入临界区
{
P(mutex);
}
readcount++;//有新的读者,修改readcount的值
V(rmutex);//readcount修改完,释放对readcount的控制权
读操作
P(rmutex);//读者操作结束,需减少readcount的值
readcount--;
if(readcount == 0)//没有读者则允许写者进入临界区
{
V(mutex)
};
V(rmutex);
}
}
writer()
{
while(1)
{
P(mutex);//等待进入临界区
数据写
V(mutex);
}
}
2、公平情况算法
当读者申请访问时,若前面存在写者正在写或正在等待,读者需等待写者完成操作。
为解决此问题,在1的基础上还需要设置一个信号量wmutex,表示是否存在正在写或等待的写者,初值为1
semphore rmutex = 1;
semphore wmutex = 1;
semphore mutex = 1;
int readcount = 0;
reader()
{
while(1)
{
P(wmutex);//申请读
P(rmutex);//申请修改readcount
if(readcount == 0)
{
P(mutex);
}
readcount++;
V(rmutex);
V(wmutex);
//读操作
P(rmutex);
readcount--;
if(readcount == 0)
{
V(mutex);
}
v(rmutex);
}
3、写者优先
写者优先要求:只要等待队列中存在写者,不管何时到达,都优先于读者被唤醒
此时需设置一个互斥信号量readable 用于表示当前是否有写者,控制写者优先于读者,初值为1
设置两个变量,readcount和writecount,分别用于表示当前读写者的数量,初值为0
设置两个信号量,rmutex用于读者互斥,wmutex用于写者互斥(需要修改readcount和writecount的值)
semphore mutex = 1;
semphore wmutex = 1;
semphore rmutex = 1;
semphore readable = 1;
int readcount = 0;
int writecount = 0;
reader()
{
P(reachable);
P(rmutex);
if(readcount == 0)
{
P(mutex);
}
readcount++;
V(rmutex);
V(readable);
//读操作
P(rmutex);
readcount--;
if(readcount == 0 )
{
V(mutex);
}
V(rmutex);
}
write()
{
P(wmutex);
if(writecount == 0)
{
P(readable);
}
writecount++;
V(wmutex);
P(mutex);
//写操作
V(mutex);
P(wmutex);
writecount--;
if(writecount == 0)
{
V(eadable);
}
V(wmutex);
}
哲学家就餐问题
有5个哲学家在圆桌上就餐,每两个人之间有一只筷子。当一个人进餐时,他需同时拿起左右的两只筷子。
在此问题中,筷子是临界资源,不能同时被两个哲学家拿。
对哲学家和筷子进行编号,0-4。当哲学家同时拿起右边的筷子会发生死锁。
semphore chopstick[5] = {1, 1, 1, 1, 1};
philosphere(int i)
{
if(i&1)//奇数号哲学家
{
P(chopstick[i%5]);
P(chopstick[(i+1)%5]);
//进餐
V(chopstick[i%5]);
V(chopstick[(i+1)%5]);
}
else
{
P(chopstick[(i+1)%5]);
P(chopstick[i%5]);
//进餐
V(chopstick[(i+1)%5]);
V(chopstick[i%5]);
}
}
解决:规定奇数号哲学家先拿起左边的筷子后再拿起右边的筷子,规定偶数号哲学家先拿起右边的筷子再拿起左边的筷子。