聚焦操作系统中的PV操作

个人主页-爱因斯晨

文章专栏-操作系统

#前言

在操作系统学习中,PV操作这个板块较为复杂,本文用大白话一文讲清其中逻辑。

我们在学习进程控制的时候,绕不开进程同步和互斥问题。为什么会有这两个概念呢?随着多道进程并发执行,多个进程会共享系统资源。以打印机为例,多个进程同时访问同一台独占资源时,会发生资源争抢、数据错乱,因此需要进程互斥:指多个进程竞争临界资源,同一时刻仅允许一个进程访问,保证资源排他性使用。

而部分进程存在前后协作关系,必须遵循固定执行顺序,比如生产完成后才能消费,由此产生进程同步:指具有依赖关系的进程,按既定次序协调执行,保证任务有序推进。二者常借助 PV 操作实现并发环境下的正常运行。

为什么一定要用PV操作呢?别的方法不行吗?别的也是可以的,之前我们学进程互斥的时候学过两类方法:软件实现,硬件实现。软件实现就是靠自己写代码控制,靠几个变量互相标记,我要用了,你不要来。但是代码很复杂,且只能用单进程,多进程就乱套了,还满足不了。硬件实现解决了原子性的问题,但是都没有满足让权等待,这就是导致进程忙等,CPU浪费严重。

一、浅析PV操作

PV操作就很好的解决了这一点,通过阻塞和唤醒机制消除了忙等待的问题,当进程执行P操作资源不足时,进程会被阻塞并进入等待队列,主动放弃CPU资源,不再循环占用CPU, 待其他进程执行V操作释放资源后,再唤醒等待进程。因此信号量机制满足让权等待,资源利用率更高。

PV操作具体是什么?

要谈这个,就要先说什么是信号量,信号量相当于你的钱包余额,主要记录资源的载体,就是管理资源的计数器,说书面一点就是:操作系统用来管理共享资源、控制进程有序执行的一种机制,分为整形信号量和记录型信号量。

整型信号量只有一个整数数值,仅能记录资源数量,搭配简单的 P、V 操作,资源不足时进程会循环等待,产生忙等待,效率很低。只会数数,不会安排排队。

而我们课本所学的记录型信号量,在数值基础上增加了进程等待队列;这个就很高级了,会数数,还会让你排队等待,负责让你休息,和负责叫醒你,可以说是个很负责的管家。

P 操作负责申请资源,让信号量数值减一,资源不够时就把进程阻塞、进入队列等待并让出 CPU;

V 操作负责释放资源,让信号量数值加一,如果有进程在排队就将其唤醒。

二、经典例题

我们常常学的那几个例子:生产者-消费者问题,还有他的延申(多生产者-多消费者问题),读者写者,哲学家进餐问题。这些不是新的知识点,而是PV操作这个工具在不同场景下的不同应用。

2.1万能公式

复制代码
P=拿资源/等条件
V=放资源/发信号
mutex互斥信号量=1:只要多个进程抢同一个缓冲区、同一个公共数据,就加他,防止打架。(互斥锁)
同步信号量就是控制先后顺序的

2.2生产者-消费者(源头-祖宗)

场景

复制代码
生产者:往缓冲区放数据
消费者:从缓冲区拿数据
缓冲区是有限大小的仓库

两个硬性的规则

复制代码
1.仓库满了-生产者不准放,没地方放了(要等待)
2.仓库空了-消费者不准拿,没东西了咋拿(要等待)
3.仓库同一时间只能一个人操作(互斥)

三个信号量

复制代码
empty:空格子数量 → 初值 = 仓库大小
full:装满的格子 → 初值 = 0
mutex:互斥锁 → 初值 = 1

这里要特别注意:mutex的值的变化:

他的初始值一定是1,代表,临界区一次只能进一个进程。

只有两个操作
  • P(mutex) 上锁
  • V(mutex) 解锁
① 执行 P (mutex)
复制代码
mutex = mutex - 1
② 执行 V (mutex)
复制代码
mutex = mutex + 1

就这么简单。


全程三种情况,值怎么变(重点)
情况 1:没人在用资源

mutex = 1

进程 A 来了:

执行 P (mutex)

mutex = 0

✅ 上锁成功,A 进入临界区


情况 2:A 正在用(mutex=0)

这时进程 B 也想来,执行 P (mutex)

mutex = -1

mutex < 0

👉 B 阻塞、排队、睡觉,不能进去


情况 3:A 用完了,离开

A 执行 V (mutex)

mutex = 0

发现外面有人排队(B)

👉 自动唤醒 B,B 进去干活

B 结束再 V 一下,mutex 回到 1


总结 mutex 数值含义
  • mutex = 1:没人占用,空闲
  • mutex = 0:有 1 个进程正在用
  • mutex < 0 :有人在用,还有 |mutex| 个进程在排队等着

逻辑

生产者:

c 复制代码
P (empty)  // 先找空位,没空位就卡住
P (mutex)  // 上锁,独占仓库
放数据
V (mutex)  // 解锁
V (full)   // 成品 + 1,唤醒消费者

