📜  进程同步中的信号量

📅  最后修改于: 2021-09-27 22:43:04             🧑  作者: Mango

先决条件:进程同步,互斥与信号量
信号量是由 Dijkstra 在 1965 年提出的,它是一种非常重要的技术,通过使用一个简单的整数值来管理并发进程,这被称为信号量。信号量只是一个非负的变量,在线程之间共享。该变量用于解决临界区问题并在多处理环境中实现进程同步。
信号量有两种类型:

  1. 二进制信号量 –
    这也称为互斥锁。它只能有两个值——0 和 1。它的值被初始化为 1。它用于实现多进程临界区问题的解决方案。
  2. 计数信号量 –
    它的值可以跨越一个不受限制的域。它用于控制对具有多个实例的资源的访问。

现在让我们看看它是如何做到的。
首先,看两个可用于访问和更改信号量变量值的操作。

操作系统中的 P 和 V 操作

关于 P 和 V 操作的一些观点

  1. P 操作也称为等待、睡眠或向下操作,V 操作也称为信号、唤醒或向上操作。
  2. 这两个操作都是原子的,并且信号量总是被初始化为 1。这里的原子意味着读取、修改和更新同时/时刻发生而没有抢占的变量,即在读取、修改和更新之间不执行可能更改变量的其他操作。
  3. 临界区被两个操作包围以实现进程同步。见下图。进程 P 的临界区在 P 和 V 操作之间。

现在,让我们看看它是如何实现互斥的。假设有两个进程 P1 和 P2,一个信号量 s 被初始化为 1。现在如果假设 P1 进入它的临界区,那么信号量 s 的值变为 0。现在如果 P2 想要进入它的临界区,那么它会等到 s > 0,这只能在 P1 完成其临界区并对信号量调用 V 操作时发生。这样就实现了互斥。查看下图了解二进制信号量的详细信息。

二元信号量的实现:

CPP
struct semaphore {
    enum value(0, 1);
 
    // q contains all Process Control Blocks (PCBs)
    // corresponding to processes got blocked
    // while performing down operation.
    Queue q;
 
} P(semaphore s)
{
    if (s.value == 1) {
        s.value = 0;
    }
    else {
        // add the process to the waiting queue
        q.push(P)
        sleep();
    }
}
V(Semaphore s)
{
    if (s.q is empty) {
        s.value = 1;
    }
    else {
 
        // select a process from waiting queue
        Process p=q.pop();
        wakeup(p);
    }
}


CPP
struct Semaphore {
    int value;
 
    // q contains all Process Control Blocks(PCBs)
    // corresponding to processes got blocked
    // while performing down operation.
    Queue q;
 
} P(Semaphore s)
{
    s.value = s.value - 1;
    if (s.value < 0) {
 
        // add process to queue
        // here p is a process which is currently executing
        q.push(p);
        block();
    }
    else
        return;
}
 
V(Semaphore s)
{
    s.value = s.value + 1;
    if (s.value <= 0) {
 
        // remove process p from queue
        Process p=q.pop();
        wakeup(p);
    }
    else
        return;
}


上面的描述是针对只能取 0 和 1 两个值并确保互斥的二进制信号量。还有另一种类型的信号量称为计数信号量,它可以采用大于 1 的值。

现在假设有一个资源的实例数为 4。现在我们初始化 S = 4,其余的与二进制信号量相同。每当进程需要该资源时,它就会调用 P 或等待函数 ,完成后它会调用 V 或信号函数。如果 S 的值变为零,则进程必须等到 S 变正。例如,假设有 4 个进程 P1、P2、P3、P4,它们都调用了对 S(初始化为 4)的等待操作。如果另一个进程 P5 需要该资源,那么它应该等到四个进程之一调用信号函数并且信号量的值变为正值。

限制:

  1. 信号量的最大限制之一是优先级反转。
  2. 死锁,假设一个进程试图唤醒另一个未处于睡眠状态的进程。因此,死锁可能会无限期地阻塞。
  3. 操作系统必须跟踪所有等待和向信号量发出信号的调用。

信号量的这种实现中的问题:

信号量的主要问题是它们需要忙等待,如果一个进程在临界区,那么其他试图进入临界区的进程将一直等待,直到临界区没有被任何进程占用。
每当任何进程等待时,它都会不断检查信号量值(查看此行 while (s==0); 在 P 操作中)并浪费 CPU 周期。

也有可能出现“自旋锁”,因为进程在等待锁定时会继续自旋。

为了避免这种情况,下面提供了另一种实现方式。

计数信号量的实现:

CPP

struct Semaphore {
    int value;
 
    // q contains all Process Control Blocks(PCBs)
    // corresponding to processes got blocked
    // while performing down operation.
    Queue q;
 
} P(Semaphore s)
{
    s.value = s.value - 1;
    if (s.value < 0) {
 
        // add process to queue
        // here p is a process which is currently executing
        q.push(p);
        block();
    }
    else
        return;
}
 
V(Semaphore s)
{
    s.value = s.value + 1;
    if (s.value <= 0) {
 
        // remove process p from queue
        Process p=q.pop();
        wakeup(p);
    }
    else
        return;
}

在这个实现中,每当进程等待时,它就会被添加到与该信号量关联的进程的等待队列中。这是通过该进程上的系统调用 block() 完成的。当一个进程完成时,它调用信号函数并恢复队列中的一个进程。它使用wakeup() 系统调用。