第六章
背景:为什么并发执行要互斥:共享数据的并发访问可能会产生数据的不一致
互斥使用临界资源(物理设备、软件、变量)
临界区:使用临界资源的代码:空闲让进,忙时等待-互斥,有限等待
Peterson算法:软件,解决互斥,但是只能两个进程,忙时等待不实用,必须知道和谁互斥
硬件同步:关中断、硬件指令,会用且知道内容会用
对于临界区使用关中断可实现但不可行,因为若该程序很长,关中断会导致cpu始终被占据。 wait和signal可以用关中断实现,因为程序很短。指令,intelx86系列都是int
信号量:OS支持;二进制、计数(忙式)、纪录(让权;是特点)
经典同步问题,哲学家进餐问题的改进:最多四个人;奇数拿左,偶数拿右
管程:概念-类的定义,描述一个资源互斥的使用:一组属性、方法的初始化,条件变量-两种操作-wait和signal。--实现进程的同步和互斥的一种机制,应用程序实现方法互斥,管程内执行的只有一个程序
1 【协作进程】:可以与系统内执行的其他进程相互影响的进程
2 【竞争条件RC】:多个程序并发访问和操作同一数据且执行结果与访问发生的特定顺序有关系
3 【临界区】:不管是硬件资源还是软件资源,多个进程必须互斥的对它进行访问,每个进程中访问临界资源的那段代码称为临界区;(内存中的两个或多个代码块,他们共享一些资源,如果这两个代码块并发执行的话可能产生RC条件,所以这两个或多个代码块必须需要实现互斥,那么这样的两个或多个代码块互称临界区)
4 【进入区】:实现请求进入临界区的代码段
5 【互斥】(解决临界区必须满足):如果进程P在其临界区内执行,那么其他的进程都不能在其临界区内执行
6 【前进】(解决临界区必须满足):如果没有进程在其临界区内执行且有进程需进入临界区,那么只有那些不在剩余区内执行的进程可参加选择,以确定是哪一个进程进入其临界区,且这种选择不能无限的推迟
7 【有限等待】(解决临界区必须满足):从一个进程做出进入临界区的请求,直到该请求允许为止,其他进程允许进入其临界区的次数有上限
8 【抢占内核】是指允许内核模式的进程被抢占; 抢占内核更适合实时编程,因为它能允许实时进程抢占处于内核模式运行的其他进程
9 【Peterson 算法】:基于软件的临界区问题的解答; 算法如下所示:两个进程P0、P1,变量turn表示哪个进程可以进入临界区; flag表示哪个进程想要进入临界区[cpp] view plaincopy
1 do{
2 flag[i] = true;
3 turn = j;
4 while(flag[j] && turn == j);
5 //
6 //临界区
7 //
8 flag[i] = false;
9 //
10 //剩余区
11 //
12 }while(true)
10
11 【锁】进程在进入临界区之前必须得到锁,而在退出临界区时释放锁
12 对于单处理器环境,临界区问题可以简单地加以解决:在修改共享变量时要禁止中断出现
13 【原子的】:不可中断地指令
14 TestAndSet(不可中断)[cpp] view plaincopy
1 bool TestAndSet(bool *target){
2 bool rv = *target;
3 *target = true;
4 return rv;
5 }
15 使用,声明一个bool 的 lock ,初始化为false[cpp] view plaincopy
1 do{
2 while(TestAndSet(&lock))
3 ;//do nothing
4 //
5 //临界区
6 //
7 lock = false;
8 }while(true)
16
17 Swap(不可中断)[cpp] view plaincopy
1 void Swap(bool *a, bool *b)
2 {
3 bool temp = *a;
4 *a = *b;
5 *b = temp;
6 }
18 使用,声明一个bool 的lock ,初始化为false[cpp] view plaincopy
1 do{
2 key = true;
3 while(key == true)
4 Swap(&lock, &key);
5 //
6 //临界区
7 //
8 lock = false;
9 }while(true)
19
20 上面两种情况解决了互斥,但是并没有解决有限等待要求;下面是使用TestAndSet 的有限等待互斥算法[cpp] view plaincopy
1 do{
2 waiting[i] = true
3 key = true;
4 while(waiting[i] && key)
5 key = TestAndSet(&lock);
6
7 waiting[i] = false;
8 //
9 //临界区
10 //
11 j = (i + 1)%n;
12 while((j != 1) && !waiting[j])
13 j = (j + 1)%n;
14
15 if(j == i)
16 lock = false;
17 else
18 waiting[j] = false;
19 }while(true)
21 分析:bool waiting[n] 初始化为false; bool lock 初始化为 false; 只有waiting[i] = false 或 key = false时,进程Pi才进入临界区;只有当其他进程离开其临界区时,变量waiting[i] 的值才能变成false; 每次只有一个waiting[i] 被设置为false,以满足互斥要求
22 若多个进程的某些操作在执行的时间、顺序上有制约,则称这几个进程是【同步】关系
23 信号量S(将信号量初始化为1)[cpp] view plaincopy
1 wait(s)
2 {
3 while(s <= 0)
4 ;//do nothing
5 s--;
6 }
7
8 signal(s)
9 {
10 s++;
11 }
24在wait()和signal()操作中,对信号量整形值的修改必须不可分地执行,即当一个进程修改信号量值时,不能有其他进程同时修改同一信号量的值
25 【忙等待】:当一个进程位于其临界区内时,任何其他试图进入其临界区的进程都必须在其进入代码中连续地循环; 这浪费了CPU时钟
26 【自旋锁】:处理办法是用忙等待的信号量也叫做自旋锁,因为进程在等待锁时还在运行(自旋锁有其优点,进程在等待锁时不进行上下文切换; 所以如果锁的占有时间短的话,自旋锁就有用了;)多用于多处理器系统
27 【死锁】:两个或多个进程无限地等待一个事件,而该事件只能由这些等待进程之一产生; 这样,这几个进程就无法获得资源而继续运行下去;(组内每个进程都是等待一个事件,而该事件只能由组中的另一个进程产生)
28 【无限期阻塞】、【饥饿】:进程在信号量内无限期等待
29 【管程】:有程序员定义的、在管程内互斥的操作; 管程内定义的子程序只能访问位于管程内的局部声明的变量和新手参数; 管程结构确保一次只有一个进程能在管程内活动
30 【适应互斥】:保护对每个临界数据项的访问
31 【十字转门】:一个队列结构,包含有阻塞在锁上的进程
32 【触发状态】:表示对象可用且线程在获取它时不会阻塞
33 【非触发状态】:表示对象不可用且线程在获取它时会阻塞
34 当线程阻塞在非触发调度对象上时。其状态从就绪转变为等待,且该进程被放到对象的等待队列上; 当调度对象的状态成为触发时,内核会检查是不是有线程在该对象上等待,如果有,内核将改变一个或多个线程的状态,使他们从等待状态切换到就绪状态以重新执行
35 如果一个线程试图获取处于非触发状态的互斥调度对象是,那么该线程会被挂起,并被放到互斥对象的等待队列上
36【事务】:执行单个逻辑功能的一组指令或操作37处理事务的关键就是不管计算机系统出现什么错误,都要保证事务的原子性(不可被中断); 可以认为事务是访问且可能更新各种驻留在磁盘文件中的数据项的程序单元38【提交】:commit,表示事务已经成功执行;39【撤销】:abort,表示事务因各种逻辑错误,事务必须停止操作40【回退】:被中止的事务必须对其修改的数据不产生任何影响,以确保原子性; (必须恢复到事务刚开始执行之前)41【稳定存储】:驻留在稳定存储上的信息绝不会损失42【先记日志后操作】:确保原子性的一种方法, 在稳定存储上记录有关事务对其访问的数据所做各种修改的描述信息43【检查点】:检查点之前是已经提交了的数据,可以只从检查点之后的事务恢复为原来的值;(只要出现在日志中,就执行redo(Tk)更新值; 如果没有出现在日志中,就执行undo(Tk),恢复值)
44 【串行调度】:每个事务原子的执行的调度;属于单个事务的指令在调度中一起出现,因此,对于n个事务的集合,共有n!个不同的有效的串行调度
45 【冲突可串行化】:调度S可以经过一系列非冲突操作的交换而转换成串行调度的话, S就是冲突可串行化
46 【两阶段加锁协议】:要求每个事务按两个阶段来发出加锁和放锁请求;(增长阶段:事务可以获取锁,但不能释放锁; 收缩阶段:事务可释放锁,但不能获取新的锁)
47 【时间戳】:对于系统内的每个事务Ti,都为之关联一个唯一固定的时间戳,并记为TS(Ti),这个在事务Ti开始执行之前由系统赋予
48 如果TS(Ti) < TS(Tj),那么系统必须确保所产生的调度相当于事务Ti在事务Tj之前的串行化调度