目录
前言
不同的进程会在运行过程中争夺系统资源,保证进程有序地利用资源的规则就是进程同步。我们可以把两个的进程想象成两个人,争夺的资源想象成一个算盘,同时只能有一个人使用它。
一、进程同步的基本概念
1.两种形式的制约关系
1)间接相互制约关系
2)直接相互制约关系
间接相互制约关系就是两个人本来不是干一件事,但要用同一件东西,就像人事部的和宣传部的同学要用同一台电脑,两个进程都要使用打印机或者cpu。
直接相互制约关系就是两个人共同完成一件事,用到的一件东西,就像两个人一起记账的账本,两个进程一起计算的公用缓存区。
2.临界资源
上面人事部和宣传部要用的电脑,一起记账的账本,打印机和公用缓存都是临界资源。
生产者-消费者问题是一个著名的进程同步问题。生产者和消费者分别执行时都没有问题,但是并发执行就可能会出错,问题就在于两个进程共享计数变量counter。
解决的关键就在于把counter作为临界资源,让生产者和消费者互斥地访问counter。
3.临界区
我们把每一个进程中访问临界区的那一段代码称为临界区,一个进程进入自己的临界区只能是没有其他进程在它们自己的临界区中才行。
进程同步的关键就在于让每个进程互斥地进入临界区,就像两个人合作的关键在于两个人产生交集的地方,下述的方法都是基于这一点展开的。
4.同步机制应遵循的规则
(1)空闲让进。尽量让临界资源一直有人用,提高资源利用率。
(2)忙则等待。有进程在临界区中,其他进程不能进入临界区。
(3)有限等待。不能让一个进程一直是等待状态。
(4)让权等待。当进程不能进入临界区时,要切换到等待状态。
二、硬件同步机制
软件同步机制已经很少使用了。硬件同步机制可以看成给临界区装了一个锁,若“锁开”,则可以进入;若“锁关”则不能进入。若多个进程同时测试到“锁开”那就会同时访问临界资源产生错误,所以“测试锁状态”和“关锁”必须是连续的,这也是硬件同步机制的关键所在。
1.关中断
关中断是实现互斥最简单的方式。在进程进入“测试锁状态”时关中断,在“关锁”结束后打开中断。上述的问题就解决了。
这是最符合直觉的解决方式,“测试锁状态”和“关锁”之所以有可能会不连续,就是因为“测试锁状态”后可能会发生中断,现在把直接关中断,把问题的源头解决了,自然也就没有这个问题了。
但不幸的是,这也会带来新的问题。
- 关中断不能乱用,乱用会出事
- 频繁关中断会影响系统效率
并且,该方法也不适用于多cpu的系统。
2.利用Test-and-Set指令实现互斥
该方法的核心就是下述的代码:
boolean TS(boolean *lock){
boolean old;
old = *lock;
*lock = TRUE;
return old;
}
do {
……
while TS(&lock);
critical section;
lock = False;
remainder section;
}while(TRUE)
这里的TS指令是一条原语,和第1种方法的关中断作用是一样的,区别就在于这种方法不需要关中断。听着有点绕,比如一条进程进入临界区时发来一条中断请求,如果是用第一种关中断的方式,该请求会直接被拒绝;如果是第二种硬件指令的方式,会停止执行,然后回滚已经进行的操作,响应中断。也就是说第二种方式是可以在执行TS指令时响应中断的。
说句题外话,我觉得“while TS(&lock);”这行代码很有趣,以前我只知道while是循环语句。这句代码虽然也是循环语句,但是循环体中没有任何东西,像一堵墙,分隔在进入区和临界区之间,只有临界区的锁开了,进程才能突破这面墙,可以说是非常形象的代码了。
3.利用Swap指令实现进程互斥
这个方法和上面的方法思路是一样的,就是用一条硬件指令来代替关中断,同时消除关中断的不良影响。只是这个方法和第2个方法采用了不一样的算法,核心代码如下:
void swap(boolean *a,boolean *b){
boolean temp;
temp = *a;
*a = *b;
*b = temp;
}
do{
key = TRUE;
do{
swap(&lock,&key);
}while(key!=FALSE);
临界区操作;
lock = FALSE;
……
}while(TRUE)
不足之处:
上述的两个硬件指令可以有效地实现互斥,但是当临界资源正在被使用时,其他进程就用时不时地看看锁开了没,也就是要不停地测试,不符合“让权等待”的原则,造成处理机时间的浪费。
三、信号量机制
1.整型信号量
整形信号量是一个用于表示资源数目的整形量S。
wait(S){
while(S<=0);
S--;
}
signal(S){
S++;
}
2.记录型信号量
整型信号量依然没有解决“忙等”的问题,为了解决这个问题,需要引入一个进程链表指针list,其实就是等待队列。
typedef struct{
int value;
struct process_control_block *list;
}semaphore;
wait(semaphore *S){
S->value--;
if(S->value < 0) block(S->list);
}
signal(semaphore *S){
S->value++;
if(S->value<=0) wakeup(S->list);
}
当资源都被占满的时候,新来的进程进入等待队列,每当有一个进程不再占用资源了,就让等待队列的第一个进入。
这和我10月1号去的我家牛排很像,店里面的位子已经占满了,我不需要一直站在门口询问是否有空位让出来了,而只用拿着店员给我的排队号等待,每当有一桌人离开,店员就会马上喊排队号最小的人进去就餐。
3.AND型信号量
上面说的都是多个并发进程共享一个临界资源的情况。但有时多个并发进程还会共享多个临界资源。设想有两个厨子共享平底锅和炒勺,他们都必须同时拿到平底锅和炒勺才能烧菜,现在一个厨师拿着平底锅等炒勺,一个厨师拿着炒勺等平底锅,谁都烧不了菜,这就是死锁。
共享的资源越多,发生死锁的可能就越大。为了解决这个问题,在wait操作中增加一个“AND”条件,简单来说就是一个进程要么把所需的共享资源全拿了,要么一个也不拿。这样自然也不会发生思索了。
4.信号量集
依然是对3的扩充。上述的操作都是一次申请或释放一个资源,当需要N个同类资源时显然是低效的,所以要求可以同时增加或释放多个资源。
另外,当资源数量低于某一个值时,应该进行管制,不予分配。
总之,4是对3在细节上的补充。