进程互斥的四种软件实现方式(单标志法、双标志先检查法、双标志后检查法、以及Peterson算法),三种硬件实现方式(中断屏蔽方法、TSL指令、Swap指令)。在所有的解决方案中都无法实现让权等待问题。
1 信号量机制
信号量机制是一种可以实现进程互斥、同步的有效方法。
信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量。如系统中只有一台打印机,就可以设置一个初值为1的信号量。如所有的临界资源在同一时刻只能被一个进程访问,所以临界资源的信号量为1。
用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程的互斥和同步。
一对原语:wait(S)原语和signal(S)原语,S表示信号量。wait、signal原语通常称为P、V 操作。
2 信号量机制——整型信号量
用一个整数型的变量作为信号量,用来表示系统某种资源的数量。
如:某计算机系统中只有一台打印机...
int S = 1; //初始化整型信号量S,表示当前系统中可用的打印机资源。
void wait(int S){ //wait原语,相当于“进入区”
while(S <= 0)); // 如果资源不够,就一直循环等待
S = S - 1; // 如果资源数够,则占用一个资源
}
void signal(int S){ // signal原语,相当于“退出区”
S = S + 1; // 使用完资源后,在退出区释放资源
}
使用原语将检查和上锁变成了原子性操作,避免了并发、异步导致的问题。
存在问题:不满足让权等待原则,会发生忙等。
3 信号量机制——记录型信号
整型信号量的缺点是存在忙等的问题,所以又提出了记录型信号量。
/*记录型信号量的定义*/
typedef struct{
int value; // 剩余资源
struct process *L; // 等待队列,当没有资源剩余时,新来的进程将会被放在这个阻塞队列中
} semaphore
/* 某进程需要使用资源时,通过wait原语申请*/
void wait(semaphore S) {
S.value--;
if(S.value < 0) // 如果剩余资源不够,使用block原语使进程从运行态进入阻塞态,
block(S.L); // 并挂到信号量的等待队列中。
}
/* 进程使用完资源后,通过signal原语释放*/
void signal(semaphore S) {
S.value++;
if(S.value <= 0) // 如果value的值小于0,表示有新的进程在等待资源,所以要唤醒等待队列中的进程
wakeup(S.L);
}
(1) 对信号量S的一次P操作意味着进程请求一个单位该类的资源,需要执行S.value--,表示资源数减1,当S.value < 0时表示该类资源已经分配完毕,因此进程应该调用block原语进行自我阻塞(当前运行的进程从运行态—>阻塞态),主动放弃处理机,并插入到该类资源的等待队列S.L中。可见,该机制遵循了“让权等待”原则,不会出现忙等现象。
(2) 对信号量S的一次V操作意味着进程释放了一个单位的该类资源,因此需要执行S.value++,表示该类资源加1,若加1后仍是S.value <= 0,表示依然有进程在等待该类资源,因此应调用wakeup原语唤醒等待队列中第一个进程(被唤醒进程从阻塞态—>就绪态)。
4 信号量机制实现进程互斥
(1) 确定临界区(如对临界资源打印机的访问就应放在临界区)
(2) 设置互斥信号量(mutex)。
(3) 在临界区之前执行P(mutex)
(4) 在临界区之后执行V(mutex)
/* 信号量机制实现互斥 */
semaphore mutex = 1; // 初始化信号量
P1(){
...
P(mutex); // 使用临界资源前需要加锁
临界区代码段.....
V(mutex); // 使用临界资源后需要解锁
...
}
P2(){
...
P(mutex);
临界区代码段.....
V(mutex);
...
}
.....
5 信号量机制实现进程同步
进程同步:就是要让并发进程按要求有序的推进。
用信号量实现进程同步:
(1) 分析需要同步的两个操作,即必须保证一前一后的执行顺序的操作。
(2) 设置同步信号量S,初始为0。
(3) 在前操作之后执行V(S)。
(4) 在后操作之前执行P(S)。
如P1、P2并发执行,假设需要保证代码4必须要代码1和代码2的运行结果才能执行,那么就必须保证代码4必须在代码2之后执行。
P1(){
代码1;
代码2;
代码3;
}
P2(){
代码4;
代码5;
代码6;
}
使用信号量机制实现同步
semaphore S = 0; //初始化同步信号量,初始值为0;
P1(){ P2(){
代码1; P(s)
代码2; 代码4;
V(S); 代码5;
代码3; 代码6;
} }
(1) 若先执行到V(S)操作,则S++后S=1,。之后执行到P(S)时,由于S=1,表示有资源,执行S--后,S的值为0,P2不会执行block原语,而是继续往下执行代码4。
(2) 若先执行到P(S)操作,由于S=0,S--后S=-1,表示没有资源,因此P操作会执行block原语,主动请求阻塞。之后当执行完代码2,继而执行V(S)操作,S++,S的值变为0,由于此时有进程在该信号量的阻塞队列中,因此V操作会执行wakeup原语,唤醒P2进程,这样P2进程就可以继续执行代码4了。
无论是哪种情况,都可以保证代码4一定在代码2之后执行。
6 信号量机制实现前驱关系
进程P1中有句代码S1,P2中有句代码S2...,这些代码要求按照如下的顺序执行:
其实每一对前驱关系都是一个进程同步问题(需要保证一前一后的操作),因此:
(1) 要为每一对前驱关系各设置一个同步变量。
(2) 在前操作之后对应的同步变量执行V操作。
(3) 在后操作之前对应的同步变量执行P操作。
对应的代码如下: