PV操作是一种用于实现进程间同步与互斥的核心机制,由荷兰计算机科学家E.W.Dijkstra提出。它包含两个不可中断的原子操作,通常与信号量(Semaphore)结合使用。
1. PV操作与信号量
信号量是一个用于控制多个进程对共享资源访问的整型变量。其值通常代表可用资源的数量。根据信号量的用途,其初始化值有所不同。PV操作的定义如下:
- P操作(Proberen, 意为“尝试”或“等待”): 当一个进程需要申请一个资源时执行P操作。它会检查信号量的值,如果值大于0,则将其减1,表示成功获取一个资源;如果值等于0,则进程被阻塞,进入等待队列,直到有资源被释放。
- V操作(Verhogen, 意为“增加”或“释放”): 当一个进程释放一个资源时执行V操作。它会将信号量的值加1。如果此时有进程正在等待该资源(即等待队列非空),则会唤醒其中一个等待进程。
信号量可分为:
- 互斥信号量 (Mutex): 用于实现互斥访问,其初始值通常为1,保证同一时刻只有一个进程能进入临界区。
- 资源信号量: 用于实现同步,其初始值代表某类资源的初始可用数量,例如生产者-消费者问题中的缓冲区空位数量(empty)或已存放产品数量(full)。
2. PV操作的应用模型
PV操作是解决一系列经典并发问题的基石。
2.1 生产者-消费者问题
这是最经典的同步问题。生产者进程生产数据放入缓冲区,消费者进程从缓冲区取出数据消费。需要解决三个同步互斥点:1)缓冲区满时生产者等待;2)缓冲区空时消费者等待;3)缓冲区的访问必须是互斥的。
假设缓冲区大小为N,使用三个信号量:
mutex: 互斥信号量,初始为1,用于保证对缓冲区的互斥访问。empty: 资源信号量,初始为N,代表空缓冲区数量。full: 资源信号量,初始为0,代表已填充的缓冲区数量。
其伪代码实现如下:
// 生产者进程 void producer() { while (true) { 生产一个产品 item; P(empty); // 申请一个空缓冲区,若无则等待 P(mutex); // 申请进入临界区(访问缓冲区) 将 item 放入缓冲区; V(mutex); // 离开临界区,释放缓冲区访问权 V(full); // 释放一个“已填充”资源,通知消费者 } } // 消费者进程 void consumer() { while (true) { P(full); // 申请一个已填充的产品,若无则等待 P(mutex); // 申请进入临界区(访问缓冲区) 从缓冲区取出一个产品 item; V(mutex); // 离开临界区,释放缓冲区访问权 V(empty); // 释放一个“空位”资源,通知生产者 消费 item; } }注意:两个P操作的顺序至关重要。生产者必须先P(empty)再P(mutex),消费者必须先P(full)再P(mutex)。如果顺序颠倒,可能导致死锁。例如,若生产者先P(mutex)获得了缓冲区锁,但发现empty=0(缓冲区满)而阻塞,此时消费者因无法获得mutex而无法消费释放空位,双方将永远等待下去。
2.2 读者-写者问题
该问题中,多个读者进程可以同时读取共享数据,但写者进程必须独占访问。它更侧重于实现复杂的同步策略,例如“写者优先”或“读者优先”。
一个简单的“读者优先”模型(可能导致写者饥饿)可以使用一个互斥信号量rw_mutex(控制写互斥,初始为1)和一个计数器read_count(记录当前读者数量)及其保护锁mutex(初始为1)来实现。
// 写者进程 void writer() { while (true) { P(rw_mutex); // 申请独占写权限 执行写操作; V(rw_mutex); // 释放写权限 } } // 读者进程 void reader() { while (true) { P(mutex); // 保护 read_count read_count++; if (read_count == 1) { P(rw_mutex); // 第一个读者需要“锁住”写者 } V(mutex); 执行读操作; P(mutex); read_count--; if (read_count == 0) { V(rw_mutex); // 最后一个读者“释放”写者 } V(mutex); } }3. 使用PV操作实现进程互斥
对于只需要互斥访问的临界区,使用一个初始值为1的互斥信号量即可。
semaphore mutex = 1; // 互斥信号量 void process_i() { while (true) { // ... 剩余区 P(mutex); // 进入区:尝试获取锁 // ... 临界区(访问共享资源) V(mutex); // 退出区:释放锁 // ... 剩余区 } }4. 信号量的底层实现与注意事项
PV操作必须是原子操作,即在执行期间不可被中断。在单处理器系统中,可以通过屏蔽中断来实现;在多处理器系统中,则需要借助硬件提供的特殊原子指令(如Test-and-Set, Compare-and-Swap)。
信号量的实现通常包含一个整型值value和一个进程等待队列queue。P操作和V操作的具体逻辑如下表所示:
| 操作 | 伪代码逻辑描述 |
|---|---|
| P(semaphore S) | S.value--;if (S.value < 0) {将当前进程状态置为“等待”;将当前进程PCB插入S.queue;调度其他进程运行;} |
| V(semaphore S) | S.value++;if (S.value <= 0) {从S.queue中移出一个等待进程P;将进程P状态置为“就绪”;将P插入就绪队列;} |
核心要点:当S.value为负数时,其绝对值表示正在等待该信号量的进程数量。
在使用PV操作时,必须警惕死锁。死锁发生的四个必要条件是:互斥、持有并等待、非抢占、循环等待。错误的P操作顺序(如前文生产者-消费者问题所述)或资源分配不当都可能引发死锁。银行家算法是一种经典的死锁避免算法,它通过预判分配后系统是否处于安全状态来决定是否分配资源。
参考来源
- pv原语模拟实现_手把手教你如何玩转操作系统信号量和PV操作
- PV操作
- 【操作系统学习笔记(二)】之 进程,进程调度,进程同步与互斥
- 计算机四级嵌入式之操作系统原理(四)并发与同步
- 信号量
- Semaphore(信号量)