消费者:

c 复制代码
P (full)   // 先等有产品,没货就卡住
P (mutex)  // 上锁
拿数据
V (mutex)  // 解锁
V (empty)  // 空位 + 1,唤醒生产者

伪代码:

信号量定义:

c 复制代码
semaphore empty = n;   // 空缓冲区数量,初值n
semaphore full  = 0;   // 满缓冲区数量,初值0
semaphore mutex = 1;   // 互斥锁,初值1

生产者

c 复制代码
生产者进程(){
    while(1){
        生产一个产品;

        P(empty);   // ① 先同步:申请空位
        P(mutex);   // ② 后互斥:上锁

        把产品放入缓冲区;

        V(mutex);   // 先解锁
        V(full);    // 再通知消费者
    }
}

消费者:

c 复制代码
消费者进程(){
    while(1){
        P(full);    // ① 先同步:申请产品
        P(mutex);   // ② 后互斥:上锁

        从缓冲区取出产品;

        V(mutex);   // 先解锁
        V(empty);   // 再通知生产者

        消费产品;
    }
}

如果把 P(mutex) 写在最前面可以吗?

不可以!

c 复制代码
// ❌ 错误:互斥P在前,同步P在后
P(mutex);   // 先上锁
P(empty);   // 再等空位

会出现什么致命问题死锁!

举个场景:

  1. 缓冲区已经满了empty = 0

  2. 一个生产者先执行

    复制代码
    P(mutex)

    → 抢到锁,

    复制代码
    mutex=0

    ,把缓冲区

    死死锁住

  3. 然后执行

    复制代码
    P(empty)

    → empty 减 1 变成 -1,

    阻塞卡住

  4. 重点:

    它拿着 mutex 锁不放、又卡在原地睡觉

    ✅ 消费者想拿产品释放空位?

    进不来!因为拿不到 mutex

    ✅ 别的生产者也进不来

    👉 所有人全部卡死,死锁


2.3多生产者、多消费者

区别就一点

不止 1 个工厂、不止 1 个买家,一堆人同时生产、一堆人同时消费

会出现什么新问题

多个生产者同时往里塞 、多个消费者同时往外抢

更容易乱套、覆盖数据、重复拿数据。

解决办法

信号量完全不变,代码完全不变!

只靠 mutex 互斥锁拦住所有人:

不管你有多少个生产者、多少个消费者,

只要操作缓冲区,必须排队,一个一个来。

代码实现:

c 复制代码
semaphore empty = n;   // 空缓冲区数量
semaphore full  = 0;   // 满缓冲区数量
semaphore mutex = 1;   // 互斥锁:保护缓冲区
process 生产者()
{
    while(1)
    {
        生产产品;
        
        P(empty);   // 【先同步】申请空位
        P(mutex);   // 互斥上锁
        
        放入缓冲区;
        
        V(mutex);   // 解锁
        V(full);    // 唤醒消费者
    }
}
process 消费者()
{
    while(1)
    {
        P(full);    // 【先同步】申请产品
        P(mutex);   // 互斥上锁
        
        取出缓冲区数据;
        
        V(mutex);   // 解锁
        V(empty);   // 唤醒生产者
        
        消费产品;
    }
}

核心思路:

多生产、多消费,代码和单生产单消费完全一样

mutex 作用:

不管多少个进程,同一时刻只能一个人操作缓冲区

防止多个生产者同时放数据、覆盖出错

硬性规则:

P 同步在前,P 互斥在后,防止死锁

V 操作顺序随便换,不会出错

结论

多生产多消费 = 原版生产者消费者 + 加强互斥保护

套路一模一样,只是进程变多了,模型完全通用。


2.4读者--写者问题(第二大经典)

场景

一个文件:

  • 读者:只看、不改
  • 写者:修改、改动

规则

  1. 读和读:可以一起读(不用互斥)
  2. 读和写:绝对不能同时
  3. 写和写:绝对不能同时

核心思想

不用所有人都排队

  • 一堆读者一起看书没问题
  • 只要来了写者,全部锁住

类比:图书馆

自习的人随便坐(读者)

装修的人一来,全部清场(写者)

c 复制代码
semaphore rmutex = 1;   // 保护读者计数器
semaphore wmutex = 1;   // 读写、写写互斥
int read_count = 0;     // 记录当前读者数量
process 读者()
{
    while(1)
    {
        P(rmutex);        // 锁住计数器
        if(read_count == 0)
        {
            P(wmutex);    // 第一个读者:禁止写者进来
        }
        read_count++;
        V(rmutex);        // 释放计数器
        
        读取文件;          // 多个读者可以一起读
        
//只要没人在读(read_count=0)
//第一个进来的读者,直接锁住写者
//后面再来一堆读者:
//人数 + 1 就行,不用再锁文件,大家一起读
    
        P(rmutex);
        read_count--;
        if(read_count == 0)
        {
            V(wmutex);    // 最后一个读者:放开写者
        }
        V(rmutex);
    }
}

