L2.9 进程同步与信号量
让进程走走停停,实现进程同步。
1.、信号量的定义
- 生产者
Producer
需要判断是否还有空闲缓冲区生产资源,所以定义一个标志empty,初始值为最大可用资源数,在开头维护;同时,在消费者Consumer
中释放了一个资源后,需要释放一个empty,在结尾维护。 - 消费者
Consumer
需要判断是否有资源需要去执行,所以定义一个标志full,初始值为0,在开头维护;同时,在生产者Producer
产生新的资源需要增加full,在结尾维护。 - 共享缓冲区 定义在文件中,所以要考虑文件读写的互斥,定义了一个锁mutex,初始值为1。任何一个使用时,调用
P()
,mutex--,则mutex=0。当另外一个调用P()
时,mutex=-1,此时小于零,将等待。
L2.10 信号量临界区保护
用临界保护信号量,用信号量实现同步
1、竞争条件:和调度有关的共享数据语义错误
若empty修改到一半,跳转到其他进程运行,将导致少减一个。
2、临界区
临界区:一次只允许一个进程进入的一段代码。
- 互斥进入
- 有空让进
- 有限等待
3、两个进程的临界区策略
4、临界区保护:面包店算法(软件方法)
- 每次进来取个号,取号值为当前最大号再+1
while
中判断如果别人想进 且 别人的号比我的小,则我等着choossing
用来保证:有人在选号时,我就等着,等选完好再进行判断
5、临界区保护:硬件方法
-
关中断:两个进程不能同时进入临界区 -> 阻止调度 -> 关中断(cli)。
- 缺点:在多CPU下失效,关中断只能控制一个CPU,另一个CPU不管用
-
硬件原子指令法:
修改empty需要 mutex 保护,但其实mutex也是一个变量,将导致修改mutex也需要保护,修改mutex的保护也需要保护,没完没了。进而引出了一种硬件原子指令法,使得mutex锁时不能被打断,即硬件实现保护。
- 如果变量已经被锁上(x=1),返回的就是
true
,就会陷入无限循环等待; - 如果没有被锁上(x=0),此时会将x锁上(x=1),并且返回
false
,此时就可以进入临界区,执行完毕再解锁(x=0)。
L2.11 信号量的代码实现
-
定义了一个信号量结构体,包含信号量名称、值、等待唤醒进程队列
-
sem_open
根据信号名打开信号量,若没有则创建 -
sem_wait
中通过开关中断实现保护临界区,信号量值小于零阻塞
-
开关中断来实现信号量的保护
-
根据信号量来判断是否睡眠等待
-
磁盘读完后,睡眠的进程将由中断唤醒
-
隐蔽的队列:tmp作为局部变量,存放在当前进程内核栈中。所以当前进程能找到tmp,建立了索引;而tmp指向下一个进程的PCB;下个进程PCB找到下个进程的内核栈,内核栈找到tmp;tmp再指向下下个进程的PCB...构成了一个队列。
-
则
sleep_on
把自己放到队列中,变成阻塞态,然后schedule
切出去
-
变成阻塞态的睡眠队列,在磁盘中断中被唤醒
-
在
while
中sleep_on
实现将队列中所有进程进行唤醒
L2.12 死锁处理
1、死锁四个必要条件
- 互斥使用
- 不可抢占
- 请求和保持
- 循环等待
2、死锁处理方法
分析死锁:
- 死锁的产生:如果将生产者和消费者的各自的一二行代码交换一下顺序。对于生产者,当empty=0时,mutex执行后锁住,执行empty时阻塞,等待消费者执行V(empty);对于消费者,此时mutex=0,还要等待生产者执行V(mutex),互相需要对方资源,造成死锁。
- 而相对于原本正确的实现:对于生产者,当empty=0时,P(empty)就阻塞了,等待消费者;消费者可以正常执行,因为mutex没有被锁住。
- 死锁预防:破坏出现的条件
- 一次性申请所有需要的资源
- 资源申请必须按序申请
- 死锁避免:检查每个请求资源
- 银行家算法:判断此次请求是否引起死锁
- 死锁检测+恢复:让一些进程回滚,让出资源
- 定时监测或发现CPU资源利用率低时检测,检测到时通过银行家算法看需要回滚多少
- 死锁忽略:直接忽略
- 重启