1.本文内容总结自 B站 《操作系统-哈工大李治军老师》,内容非常棒,墙裂推荐;
2.操作系统使用信号量实现进程同步(合作),走走停停,推进多进程合理有序向前执行;
3.靠临界区保护信号量,靠信号量实现进程同步;
0)信号量
1)问题

图解: 并发问题例子,empty = -1,但有2个生产者进程睡眠,这说明empty信号量的值是错误的;
1)竞争条件:

图片解说:
错误和调度顺序有关;
2)解决竞争条件的方法

上图显示的执行顺序如下:
| 时间 | 进程 | 代码或操作 |
| 1 | P1 | 检查并给empty上锁 |
| 2 |
| P1.register=empty |
| 3 |
| P1.register=P1.register – 1 |
| 4 | P2 | 检查empty锁;锁不可用,则阻塞; |
| 5 | P1 | Empty = P1.register |
| 6 |
| 给empty 开锁(释放锁) |
| 7 | P2 | 检查并给empty上锁 |
| 8 |
| P2.register=empty |
| 9 |
| P2.register=P2.register – 1 |
| 10 |
| Empty = P2.register |
| 11 |
| 给empty开锁(释放锁) |
其中,以下代码只允许同时只有一个进程P来访问;
【代码段1】
- P.register = empty
- P.register = P.register – 1;
- Empty = P.register
1)临界区定义:
补充: 临界区中存放操作信号量的代码;

3)重要工作: 找出进程中的临界区代码 ;
4)读写信号量的代码一定是临界区;
5)进程代码结构
6)如何编写临界区代码
0)3个原则
1)基本原则: 互斥进入(或不能同时进入): 如果一个进程在临界区中执行,则其他进程不允许进入; (原则1)

2)好的临界区保护原则

轮换法解说:
轮换法问题:

1)轮换法类似于值日;
2)问题:可能丈夫妻子都去买了牛奶,买了两瓶牛奶,不满足需求(我们只买一瓶牛奶);
3)改进方法: 无论丈夫还是妻子,买牛奶前,在冰箱上留便条以示标记;

图片解说: 由左边代码 改进为 右边代码leavenote;
1)标记法的问题(两个进程不同执行顺序造成的死锁问题);
表1 两个进程不同执行顺序造成的死锁问题
| 进程p0 | 进程p1 |
| Flag[0]=true |
|
|
| Flag[1]=true |
| While(flag[1])// 自旋阻塞(有问题) |
|
|
| While(flag[0]) ;// 自旋阻塞 (有问题) |
两个进程都阻塞,则发生了死锁,程序挂起;即 进程p0 p1的进入请求会无限等待(不满足有限等待规则);
底层原因在于, 进程1,进程2 依据的是不同变量来判断是否应该执行,而不是同一个变量,若是同一个变量就可以解决;
补充:回到买牛奶的场景:根据表1的执行顺序,那丈夫和妻子都会看到对方留了条,都不会去买牛奶,这就造成了死锁;
2)解决方法-非对称标记

非对称标记执行细节(丈夫比妻子更加忙碌:丈夫看见妻子留条了,还是会继续等待;而妻子看见丈夫留条了,不会等待直接跳过并撤销留条)
| 丈夫A | 妻子B |
| Leave note A // 丈夫A留条 While(note B){ // 若妻子留条,则持续等待; // 当妻子移除留条后,循环结束 do nothing } If (noMilk) { // 若没有牛奶 buy milk // 再去买牛奶 } Remove note A;// 移除留条 | Leave note B; // 妻子B留条 If (noNote A) { // 若丈夫A没有留条 If (noMilk) { // 若没有牛奶 Buy milk // 买牛奶 } } Remove note B;// 移除留条 |
1)结合了标记和轮转两种思想;

【代码解说】

问题: 上述是2个进程同步执行的调度算法;多个进程会怎么办 ?
1)进程进入临界区之前,取号(非0标记);

2) 面包算法正确性

小结: 面包店算法的代码实现有点复杂;
1)引入软硬件协同设计思想,如为了让一个路由算法执行更加高效,需要对路由器硬件做一些改进;
2)回顾临界区定义:
3)被调度:另一个进程只有被调度才能执行,才可能进入临界区;

如何阻止调度? 调度是通过时钟中断起作用,关闭时钟中断就阻止了调度;
| cli(); 关中断 |
| 临界区; |
| sti();开中断; |
| 剩余区; |
通过开关中断在 系统多cpu情况下不起作用;
因为:
写在前面: 硬件原子指令法,是引入软硬件协同设计思想后的产物;
1)背景
为了多进程合作,交替执行,不出错,采用临界区保护信号量使得同时只能允许一个进程进入; 算法设计如下:
上锁,开锁,其实都是对变量进行操作;
如以 mutex作为变量,mutex=1表示有资源,可以进入,即锁是开着的;而mutex=0表示没有资源,不能进入,当前进程阻塞,即上锁状态;
【上锁问题】
2)mutex修改代码的问题
2.1)修改mutex伪代码
| 步骤 | 代码 |
| 1 | P1.register=mutet;// 内存mutex变量送入寄存器 |
| 2 | P1.resigter=p1.register-1;// 寄存器值减1(当然可以是其他计算操作) |
| 3 | Mutex=p1.register;// 计算后的寄存器值送入内存mutex |
2.2)问题
小结:
3)修改mutex存在并发问题的解决方法
4)硬件原子指令例子-testAndSet(里面的操作要么全部成功,要么全部失败,中途不会切换)
- // 原子指令 TestAndSet
- boolean TestAndSet(boolean &x)
- {
- boolean rv = x;
- x = true;
- return rv;
- }
- // 临界区加锁开锁代码
- while(TestAndSet(&lock))
- {
- 临界区;
- lock = false;
- 剩余区;
- }
原子指令代码解说:

5) 有个问题: 硬件原子指令是否适合多cpu的情况?
1)信号量定义:
2)临界区定义:
3)信号量是一个整型数字,对信号量的修改存在并发问题,所以用临界区保护信号量;
4)使用临界区保护信号量,那临界区也需要保护,临界区保护原则如下:
5)临界区保护的7种方法
6)临界区保护信号量
7)对信号量修改,采用硬件原子指令,要么全部成功,要么全部失败,中途不会切换;