152.OS补充(一)---进/线程_PV同步互斥问题

OS操作系统的综合题整理如期到来
关于OS的综合题目主要是PV同步互斥问题+存储管理/文件系统+其他
这一章也是对之前那些什么哲学家问题的再总结理解 这也是综合题最常考察的内容 主要总结PV所需的重要前缀知识 过多关于进/线程的知识会单独放到一个总结里

1.同步 互斥 临界资源

同步:很好理解 如对于A和B需要双发协同 A需要B的某个东西才能继续 B提供A需要的东西
互斥:一个进程使用临界资源 另一个进程必须等待
这些问题关于之前的java有关代码总结足够详细了

所谓临界资源即是否多个进程共享的资源且一次只能一个用 举个不恰当的例子,一个女人可以被多个男的追求 但一次只能一个使用

理解临界资源访问过程
image
临界区(临界段):就是访问临界资源的区域
退出区即将访问临界区的标志清除 剩余区是代码的其他区域
进入区:检查进程是否能进入临界区 若能为其设置标志

临界区互斥必须遵循准则:
空闲让进:临界区空闲 可以让进程进入临界区
忙则等待:临界区被占用时 其他必须等待
有限等待:有限时间内进入临界区 防止无限等待
让权等待:不能进入时 立即释放处理器 防止忙等待 (并非必须遵循)

这是分析后续实现临界区进程互斥方法的判断优缺点

再进行下述的知识点论述之前 我们先总的以2024一道综合题来看这节内容的主要考察点 基本都含沙射影的涉及到了
(2024)
image
(1)两个进程都执行C1对B进行写操作 但一次只能一个操作 自然C1的操作为共享互斥资源即临界资源
(2)B初始空 很明显就是同步问题 只有执行C1完事了才能执行C2
image
同步信号量B 初值为0 当P1执行完操作然后释放B+1 这是非空了P2等待后可以操作
(3)B初始不为空 俩进程都执行C3修改操作 但一次只能一个操作 所以互斥关系
image
互斥信号量B 初值1 然后俩进程都是占用互斥信号量然后操作再释放

基本上这就是本篇主要涉及的知识点考察就是搞懂互斥同步关系 使用方法(一般是信号量)来表示出进程的关系

2.实现临界区互斥的基本方法

分为软件实现和硬件实现 都涉及考察
先介绍硬件实现 也是为后续最重要的信号量实现铺垫

  • 中断屏蔽方法
  • 硬件TS指令
  • 硬件Swap指令

介绍之前关于核心就是实现互斥这一概念 为的就是临界区只有一个进程访问 释放后再给其他进程

中断屏蔽方法

一句话就是一个进程访问的时候直接断掉 其他就访问不了
而中断屏蔽即关中断的时候是否被响应 为进程访问临界区代码顺利执行完
image
开中断即进程访问临界区完事了 允许其他进程的中断请求进入
缺点比较明显 断->访问->开->断.... 并没有并发执行没法发挥CPU能力 效率低
交给进程来关中断 万一不开中断其他就进不来 系统可能出问题

来看题
(2021)
image
image
这里也涉及到后续信号量的有关问题 后续介绍
(1)可以看到S是多个进程可共享访问的资源即临界资源 所以需要互斥实现
(2)看方法1和2都有明显的中断指令 所以这里就是中断屏蔽实现
image
问题在于这里 等待操作:关中断 访问临界区代码 如果资源数S就不够呢 那就死循环了 其他进程可能释放资源也不允许断开给其他进程操作 正确方法参考方法2即可
(3)之前介绍过开/关中断指令肯定不能交给用户 万一断开不放开有可能造成问题 本身有关I/O中断指令是特权指令OS内核态才可以系统调用的

硬件TS指令

借助此原子操作TestAndSet指令实现
首先需要看懂这条指令实现了啥

bool TestAndSet(bool *lock){
	bool old;
	old=*lock;
	*lock=true;
	return old;
}

核心在于进入区可访问临界区的进程设置标志
这个lock就是为临界资源设置的一个锁:true表示占用 false表示空闲
程序代码为的就是检查并加锁

重点理解互斥的过程
image
这个就一目了然了
循环TS直到false跳出循环 也就相当于空闲了 那么就访问临界区然后释放lock置为false
而TS重点lock置为true就是空闲的时候就上锁因为要进程执行完代码才能给其他进程访问true循环等待

硬件Swap指令

