简介
进程同步是一个操作系统级别的概念,是在多道程序的环境下,存在着不同的制约关系,为了协调这种互相制约的关系,实现资源共享和进程协作,从而避免进程之间的冲突,引入了进程同步。
临界资源
在操作系统中,进程是占有资源的最小单位(线程可以访问其所在进程内的所有资源,但线程本身并不占有资源或仅仅占有一点必须资源)。但对于某些资源来说,其在同一时间只能被一个进程所占用。这些一次只能被一个进程所占用的资源就是所谓的临界资源。典型的临界资源比如物理上的打印机,或是存在硬盘或内存中被多个进程所共享的一些变量和数据等(如果这类资源不被看成临界资源加以保护,那么很有可能造成丢数据的问题)。
对于临界资源的访问,必须是互诉进行。也就是当临界资源被占用时,另一个申请临界资源的进程会被阻塞,直到其所申请的临界资源被释放。而进程内访问临界资源的代码被成为临界区。
对于临界区的访问过程分为四个部分:
1.进入区:查看临界区是否可访问,如果可以访问,则转到步骤二,否则进程会被阻塞
2.临界区:在临界区做操作
3.退出区:清除临界区被占用的标志
4.剩余区:进程与临界区不相关部分的代码
临界资源使用规则:忙则等待、优先等待、空闲让进、让权等待(在临界区的进程,不能在临界区内长时间处于事件等待,必须在一定时间退出临界区)。
多个进程常常需要共同修改某些共享变量、表格、文件数据库等,协作完成一些功能。共享协作带来了进程的同步和互斥、死锁、饥饿等问题。
进程间同步和互诉的概念
进程同步
进程同步也是进程之间直接的制约关系,是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系来源于他们之间的合作。
比如说进程A需要从缓冲区读取进程B产生的信息,当缓冲区为空时,进程B因为读取不到信息而被阻塞。而当进程A产生信息放入缓冲区时,进程B才会被唤醒。概念如图1所示。
图1.进程之间的同步
进程互斥
进程互斥是进程之间的间接制约关系。当一个进程进入临界区使用临界资源时,另一个进程必须等待。只有当使用临界资源的进程退出临界区后,这个进程才会解除阻塞状态。
比如进程B需要访问打印机,但此时进程A占有了打印机,进程B会被阻塞,直到进程A释放了打印机资源,进程B才可以继续执行。概念如图3所示。
图3.进程之间的互斥
实现临界区互斥的基本方法
软件方法
Dekker算法和peterson算法
硬件实现方法
通过硬件实现临界区最简单的办法就是关CPU的中断。从计算机原理我们知道,CPU进行进程切换是需要通过中断来进行。如果屏蔽了中断那么就可以保证当前进程顺利的将临界区代码执行完,从而实现了互斥。这个办法的步骤就是:屏蔽中断--执行临界区--开中断。但这样做并不好,这大大限制了处理器交替执行任务的能力。并且将关中断的权限交给用户代码,那么如果用户代码屏蔽了中断后不再开,那系统岂不是跪了?
还有硬件的指令实现方式,这个方式和接下来要说的信号量方式如出一辙。但是通过硬件来实现,这里就不细说了。
信号量实现方式
这也是我们比较熟悉P V操作。通过设置一个表示资源个数的信号量S,通过对信号量S的P和V操作来实现进程的的互斥。
P和V操作分别来自荷兰语Passeren和Vrijgeven,分别表示占有和释放。P V操作是操作系统的原语,意味着具有原子性。
P操作首先减少信号量,表示有一个进程将占用或等待资源,然后检测S是否小于0,如果小于0则阻塞,如果大于0则占有资源进行执行。
V操作是和P操作相反的操作,首先增加信号量,表示占用或等待资源的进程减少了1个。然后检测S是否小于0,如果小于0则唤醒等待使用S资源的其它进程。
前面我们C#模拟进程的同步和互斥其实算是信号量进行实现的。
一些经典利用信号量实现同步的问题
生产者--消费者问题
问题描述:生产者-消费者问题是一个经典的进程同步问题,该问题最早由Dijkstra提出,用以演示他提出的信号量机制。本作业要求设计在同一个进程地址空间内执行的两个线程。生产者线程生产物品,然后将物品放置在一个空缓冲区中供消费者线程消费。消费者线程从缓冲区中获得物品,然后释放缓冲区。当生产者线程生产物品时,如果没有空缓冲区可用,那么生产者线程必须等待消费者线程释放出一个空缓冲区。当消费者线程消费物品时,如果没有满的缓冲区,那么消费者线程将被阻塞,直到新的物品被生产出来
这里生产者和消费者是既同步又互斥的关系,首先只有生产者生产了,消费着才能消费,这里是同步的关系。但他们对于临界区的访问又是互斥的关系。因此需要三个信号量empty和full用于同步缓冲区,而mut变量用于在访问缓冲区时是互斥的。
读者--写者问题
问题描述:
一个数据文件或记录,统称数据对象,可被多个进程共享,其中有些进程只要求读称为"读者",而另一些进程要求写或修改称为"写者"。
规定:允许多个读者同时读一个共享对象,但禁止读者、写者同时访问一个共享对象,也禁止多个写者访问一个共享对象,否则将违反Bernstein并发执行条件。
通过描述可以分析,这里的读者和写者是互斥的,而写者和写者也是互斥的,但读者之间并不互斥。
由此我们可以设置3个变量,一个用来统计读者的数量,另外两个分别用于对读者数量读写的互斥,读者和读者写者和写者的互斥。如代码4所示。
哲学家进餐问题
问题描述:
有五个哲学家,他们的生活方式是交替地进行思考和进餐。哲学家们公用一张圆桌,周围放有五把椅子,每人坐一把。在圆桌上有五个碗和五根筷子,当一个哲学家思考时,他不与其他人交谈,饥饿时便试图取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到两根筷子时,方能进餐,进餐完后,放下筷子又继续思考。
图7.哲学家进餐问题
根据问题描述,五个哲学家分别可以看作是五个进程。五只筷子分别看作是五个资源。只有当哲学家分别拥有左右的资源时,才得以进餐。如果不指定规则,当每个哲学家手中只拿了一只筷子时会造成死锁,从而五个哲学家都因为吃不到饭而饿死。因此我们的策略是让哲学家同时拿起两只筷子。因此我们需要对每个资源设置一个信号量,此外,还需要使得哲学家同时拿起两只筷子而设置一个互斥信号量,如代码5所示。
总结
本文介绍了进程的同步和互斥的概念,临界区的概念,以及实现进程同步互斥的方式,并解决了3种实现同步的经典问题,并给出了相应的C#模拟代码。操作系统对于进程的管理是是计算机编程的基础之一,因此掌握这个概念会使你的内功更上一层:-D
C++代码实现生产者消费者问题
#include <iostream>
using namespace std;
int const buffersize = 5;
/* 定义进程控制块PCB */
struct PCB
{
int flag; //flag=1 denote producer;flag=2 denote consumer;
int numlabel;
char product;
char state;
struct PCB* processlink; ////process link pointer
}*exe=0,*over=0;
PCB * readyhead = 0, * readytail = 0; /////ready queue;
PCB * consumerhead = 0,* consumertail = 0;/////consumer queue;
PCB * producerhead = 0,* producertail = 0;//////producer queue;
int productnum=0; //产品数量
int processnum=0; //进程数
int full=0,empty=buffersize; // semaphore
char buffer[buffersize]; //缓冲区
int bufferpoint=0; //缓冲区指针
void linkqueue( PCB* process, PCB* & tail );
PCB* getp(PCB* head, PCB* & tail);
int hasElement(PCB* pro);
void display(PCB* p);
void linklist(PCB* p,PCB* listhead);
void freelink(PCB* linkhead);
int processproc();
int waitfull();
int waitempty();
void signalempty();
void signalfull();
void producerrun();
void comsuerrun();
int main()
{
char begin;
int element;
cout<<"你想开始程序吗?(y/n)";
cin>>begin;
producerhead = new PCB;
if( !producerhead ) return 1;
producertail = producerhead;
producerhead->processlink = 0;
producerhead->flag = 1;
producerhead->numlabel = processnum;
consumerhead = new PCB;
if( !consumerhead ) return 1;
consumertail = consumerhead;
consumerhead->processlink = 0;
consumerhead->flag = 2;
consumerhead->numlabel = processnum;
readyhead = new PCB;
if( !readyhead ) return 1;
readytail = readyhead;
readyhead->processlink = 0;
readyhead->flag = 3;
readyhead->numlabel = processnum;
over = new PCB;
if ( !over ) return 1;
over->processlink = 0;
while( begin=='y')
{
if( !processproc() ) break;/////
element = hasElement( readyhead );
while( element )
{
exe = getp( readyhead,readytail );
printf("进程%d申请运行,它是一个",exe->numlabel);
exe->flag==1? printf("生产者
"):printf("消费者
");
if( exe->flag==1 )
producerrun( );
/* comsuerrun( ); */
else
comsuerrun( );
element = hasElement(readyhead);
}
cout<<"就绪队列中无进程"<<endl;
if( hasElement(consumerhead) )
{
cout<<"消费者等待队列中有进程:"<<endl;
display( consumerhead );
}
else
{
cout<<"消费者等待队列中已无进程!"<<endl;
}
if(hasElement(producerhead))
{
cout<<"生产者等待队列中有进程:"<<endl;
display( producerhead );
}
else
{
cout<<"生产者等待队列中已无进程!"<<endl;
}
cout<<"
你想继续吗?(press /'y/' for /'n/')";
cin>>begin;
}
cout<<"
进程模拟完成."<<endl;
freelink(over);
over = 0;
freelink(readyhead);
readyhead = 0;
readytail = 0;
freelink(consumerhead);
consumerhead = 0;
consumertail = 0;
freelink(producerhead);
producerhead = 0;
producertail = 0;
return 0;
}
void linkqueue( PCB* process, PCB* & tail ) /////将进程process插入到队列尾部
{
if( tail )
{
tail->processlink = process;
tail = process;
}
else
{
cout<<"队列未初始化!";
exit(1);
}
}
PCB* getp(PCB* head,PCB* & tail)//////获取进程链表首元素
{
PCB* p;
p = head->processlink;
if( p )
{
head->processlink = p->processlink;
p->processlink = 0;
if( !head->processlink )
tail = head;
}
else
return 0;
return p;
}
int processproc( )
{
int flag,num;
char ch;
PCB* p = 0;
cout<<"
请输入希望产生的进程个数:";
cin>>num;
for(int i=0;i<num;i++)
{
cout<<"
请输入您要产生的进程:输入1为生产者进程;输入2为消费者进程
";
cin>>flag;
p = new PCB;
if( !p )
{
cout<<"内存分配失败!"<<endl;
return 0;
}
p->flag = flag;
processnum++;
p->numlabel = processnum;
p->processlink = 0;
if( p->flag == 1 )
{
printf("您要产生的是生产者进程,它是第%d个进程,请输入您要该进程产生的字符:
",processnum);
cin>>ch;
p->product=ch;
productnum++;
printf("您要该进程产生的字符是%c
",p->product);
}
else
{
printf("您要产生的是消费者进程,它是第%d个进程。
",p->numlabel);
}
linkqueue( p , readytail);
}
return 1;
}
void signalempty( ) //////生产者队列中有数据就放到就绪队列中,无数据空数据加一
{
PCB* p;
if( hasElement(producerhead) )
{
p = getp( producerhead,producertail );
linkqueue( p, readytail );
printf("等待中的生产者进程进入就绪队列,它的进程号是%d
",p->numlabel);
}
empty++;
}
void signalfull( )/////消费者队列中有数据就放到就绪队列中(有无数据时)消费者进程数目full+1
{
PCB* p;
if( hasElement(consumerhead) )
{
p = getp(consumerhead,consumertail);
linkqueue(p,readytail);
printf("等待中的消费者进程进入就绪队列,它的进程号是%d
",p->numlabel);
}
full++;
}
void producerrun( )
{
if(!waitempty())
return;
printf("进程%d开始向缓冲区存数%c
",exe->numlabel,exe->product);
buffer[bufferpoint] = exe->product;
bufferpoint++;
printf("进程%d向缓冲区存数操作结束
",exe->numlabel);
signalfull();
linklist(exe,over);
}
void comsuerrun( )
{
//if(!waitfull())
// return;
//int var = bufferpoint;
//for (int i =0; i<var; i++)
//{
// printf("进程%d开始从缓冲区取数
",exe->numlabel);
// exe->product = buffer[bufferpoint-1];
// bufferpoint--;
// exe->product = buffer[i];
// printf("进程%d从缓冲区取数操作结束,取数是%c
",exe->numlabel,exe->product);
// signalempty();
// linklist( exe, over );
//}
if(!waitfull())
return;
printf("进程%d开始从缓冲区取数
",exe->numlabel);
exe->product = buffer[bufferpoint-1];
bufferpoint--;
printf("进程%d从缓冲区取数操作结束,取数是%c
",exe->numlabel,exe->product);
signalempty();
linklist( exe, over );
}
void display(PCB* p)/////查看进程中的进程类型和数目
{
p=p->processlink;
while( p )
{
p->flag==1? printf("生产者进程"):printf("消费者进程");
printf("%d
",p->numlabel);
p = p->processlink;
}
}
int hasElement(PCB* pro)//////判读是否有进程
{
if( !pro->processlink )
return 0;
else
return 1;
}
int waitempty()////消费者消费(完了)生成者产生的数据,值0,运行进程进入生成者等待队列
{
if(empty<=0 )
{
printf("进程%d:缓冲区存数,缓冲区满,该进程进入生产者等待队列
",exe->numlabel);
linkqueue( exe, producertail );
return 0;
}
else
{
empty--;
return 1;
}
}
int waitfull() ////生成者生成数据填满,进程进入消费者等待队列;
{
if(full<=0)
{
printf("进程%d:缓冲区取数,缓冲区空,该进程进入消费者等待队列
",exe->numlabel);
linkqueue( exe, consumertail );
return 0;
}
else
{
full--;
return 1;
}
}
void linklist(PCB* p,PCB* listhead) ////把进程p追加到listhead链表之后
{
PCB* cursor = listhead;
while( cursor->processlink )
{
cursor = cursor->processlink;
}
cursor->processlink = p;
}
void freelink(PCB* linkhead)
{
PCB* p;
while( linkhead )
{
p = linkhead;
linkhead = linkhead->processlink;
delete(p);
}
}