目录
2.2 需要遵循的原则(空闲让进、忙则等待、有限等待、让权等待)
1 进程同步
回顾:进程具有异步性的特征。异步性是指,各自并发执行的进程以各自独立的、不可预知的速度前进。
再看另外一个例子:进程通信-管道通信
读进程和写进程并发的运行,由于并发必然导致异步性,因此 ‘读数据’ 和 ‘写数据’ 两个操作执行的先后顺序是不确定的。而实际应用中,又必须按照 ‘写数据-->读数据’ 的顺序来执行。进程同步就是讨论如何解决这种异步问题。
同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
2 进程互斥
进程的 ‘并发’ 需要 ‘共享’ 的支持。各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,又如打印机、摄像头这样的I/O设备)
两种资源共享方式:
- 互斥共享方式:系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源
- 同时共享方式:系统中的某些资源,允许一个时间段内由多个进程 ‘同时’ 对它们进行访问
把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(比如摄像机、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。
对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系。进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源后,另一个进程才可以去访问临界资源。
2.1 四个部分(进入区、临界区、退出区、剩余区)
对临界资源的互斥访问,可以在逻辑上分为四个部分:
do{
entry section; //进入区(负责检查是否可进入临界区,若可,则应设置正坐在访问临界资源标志(可理解为‘上锁’),以阻止其它进程同时进入临界区)
critical section; //临界区(访问临界区的那段代码)
exit section; //退出区(负责解除正在访问临界区资源的标志(可理解为‘解锁’))
remainder section; //剩余区(做其它处理)
} while(true)
注:
临界区是进程中访问临界资源的代码段
进入区和退出区是负责实现互斥的代码段
2.2 需要遵循的原则(空闲让进、忙则等待、有限等待、让权等待)
为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:
- 空闲让进。临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区;
- 忙则等待。当已有进程进入临界区时,其他试图进入临界区的进程必须等待;
- 有限等待。对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿)
- 让权等待。当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。
3 进程互斥的软件实现方法
学习提示:
- 理解各个算法的思想、原理
- 集合 “实现互斥要遵循的四个逻辑部分” ,重点理解各算法在进入区、退出区都做了什么
- 分析各算法存在的缺陷(结合 “实现互斥要遵循的四个原则” 进行分析)
3.1 单标志法
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予
int turn = 0; //turn 表示当前允许进入临界区的进程号
P0进程:
while (turn != 0); 1
critical section; 2
turn = 1; 3
remainder section; 4
P1进程:
while (turn != 1); 5
critical section; 6
turn = 0; 7
remainder section; 8
- turn的初值为0,即刚开始只允许0号进程进入临界区。
- 若P1先上处理机,则会一直卡在5。直到P1的时间片用完,发生调度,切换到P0上处理机运行。
- 代码1不会卡住P0,P0可以访问临界区,在P0访问临界区期间时切换回P1,P1还是会卡在5。
- 只有P0在退出区将turn的值修改为1,P1才能进入临界区。
因此,该算法可以实现 ‘同一时刻最多只允许一个进程访问临界区’
turn表示当前允许进入临界区的进程号,而只有当前允许进入临界区的进程在访问了临界区之后,才会修改turn的值。也就是,对于临界区的访问,一定是按P0->P1->P0->...这样轮流访问。
这种必须轮流访问带来的问题,如果允许进入临界区的进程是P0,而P0一直不访问临界区,那么此时虽然临界区空闲,但是不允许P1访问。
单标志法主要问题:违背 “空闲让进” 原则。
3.2 双标志先检查
算法思想:设置一个布尔型数组 flag[],数组中各个元素用来标记各进程想要进入临界区的意愿,比如 “flag[0]=ture” 意味着0号进程P0想要进入临界区。每个进程在进入临界区之前先检查有没有别的进程想要进入临界区,若没有,则把自身对应的flag[i]设为true,之后开始访问临界区。
bool flag[2]; //表示进入临界区意愿的数组
flag[0] = false;
flag[1] = false; //刚开始设置为两个进程都不想进入临界区
P0进程:
while (flag[1]); //1检测P1进程是否想要进入临界区
flag[0] = true; //2标记为P0想要进入临界区
critical section; //3
flag[0] = false; //4
remainder section;
P1进程:
while (flag[0]); 5检测P0进程是否想要进入临界区
flag[1] = true; 6
critical section; 7
flag[1] = false; 8
remainder section;
若按照152637...的顺序执行,P0和P1将会同时访问临界区。
因此,双标志检查法的主要问题是:违反 “忙则等待” 原则。
原因在于,进入区的 “检查” 和 “上锁” 两个处理不是一气呵成的。“检查” 后,“上锁”前可能发生进程切换。
3.3 双标志后检查
算法思想:双标志先检查法的改版。前一个算法的问题是先 “检查” 后 “上锁” ,但是这两个操作又无法一气呵成,因此导致两个进程同时进入临界区的问题。因此,想到先 “上锁” 后 “检查” 的方法来避免上述问题。
bool flag[2]; //表示进入临界区意愿的数组
flag[0] = false;
flag[1] = false; //刚开始设置为两个进程都不想进入临界区
P0进程:
flag[0] = true; //1标记为P0想要进入临界区
while (flag[1]); //2检测P1进程是否想要进入临界区
critical section; //3
flag[0] = false; //4
remainder section;
P1进程:
flag[1] = true; 5标记为P1想进入临界区
while (flag[0]); 6检测P0进程是否想要进入临界区
critical section; 7
flag[1] = false; 8
remainder section;
若按照152637...的顺序执行,P0和P1将都无法进入临界区
因此,双标志检查法虽然解决了 “忙则等待” 的问题,但是又违背了 “空闲让进” 和 “有限等待” 原则,会因各进程都长期无法访问临界资源而产生 “饥饿” 现象。
3.4 Peterson算法
算法思想:如果双方都争着想进入临界区,可以让进程尝试 “孔融让梨” ,主动让对方先使用临界区。
bool flag[2]; //表示进入临界区意愿的数组,初始值都为false
int turn = 0; //turn 表示优先让哪个进程进入临界区
P0进程:
flag[0] = true; //1
turn = 1; //2
while (flag[1] && turn == 1); //3
critical section; //4
flag[0] = false; //5
remainder section;
P1进程:
flag[1] = true; //6 表示自己想要进入临界区
turn = 0; //7 可以优先让对方进入临界区
while (flag[0] && turn == 0); //8 对方想进,且自己是最后一次“让梨”,自己就循环等待
critical section; //9
flag[1] = false; //10 访问临界区,表示自己已经不想访问临界区了
remainder section;
Peterson算法用软件方法解决了进程互斥问题,遵循了空闲让进、忙则等待、有限等待 三个原则,但是依然未遵循让权等待的原则。
不遵循让权等待,会发生“忙等”,就是即使P0进程暂时不进行,也会一直循环,占用CPU
4 进程互斥的硬件实现方法
4.1 中断屏蔽方法
利用 “开/关中断指令” 实现(与原语的实现思想相同,即在某进程开始访问临界区到结束访问为止都不允许被中断,也就不能发生进程切换,因此也不可能发生两个同时访问临界区的情况)
优点:简单、高效
缺点:不适用于多处理机;只适用于操作系统内核进程,不适用于用户进程(因为开/关中断指令只能运行在内核态,这组指令如果能让用户随意使用会很危险)
4.2 TestAndSet(TS指令/TSL指令)
TSL指令是用硬件实现的,执行的过程不允许被中断,只能一气呵成,以下使用C语言描述的逻辑
若刚开始 lock 是 false,则 TSL 返回的 old 值为 false, while 循环条件不满足,直接跳过循环,进入临界区。若刚开始 lock 是 true,则执行 TLS 后 old 返回的值为 true, while 循环条件满足,会一直循环,知道当前访问临界区的进程在退出临界区进行 “解锁”。
相比软件实现方法,TLS 指令吧上锁和检查操作用硬件的方式变成了一气呵成的原子操作。
优点:实现简单,无需像软件实现方法那样严格检查是否有逻辑漏洞;适用于多处理机环境
缺点:不满足 “让权等待” 原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致 “忙等”。
4.3 Swap指令(XCHG指令)
Swap 指令是用硬件实现的,执行的过程不能被中断,只能一气呵成。以下是用C语言描述的逻辑
逻辑上看Swap和TSL并无太大区别,都是先记录下此时临界区是否被上锁(记录在old变量上),再将上锁标记 lock 设置为 true,最后检查 old,如果 old 为 false 则说明之前没有别的进程对临界区上锁,则可跳出循环,进入临界区。
优点:实现简单,无需像软件实现方法那样严格检查是否会有逻辑漏洞;适用于多处理机环境
缺点:不满足 “让权等待” 原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致 “忙等”。
5 信号量机制
用户进程可以通过使用操作系统提供的一对原语来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。
信号量其实就是一个变量(可以是一个整数,也可以是更复杂的记录型变量),可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初始值为1的信号量。
一对原语:wait(S)原语和 signal(S)原语,可以把原语理解为我们自己写的函数,函数名分别为 wait 和 signal,括号里的信号量 S 其实就是函数调用时传入的一个参数。
wait、signal 原语简称 P、V 操作(来自荷兰语 proberen 和 verhogen)。因此,做题时常把wait(S)、signal(S)两个操作分别写为 P(S)、V(S)
5.1 整形信号量
用一个整数型的变量作为信号量,用来表示系统中的某种资源的数量。(与普通整数变量的区别:对信号量的操作只有三种,即 初始化、P操作、V操作)
整形信号量机制下,如果卡在wait的while情况下,由于原语不会被中断执行,是不是意味着该进程就不会被切换?
5.2 记录型信号量
整形信号量的缺陷是存在 “忙等” 问题,因此人们又提出了 “记录型信号量”,即用记录型数据结构表示的信号量。
Eg:某计算机系统中有两台打印机...,则可在初始化信号量S时,将S.value的值设为2,队列S.L设置为空
- CPU为P0服务,执行wait,然后 value - 1 ,判断 value 不小于0,所以分配打印机给P0
- CPU切换到P1,为P1进程服务,执行wait,value - 1,判断 value = 0,所以系统分配打印机给P1
- CPU切换到P2,为P2进程服务,执行wait,value - 1,此时value = -1 < 0,使用block原语,阻塞进程,P2进程挂到打印机等待队列 S.value = -1,有一个进程在等待
- CPU切换到P3,同P2,P3进程挂到打印机等待队列,S.value = -2,有两个进程在等待
- P0进程使用打印机完毕,signal原语,value + 1,此时 S.value 还是 < 0,执行 wakeup 原语,唤醒进程P2
。。。。
对信号量 S 的一次 P 操作意味着进程请求一个单位的该类资源,因此需要执行 S.value--,表示资源数减1,当 S.value < 0 时表示该类资源已分配完毕,因此进程调用 block 原语进行自我阻塞(当前运行的进程从运行态到阻塞态),主动放弃处理机,并插入该类资源的等待队列 S.L 中。可见,该机制遵循了 “让权等待” 原则,不会出现 “忙等” 现象。
对信号量 S 的一次 V 操作意味着进程释放一个单位的该类资源,因此需要执行 S.value++,表示资源数加1,若加1后仍是 S.value <= 0,表示依然有进程在等待资源,因此调用 wakeup 原语唤醒等待队列的第一个进程(被唤醒进程从阻塞态->就绪态)。
6 用信号量机制实现
6.1 进程互斥
信号量初始值设为1可以实现对该种资源的互斥访问
不同的临界区资源需要定义不同的名字
6.2 进程同步
进程同步:要让各并发进程按要求有序的推进。
用信号量实现进程同步:
- 分析什么地方需要实现 “同步关系” ,即保证 “一前一后” 执行的两个操作(或两句代码)
- 设置同步信号量S,初始为0
- 在 “前操作” 之后执行 V(S)
- 在 “后操作” 之前执行 P(S)
6.3 信号量机制实现前驱关系
参考:bilibili操作系统王道考研课