void Swap(bool *a,bool *b){
	bool tem=*a;
	*a=*b;
	*b=temp;
}

关于swap实现的就是交换内容 具体实际意义是什么 得看过程怎么实现

image
这里用到一个key初值true 用于交换lock
其实本质上的理解跟TS一样 就是循环 然后满足条件跳出 访问临界段 对比即可理解原理
它和TS都是循环就是一个进程等的时候无限等 而临界区有个准则让权等待即不能进入临界区时应该释放处理器 这俩没实现但不是必须的准则 但swap后者随机选择一个进程进入的临界区其他可能一直选不上 导致饥饿现象

具体到题目中
(2023)
image
(1)分析代码
image
顺序执行 无论key和lock的值是啥 下一步都是临界区代码 所以压根没意义
肯定得循环啊 让一个进程进其他等待
换成 while(key!=False) 也就是key为true的是否进行交换
(2)newSwap(&key,&lock)传入的为地址 其他都用且传递这个地址 那么并发执行调用它就可能混乱

再来看软件实现

  • 单/双标志法
  • Peterson算法

主要就是以代码编程实现为主 所以需要理清代码逻辑即可理解

单标志法

while (turn!=0);  //进入区
critical section;//临界区
turn=1;  //退出区
remainder section;//剩余区

很好理解:
turn:规定哪个进程进入 =1就是让进程1进入
当进程访问完退出 将turn置为其他 比如置为5就是让进程5进
这样就是交替进的 只有一个进程在访问实现了互斥

  • 但是如果它不进入临界区 就无法执行turn置为其他进程 这样就其他进程无法访问
  • 还有就是比如一个进程访问完了让另一个进程访问 但另一个进程不尽 那么turn一直就是这个值 其他都进不了

双标志法

while(flag[1]);
flag[0]=true;
critical section;
flag[0] = false;
remainder section;

核心理解flag[i]=true:表示编号i进程想要进入临界区
这里就是比如进程0 首先判断进程1是不是想进入 如果想循环等待 然后将自己设为true访问临界区 然后访问完设为false 这里是先检查后设置 可能多个进程同时检查这里涉及并发问题 有可能同时通过

所以对应的就有先设置自己要进 然后再检查其他进程想不想进
只需要将前两行代码互换即可 当然这样的情况就会出现都设置了 也都想进 就出现竞争一个也没进入 造成饥饿现象

这是原理的理解 来一道题目来分析
image
(1)猛地一看没啥问题不就是上述双标志先检查法的实现吗 但是注意bool flag[2] 初值都是false
那么比如进程0和1都是false 那么while就不会发生 进程0true访问进程1true也访问 不是互斥访问了
(2)置换一下就是双标志后检查法的实现 但影响依旧是bool flag[2] 初值false
那么进程0和进程1都设置为true 进程1满足条件循环等 进程2也满足条件循环等 就死锁

关于死锁的知识点会单独搭配调度等单独总结一篇 注意判断这一切的有关进程访问是否违背互斥准则 核心就是进程是并发执行的 也有可能会使得进程切换 正执行这个进程突然一个中断切换另一个进程执行 所以这就是判断出现问题的根据

Peterson算法

flag[0] = true;
turn=1;
while (flag[1] && turn==1);
critical section;
flag[0] = false;
remainder section;

这个就是结合了单标志法和双标志后检查法 利用flag[]解决互斥访问问题 利用turn解决饥饿问题
软件实现中较优秀的算法
很简单核心就是谦让-优先谁进问题
比如:
进程0要进 turn=1就是让1进 turn决定优先让谁进
判断1想进且turn=1那么相对的进程1想进且turn=0 此时只允许一个进程进入

3.信号量

其实就是两个原语操作 P wait() 和 V signal()

  • 原语:不可被中断,分割的原子操作 若被打断否则可能去运行另一个对变量的操作过程出现临界段问题

最常用的就是整型信号量 之前的2021题也体现出来了

wait(S){
	while(S<=0);  资源不够 等待
	S=S-1;	 使用资源
}

signal(S){
	S=S+1;   释放资源
}

核心就是S整型表示资源数量

还有就是记录型信号量
关于实现代码没有具体的样式 多了一个进程链表链接所有等待资源的进程