//只要还有人在读,写者继续等着
//最后一个读者离开
//才会解锁文件,放写者进来修改
    
process 写者()
{
    while(1)
    {
        P(wmutex);     // 上锁,禁止读、禁止其他写
        
        修改文件;
        
        V(wmutex);     // 解锁
    }
}
//写者非常霸道:
//只要拿到 wmutex,
//所有读者、其他写者 全部拦住

思路大白话

  1. 只用第一个读者 加写锁,最后一个读者解写锁

  2. 中间来的读者,直接读,不用反复加解锁,提高效率

  3. 写者全程独占,只要有人读,写者就进不来

    读者写者问题,核心就是为了提升效率,允许多个读者同时访问文件。依靠一个读者计数器统计当前阅读人数,用一把小锁保护计数不乱,再依靠文件大锁限制写者。

    依靠「首位读者上锁、末尾读者解锁」的规则,完美实现:读读可以并行、读写互相排斥、写写无法同时执行,这也是 PV 操作在不同场景下灵活运用的典型例子。

2.5哲学家进餐问题(最好理解防死锁)

场景

5 个哲学家坐一圈,

左右两边各一根筷子

要吃饭:必须同时拿到左右两根筷子

哲学家 i:左手筷子 i,右手筷子 (i+1)%5

坑点

如果 5 个人同时拿左手筷子

所有人都少一根,全部卡死 → 死锁

解决办法(三选一)

  1. 最多只允许 4 个人同时拿筷子
  2. 奇数先拿左、偶数先拿右(顺序错开)
  3. 必须一次性拿两根筷子

👉 本质:用 PV 操作限制资源申请顺序,防止卡死

c 复制代码
semaphore chopstick[5] = {1,1,1,1,1};
semaphore eat_limit = 4;  // 限制:最多4个人拿筷子,防死锁
process 哲学家(int i)
{
    while(1)
    {
        思考;
        
        P(eat_limit);        // 限制最多4人抢筷子
        
        P(chopstick[i]);     // 拿左手筷子
        P(chopstick[(i+1)%5]);// 拿右手筷子
        
        吃饭;
        
        V(chopstick[i]);
        V(chopstick[(i+1)%5]);
        
        V(eat_limit);        // 释放名额
    }
}

核心防死锁思路

原本会死锁的情况:

5 个人同时拿起左手筷子,所有人都缺右手筷子,全员卡死。

解决办法:

加一个eat_limit=4

最多允许 4 个哲学家竞争筷子

必然至少有一个人能拿到两根筷子、顺利吃饭

打破死锁的「循环等待」

哲学家进餐问题,是典型的资源争夺造成死锁的案例。

五位哲学家环形就坐,筷子相互共用,若所有人同时抢占单侧筷子,就会互相僵持、全部无法进餐。

利用 PV 操作增加人数限制,只允许最多四人竞争筷子资源,破坏循环等待的条件,保证总有哲学家可以拿到一双筷子正常吃饭。

本质就是通过合理限制资源抢夺顺序与并发数量,用 PV 操作预防死锁

三、总结

综上所述,信号量与 PV 操作是解决进程同步、互斥问题的核心手段。相比于有明显缺陷的软件、硬件实现,以记录型信号量为基础的 PV 操作,依靠阻塞与唤醒机制,真正实现了让权等待,大幅提升系统资源利用率。

生产者消费者、多生产多消费、读者写者、哲学家进餐等经典问题,本质上都是 PV 操作的实际应用延伸,万变不离其宗。只要牢记P 负责申请等待、V 负责释放唤醒 ,分清同步信号量与互斥mutex的作用,严守「先同步、后互斥」的 P 操作顺序,就能避免死锁与数据混乱。弄懂一套核心逻辑,就可以轻松吃透所有进程并发的经典模型。

相关推荐
2301_813599552 小时前
CSS中relative与absolute的区别_详解相对与绝对定位应用场景
jvm·数据库·python
云泽8082 小时前
笔试算法 - 双指针篇(一):移动零、复写零、快乐数与盛水容器
c++·算法
qq_372154232 小时前
c++怎么在写入文件流时通过peek预读功能实现复杂的逻辑判断【实战】
jvm·数据库·python
m0_514520572 小时前
CSS如何给按钮添加按下缩小的动画_利用-active配合transform
jvm·数据库·python
willhuo2 小时前
# 自动化数据采集技术研究与实现:基于Playwright的抖音网页自动化方案
运维·selenium·c#·自动化·chrome devtools·webview
yejqvow122 小时前
CSS如何制作加载时的点点点跳动效果_使用animation循环延迟
jvm·数据库·python
2401_835956812 小时前
CSS如何解决CSS引入后的样式覆盖_理解优先级原则避免重写
jvm·数据库·python
爱学的小码2 小时前
MySQL(进阶)--存储过程和触发器
数据库·oracle
小旭95272 小时前
MySql调优详解
数据库·mysql·数据库架构