本章重点
- 进程两种制约关系
- 信号量机制
- 经典进程的同步问题
- 进程通信
- 线程的概念,重点在于与进程的区别
- 线程的实现
进程同步机制
复习内容:为了提高系统资源利用率,我们提出了多道程序系统,但是它具有间断性、失去封闭性、不可再现性。
背景——为什么提出进程同步机制
在多道程序系统中,为保证多个进程能够合理利用资源,使得程序的执行具有可再现性,必须对进程加以控制,于是我们提出了进程同步机制。
什么是进程同步
多个进程可能存在两种制约关系——间接相互制约关系和直接相互制约关系
间接相互制约关系(互斥关系)
间接相互制约关系又称互斥关系,是指多个进程抢夺共享系统资源。就好像情敌一样。例如,对于单CPU计算机来说,进程A和进程B对CPU的使用就是互斥关系。
直接相互制约关系(同步关系)
直接相互制约关系又称同步关系,是指为完成某任务,进程间需要相互合作。就好像流水线一样。例如输入进程A和计算进程B,他们之间建立一个缓冲区。进程A通过缓冲区向B提供数据,进程B从缓冲区取出数据才能进行计算。
临界资源
只能一个进程访问的资源叫做临界资源。
如CPU,只有一个进程能在CPU中执行。如打印机,只有一个进程能够访问打印机,如果同时还有其他进程想要使用打印机,就需要排队了,不能两个打印任务同时使用打印机,那不就打出乱码了吗。
上述的两个图中的资源都是临界资源,互斥关系中的CPU和同步关系中的缓冲区,都是只能一个进程访问,一个访问完了,另一个再访问。
临界区
人们把每个进程中访问临界资源的那段代码叫做临界区,只要明白临界区是代码就行,然后再想什么代码——访问临界资源的那串代码。
进程同步应遵循的规则
无规矩不成方圆
- 临界资源空闲,允许一个请求临界资源的进程进入(执行)自己的临界区
- 当有进程占用临界资源(已有进程进入临界区),其他请求要等待(不能进入临界区)
- 应保证等待时间有限
- 申请临界资源的进程无论申请成功与否都要让出处理机。
怎么做到访问临界资源
或者说以下方法都是解决诸进程互斥进入临界区的方法。因为无论是互斥关系还是同步关系,对于邻接资源的访问都是互斥的。(这里的两个互斥,一个互斥关系一个互斥访问,意思是一样的,都是排他,但是一个是描述的多个进程的关系,一个描述的临界资源的属性)
注:在这一章中,有许多术语都是重复的,如上述的两个互斥表达的意思一样使用的语境不同,和“同步关系”和“进程同步”,“同步关系”描述的是两个进程的关系,“进程同步”是说让多个进程协调运作,这两个同步描述的语境也不太一样,范围也不一样,应该说“进程同步”是协调多个进程,进程之间有两种关系,一种是互斥关系,一种是“同步关系”。只要能做到不搞混就行。当初在上课的时候老师不加区分,我就被弄得一头雾水,我觉得挺混乱的,所以还是标注了出来,如果你觉得这样区分太细碎,反而不利于理解重点内容(请参看第一个大标题本章重点)那就不用加以区分了。
硬件机制
利用Test-and-Set指令
这是一种借助一条硬件指令——“测试并建立”指令(Test and Set)以实现互斥的方法。这条指令更像是一个函数,且执行过程不可分割。
//返回参数的原始状态,并修改参数的状态为true
bool TS(bool *lock){
bool old=*lock;
*lock=true;
return old;
}
该指令其实就是将测试锁(查看返回值)和上锁动作(将锁的状态设置为true)设置为一个原语,要执行都执行,要不执行都不执行。
如何使用?
while( TS( lock ) );//不断测试锁,直到TS返回false(也即lock状态为false)跳出测试进入临界区(跳出的同时把锁设置为了true)
crticle section;
lock=false;//出了临界区,关锁
remainder section;
关中断
原理与Test-and-Set相同,就是将测试锁和上锁动作设置为一个不可中断的过程。
中断:CPU暂时中止进程的执行转而处理这个新的情况的过程就叫做中断。
在测试锁之前,需要关闭中断,这时就不能发生调度了(关闭了中断相当于谁也别想打断我!!),这时就一直测试锁开了没有啊,锁开了没有啊……直到完成锁测试完成(锁打开了)并且上锁之后才能打开中断。
上述的硬件实现互斥进入临界区的方法都有一个特点,就是如果此时临界资源在忙(lock==true)我就不断测试锁,这其实是一种忙等状态,不符合“让权等待”(当进程不能进入自己的临界区时,应立即释放处理机,以免进程陷入忙等状态)
信号量机制
信号量机制是利用软件的方法解决多个进程互斥进入临界区的问题。这里一共介绍四种信号量——整型信号量、纪录型信号量、AND型信号量、信号量集。
什么是信号量??
信号量是一种数据结构,只有PV原语操作才能修改信号量的值。P操作又称wait操作,是一种原语操作,不可拆分,用来申请资源。V操作又称signal操作,是一种原语操作,不可拆分,用来释放资源。
整型信号量
数据结构:使用一个整型来表示资源个数(不能为负数)。
wait(S){
while(S<=0){ } //Test
S--;/ //Set不可分割
}
signal(S){
S++;
}
缺点:当S<=0
就一直测试,违背“让权等待”,处于忙等状态。
纪录型信号量
数据结构:
struct semaphore{
int value;
PCB *list;
}
wait(Semaphore *S){
S->value--;
if(S->value<0) block(S->list);//此时表示资源数为负
}
signal(Semphore *S){
S->value++;
if(S->value<=0) wakeup(S->list);//此时表示有等待的进程
}
int值表示资源的数目,这里就与整型有区别了,这里资源数目可以为负值。当资源数目为正数,其值表示剩余资源的数目,当资源数目为0,表示当前没有资源,当资源数目为负数时,其绝对值表示正在申请资源的进程的个数。
AND型信号量
如图,进程A和B想要进行下去都需要资源1和2,进程A占有着资源2去申请资源1,进程B占有资源1去申请资源2,这样谁也不肯放弃,就会造成死锁。
前面所说的进程互斥问题针对的是多个并发进程仅共享一个临界资源的情况。在有些应用场合,是一个进程往往需要获取两个或者更多的共享资源后方能执行其任务。
这时我们采用AND型信号量,这些资源要是分配就都分配,要是不分配就都不分配。
如上图中,要是给进程A分配资源,当资源1和资源2都有空闲的时候,就都分配给A,如果有一个没有空闲,那么就都会收回,一个也不分配给A。这样就不会造成A占有部分资源却进行不下去的问题了。
Swait(S1,S2...Sn){
while(true)
{
if(S1>=1 && S2>=1 &&...&&Sn>=1)
{
for(int i=0;i<n;i++)
Si--;
break;
}
else{
将进程放在S1...Sn第一个不足的资源的阻塞队列中
}
}
}
Ssignal(S1,S2...Sn){
while(true)
{
for(int i=0;i<n;i++)
{
Si++;
将Si等待队列中的进程放入ready队列中。
}
}
}
信号量集
上述的信号量机制,每次都是申请或者释放1个资源,如果需要N个某种资源,是不是要申请N次呢?这样是很低效的。而且以上的信号量的测试下线一直都是1,也就是某种资源的所有个数都可以被分配出去,但是在有些情况下,为了确保系统的安全性,当所申请的资源数量低于某一下限值时,都必须进行管制。
基于上述的两个问题,我们对AND型信号量进行扩充。进程对信号量Si的测试值不再是1,而是分配下限值ti,即要求Si>=ti,否则不予分配。
Swait(S1,t1,d1...Sn,tn,dn)
Ssignal(S1,t1,d1...Sn,tn,dn)
S1
为资源1的信号量,t1
为下限值,d1
为每次申请的需求。
几种特殊情况
Swait(S,d,d)//只有一个信号量,最低值为d,每次申请的需求为d
Swait(S,1,1)//只有一个信号量,最低值为1,每次申请的需求为1,其实就是整型信号量
Swait(S,1,0)//只有一个信号量,最低值为1,每次申请的需求是0,其实这是一个开关。当我把信号量的初值设置为1时,进程可以申请,当我把信号量的值改变为0时,进程就申请不到了