image
题目中说了两个整型变量记录取号值和叫号值 同时叫号和取号只能一个进行所以互斥 同样设置一个信号量
我们直接顺带分析了这种实际问题 作个铺垫
image
叫号值和取号值设为0 这个好理解
这里还设置了信号量取号和叫号操作设为1 表示互斥 就是一时刻 取号和叫号操作只能一个进行
顾客很好理解 取号操作占用资源 取号 取号值+1 释放取号操作
销售 叫号操作 只有当取号>叫号时候才叫了 否则不叫 比如取号现在才到4 你就叫5了肯定不行

这是个简单的例子来看联系实际问题中信号量实现互斥同步问题
搭配后续知识理解原理+更多练习

4.信号量实现进程 互斥 同步

互斥

核心就是设置一个互斥信号量S初值1 可用资源数1
S=0就是有一个进程占用了 S>0就无进入临界区的 S<0表示多个进程等待 数目为|S|
image
非常容易理解

  • 临界资源需要设置为互斥信号量 初值就是原资源数 无给出具体就为1
  • PV成对出现 比如进程1 P操作占用加锁 访问完了 V操作释放 其他进程再访问
  • 临界资源可能在实际题目中有多个 找到这些互斥关系 设置信号量

我们先用一个小克拉比题练练手
(2017)
image
其实就本质还是理解代码结构
三步走

  • 1.确定互斥关系的临界资源 设置信号量
  • 2.理清进程的各个关系
  • 3.对应原理 利用PV操作实现互斥

同样的同步也是类比上述实现
image
进程1:用x y y和进程3的y联系 是直接输入y还是进程3计算出来的y再输入
进程2:用y z y同样和进程1一样和进程3联系 z也和进程3联系
进程3:其z y和进程1和2联系
所以第一步找到了临界资源:
Sem(1-3的y) Sem(2-3的y) Sem(2-3的z)
因为互斥关系题目没有明确给出原资源数 所以设置初值1
然后分析关系 编写操作
image
前两个进程是正确的写法 第三个进程错误
我们正好以这个为例 分析关系的写法

image
首先执行add用到z和w z是互斥信号量2-3的 所以先P占用 然后计算 释放
再者是互斥信号量y 这个和1和2都是互斥关系 P占用这两个 现在y只是进程3使用 运算完释放

更多的是配合同步问题的共同考察

同步

这个是考察最多的东西 先搞懂什么是同步
之前介绍的互斥就是一个进程干活 其他进程必须等待
而同步则是并发执行 一说并发 这就体现它的重要性了 因为这个才是实际中最常用的
同样需要一个同步信号量S 初值为0
具体何种意义一会放到实际题目中理解
image
从这其实简单可以看出来 这就是P1用完S后释放了 对应P2才可以使用 需要等这个完成 但是这两个进程是并发执行的 不扯那么多废话 直接拿题来看
(2022)
image
图很好理解 但是更多的是理解题目的意义
三步走 类似互斥

  • 1.找到同步信号量 设置同步信号量
  • 2.分析进程之间的同步关系
  • 3.利用PV操作实现同步
    image
    这是总的分析 很好理解 层层递进
    C操作需要AB的完成 DE操作需要C完成 F操作需要E完成
    在具体落实到进程中
    image
    可以看到同步信号量已经出来了 也就是T1等C完成然后EF T2等A完成然后CD
    image
    A操作和B操作直接进行
    进程1释放A-C意思就是B可以开始操作C了 然后等待进程2释放C 然后操作EF
    进程2等待A的完成 然后操作C 释放C-E进程1可以操作E了 然后操作D

同样的一道类似题目 不带图 需要自己画出关系
(2020)
image
再二逼也能理清的关系 画出个图真不难
image
同步信号量:AC BC CE DE即C操作需要等A C操作需要等B E操作需要等C E操作需要等D
image
2022和2020代表着利用信号量实现的前驱关系 也就是完成某一步操作需要前一步的前提 其实本质就还是同步问题 所以没有单独总结 归纳到同步一齐

用一道稍微复杂点的实际问题中应用 更多是这种的考察

某工厂有两个生产车间和一个装配车间,两个生产车间分别生产 A, B 两种零件,装配车间的任务是把 A, B 两种零件组装成产品。两个生产车间每生产一个零件后,都要分别把它们送到专配车间的货架 \(F_{1}, F_{2}\) 上。 \(F_{1}\) 存放零件 A, \(F_{2}\) 存放零件 B, \(F_{1}\)\(F_{2}\) 的容量均可存放 10 个零件。装配工人每次从货架上取一个零件 A 和一个零件 B 后组装成产品。请用 P, V 操作进行正确管理。

