一、临界区
互相协作的进程之间有共享的数据,于是这里就有一个并发情况下,如何确保有序操作这些数据、维护一致性的问题,即进程同步。为了解决合作进程之间的竞争条件,引入临界区问题模型。 临界区是包含访问共享数据指令的相关代码段,也是多个进程都包含的代码段,在这段代码中可能会进行更新数据表、交换变量等操作。从数据一致性的角度来说,当一个进程进入临界区后,其他进程就不允许进入临界区,也就是不能有多个进程同时处于临界区。
临界区问题(critical-section problem)是设计一个以便进程协作的协议。每个进程必须请求允许进入其临界区。实现请求的代码段称为进入区(entry section),临界区之后可有退出区(exit section),其他代码段成为剩余区(remainder section)。
临界区问题的解答必须满足三项要求:
(1)互斥(mutual exclusion): 如果进程Pi在其临界区内执行,那么其他进程都不能在其临界区内执行;
(2)渐进(progress): 如果没有进程在其临界区内执行且有进程需进入临界区,那么只有那么不在剩余区内执行的进程可参加选择,以确定谁能下一个进入临界区,且这种选择不能无限推迟;
(3)有限等待(bounded waiting): 从一个进程做出进入临界区的请求,直到该请求允许为止,其他进程允许进入其临界区内的次数有上限。
二、信号量
信号量S是个整数变量,除了初始化外,它只能通过两个标准原子操作:wait()和signal()来访问。即P和V。
wait()就是等待资源的过程,定义可表示为:
wait(S)
{
while(S<=0)
;//busy wait
S--;
}
signal()就是释放资源的过程,定义可表示为:
signal(S)
{
S++;
}
在wait()和signal()操作中,对信号量整型值的修改必须不可分地执行。即当一个进程修改信号量值时,不能有其他进程同时修改同一信号量的值。另外,对于wait(S),对于S的整数值测试(S≤0)和对其可能的修改(S–),也必须不被中断地执行。
信号量的基本用法如下:
do
{
wait(mutex);
//critical section
signal(mutex);
//remainder section
}while(TRUE);
可以看到互斥锁其实就是信号量的一种特殊形式(当信号量为二进制信号量时),而从互斥锁中可以知道其具有忙等待的缺点,可以通过阻塞自己来解决这一缺点。
将信号量定义为结构体,如下:
typedef struct
{
int value; //记录了这个信号量的值
struct process *list; //储存正在等待这个信号量的进程(PCB链表指针)
}semaphore;
wait实现:
wait(semaphore *S)//消耗资源
{
S->value--;
if(S->value<0) //没有资源
{
add this process to S->list; //进入等待队列
block(); //堵塞
}
}
signal实现:
signal(semaphore *S)//释放资源
{
S->value++;
if(S->value<=0)
{ //上面++后,S仍然还<=0,说明资源供不应求,等待者还有很多,于是唤醒等待队列中的一个
remove a process P from S->list;
wakeup(P); //唤醒进程p
}
}
在具有忙等的信号量经典定义下,信号量的值不会为负数,但是本实现可能造成信号量为负值。如果信号量为负值,那么其绝对值就是等待该信号量的进程的个数。信号量的关键之处是它们原子的执行。必须确保没有两个进程能同时对一个信号量进行操作,在单处理器情况下,可以在执行wait()和signal()的时候简单的关闭中断,保证只有当前进程进行。