先得出进程啊 俩进程-俩生产车间 一个造/放A 一个造/放B 然后还有一个进程-装配车间 取并组装AB
再来分析信号量的设置 首先互斥信号量F1上的A一次放/放一个 F2上B也是一次放/取一个 初值为0
同步信号量很好理解就是 货架F1和F2的容量
综合理解就是:可以同步拿/取F1上的A和F2上B 但只可以一次拿/放一个即F1上的A和F2上的B放/取操作为互斥的

这个就理清信号量的设置了
然后再来梳理同步互斥关系
image
注意同步信号量初值10 表示的是资源数 车间释放后就减少一个 说明放上去了 组装释放后就加一个说明取下来一个
车1:造A->等F1上能不能放 等互斥放A操作->放A->释放互斥A操作和货架上可放数-1 车2类比即可
组装车间:看F1上放了没 互斥操作A->取一个A->释放互斥操作和货架上数+1
比较绕 但逻辑上还算简单

再来一道复杂的真题
(2025)
image
先来理进程 很简单就是甲乙丙三个进程 看他们都干什么
甲:用铁锹挖树
乙:放树苗 用铁锹填土
丙:浇水
首先最容易得出的就是铁锹是一个信号量 甲乙互斥的使用 还有就是乙放了树苗丙才可以浇水同步关系
然后题目给出 树坑树<3 甲才能挖 所以树坑数这里也要当成信号量使用
image
设置的信号量的意义是:
三角:铁锹 互斥使用 初值为1
w:浇水数 初值0 乙和丙的同步信号量
空:可用的树坑数 甲乙的同步信号量
p:能挖的洞的数量 甲乙的同步信号量
前两个好理解 对应丙操作就是等 乙放一个树苗释放+1 丙浇水
主要难点在于后两个信号量的想法 一个是能挖多少树坑 决定甲能不能挖 主要来源于乙释放-1 填一个就少一个坑
然后就是可用的树坑数 决定乙能放/填 来源于甲 每挖一个就释放少一个

继续看题
image
首先互斥信号量题目给出 输入设备一时刻只有一个能使用
P1:输a 等2的b x=a+b 等y 等z计算好了输出打印
P2:输b y=a*b
P3:输c 等y z=y+c-a
image

最后看一道实际题
image
(1)很好理解题目给出 每次只一个车辆行驶 很明显就是互斥问题 直接套用就行
image
(2)难点理解的第二问 意思就是等一个方向的车全走完后 另一个方向的车才可以走 这部本质上还是互斥问题 只不过将多辆车当成了一个整体 所以这个时候就需要多一个量就是车的数量
image
总体上还是第一问 只不过变成了综合的 大致意思就是 比如南往北走有8辆车 刚开始0 如果数量0那么就占桥 过车数量+1 然后如果数量0说明过完了就释放 没有到0那就减

5.经典同步问题-哲学家进餐问题

经典的同步问题关于这个自然是最难理解的点 之前总结过关于这点的但是不够理解 所以再次以综合题搭配再来理解

image
我们围绕的核心解决就是这张图 5个哲学家 五个碗 五根筷子
没错跟弱智一样不就拿碗拿筷子吃饭吗 介尼玛的最有意思的是五根筷子放到一个人的两边
一个人吃饭拿起左右俩筷子那么旁边的人就吃不了了 所以筷子是互斥的访问
最直观简单的理解:

semaphore chopstick[5] = {1, 1, 1, 1, 1}; // 初始化 5 根筷子的信号量
Pi() {
    do {
    P(chopstick[i]); // 拿起左侧筷子
    P(chopstick[(i+1)%5]); // 拿起右侧筷子
    进餐
    V(chopstick[i]); // 放回左侧筷子
    V(chopstick[(i+1)%5]); // 放回右侧筷子
    思考
    } while(1);
}

这个很容易理解 但是问题就在于五个人同时吃饭 拿筷子先拿左边 再拿右边没了 这不就死锁问题出现了
关于死锁的详细介绍后续会单独再总结 简单理解就是满足其条件(互斥 不可剥夺 请求并保持 循环等待)

为解决这类问题就三种方式也就是相当于破坏其死锁的四个条件之一:

  • 最多允许四个人同时吃
  • 当一名哲学家左右两边筷子都可以用时 才允许拿筷子
  • 奇数哲学家先拿左边后拿右边 偶数哲学家相反

搞懂这三种饥饿死锁的方式 也就搞懂这类哲学家进餐问题的本质了

限制人数-破坏循环等待条件

很好理解 比如限制四个人吃饭
image
首先一人肯定先拿一根筷子 这时候还剩一根 那么一个人肯定能吃 吃完就释放了两根筷子 然后两个人又能吃了 不会有循环等待了

semaphore room = 4;
semaphore chopstick[5] = {1,1,1,1,1};

while (true) {
    think();
    P(room);                 // 进入竞争区
    P(chopstick[i]);         // 拿左边筷子
    P(chopstick[(i+1)%5]);   // 拿右边筷子

	eat();

    V(chopstick[i]);         // 放左边
    V(chopstick[(i+1)%5]);   // 放右边

    V(room);                 // 离开竞争区
}

同时拿-破坏请求并保持条件

拿着一根筷子等另一个筷子 发现另一个筷子没有 就死等也不放自己手里的 死锁
核心即当两根筷子都可以拿的时候才拿

semaphore mutex = 1;
semaphore chopstick[5] = {1,1,1,1,1};
while (true) {

    think();

    P(mutex);

    if (chopstick[i] == 1 &&
        chopstick[(i+1)%5] == 1) {

        chopstick[i] = 0;
        chopstick[(i+1)%5] = 0;

        V(mutex);

        eat();

        P(mutex);

        chopstick[i] = 1;
        chopstick[(i+1)%5] = 1;

        V(mutex);
    }
    else {
        V(mutex);
        // 暂时不能吃,继续等待/重试
    }
}

代码核心就是条件 检查左右两边是不是都有筷子 如果有就拿两根吃 然后释放 没有就等

错开顺序-破坏循环等待条件

原来的方式就是都先拿一边的然后再拿另一边的不够了 造成死锁
现在改变顺序
image
比如奇数先左 偶数先右 这不4号/1号/3号就可以吃然后释放 不造成循环等待了

semaphore chopstick[5] = {1,1,1,1,1};
while (true) {

    think();

    if (i % 2 == 1) {
        // 奇数哲学家
        P(chopstick[i]);             // 左
        P(chopstick[(i+1)%5]);       // 右
    }
    else {
        // 偶数哲学家
        P(chopstick[(i+1)%5]);       // 右
        P(chopstick[i]);             // 左
    }

    eat();

    V(chopstick[i]);
    V(chopstick[(i+1)%5]);
}

并无非要条件规定奇数怎样 只要打破原来的循环顺序即可 上述就是以5个哲学家为例 具体到题目中以实际的数为准 主要理解核心原理后就可以都理解了

看题
(2019)
image
核心限制条件多了一个碗的限制 需要两根筷子和一个碗才可以吃饭 用上述三种思想来解决
无论所有这类都是一个模板

1.限制人数:
P(room);              // room = n-1

P(左);
P(右);
P(其他限制条件);

eat();

V(其他限制条件);
V(左);
V(右);

V(room);
2.同时满足才拿
P(mutex);

if (左满足 && 右满足 && 其他限制条件满足)
{
    一次获得;
}

V(mutex);

eat();

P(mutex);

释放资源;
V(mutex);
3.错开顺序 奇先左 偶先右
if (i % 2 == 1)
{
    P(左);
    P(右);
}
else
{
    P(右);
    P(左);
}

P(其他限制条件);

eat();

V(其他限制条件);
V(左);
V(右);

不能说关于此类问题百分百理解 但十之八九可以做出一二来 反映到具体实际中 其实解决的就是多个进程竞争多个有限资源的问题 算法的设计为的就是解决底层原理的臃肿不便 使其更加高效 当然这需要天赋来理解底层和对其敏感的觉察性 而现在的市场更多的是复刻重复没有这些创造性的东西 研究这些底层原理和优化这些需要深层次的天赋和愿意钻研的静心 我很喜欢静下心来钻研一些东西 但我的先天条件无论是大脑还是出身都不好 所以我需要更多的时间精力投身于解决外部条件但终有一天会有足够的时间和从容的条件来支撑做这些事情

关于OS中的PV问题大致就是这些 后续还有一个较为重要的即文件管理+存储管理综合题

posted @ 2026-09-11 10:45  蒸饺  阅读(4)  评论(0)    收藏  举报